简体   繁体   中英

C# Issues with Multi-Threading and Socket

First of all, I just want to let you know that I am not new to programming, it should be easier to help me :)

I am having issues with my Multi-Threaded Chat that I am making in C# with Socket.

I have 3 threads :

  • void ListenSocketConnection : Check for Socket that could connect. Connected Socket are added into a List<>
  • void CheckIfClientStillConnectedThread : Check if a Socket disconnected. Disconnected Socket are removed from List<>
  • void ReceiveDataListener : Check if a Socket received data
    • Here is the issue. If the first or second thread remove a Socket from the List<>, the 'foreach (ClientManager cManager in clientsList)' will raise an exception.
    • Here is the second issue. If a socket disconnect during that foreach, 'foreach ClientManager cManager in clientsList)' will raise an exception : DisposedException

Do you have any tips on how I could fix this?

Here is my code :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.ComponentModel;
using System.Threading;

namespace JAChat.Library
{
    class SocketServer
    {
        private Socket socketServer;
        private BackgroundWorker bwSocketConnectListener;
        private BackgroundWorker bwCheckIfConnected;
        private BackgroundWorker bwReceiveDataListener;
        private List<ClientManager> clientsList;

        #region Constructor
        public SocketServer(int port)
        {
            clientsList = new List<ClientManager>();

            socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            socketServer.Bind(new IPEndPoint(IPAddress.Any, port));
            socketServer.Listen(100);

            bwSocketConnectListener = new BackgroundWorker();
            bwSocketConnectListener.DoWork += new DoWorkEventHandler(ListenSocketConnection);
            bwSocketConnectListener.RunWorkerAsync();

            bwCheckIfConnected = new BackgroundWorker();
            bwCheckIfConnected.DoWork += CheckIfClientStillConnectedThread;
            bwCheckIfConnected.RunWorkerAsync();

            bwReceiveDataListener = new BackgroundWorker();
            bwReceiveDataListener.DoWork += ReceiveDataListener;
            bwReceiveDataListener.RunWorkerAsync();
        }
        #endregion

        #region Getter
        public List<ClientManager> connectedClients
        {
            get
            {
                return clientsList;
            }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Parse and send the command object to targets
        /// </summary>
        public void sendCommand(Command cmd)
        {
            BackgroundWorker test = new BackgroundWorker();
            test.DoWork += delegate {
                foreach(ClientManager cManager in clientsList){
                    cManager.sendCommand(cmd);
                }
            };
            test.RunWorkerAsync();
        }

        /// <summary>
        /// Disconnect and close the socket
        /// </summary>
        public void Disconnect()
        {
            socketServer.Disconnect(false);
            socketServer.Close();
            socketServer = null; //Stop some background worker
        }
        #endregion

        #region Private Methods
        private void ListenSocketConnection(object sender, DoWorkEventArgs e)
        {
            while (socketServer != null)
            {
                //Get and WAIT for new connection
                ClientManager newClientManager = new ClientManager(socketServer.Accept());
                clientsList.Add(newClientManager);
                onClientConnect.Invoke(newClientManager);
            }
        }

        private void CheckIfClientStillConnectedThread(object sender, DoWorkEventArgs e){
            while(socketServer != null){
                for(int i=0;i<clientsList.Count;i++){
                    if(clientsList[i].socket.Poll(10,SelectMode.SelectRead) && clientsList[i].socket.Available==0){
                        clientsList[i].socket.Close();
                        onClientDisconnect.Invoke(clientsList[i]);
                        clientsList.Remove(clientsList[i]);
                        i--;                        
                    }
                }
                Thread.Sleep(5);
            }
        }

        private void ReceiveDataListener(object unused1, DoWorkEventArgs unused2){
            while (socketServer != null){
                foreach (ClientManager cManager in clientsList)
                {
                    try
                    {
                        if (cManager.socket.Available > 0)
                        {
                            Console.WriteLine("Receive Data Listener 0");
                            //Read the command's Type.
                            byte[] buffer = new byte[4];
                            int readBytes = cManager.socket.Receive(buffer, 0, 4, SocketFlags.None);
                            Console.WriteLine("Receive Data Listener 1");
                            if (readBytes == 0)
                                break;
                            Console.WriteLine("Receive Data Listener 2");
                            CommandType cmdType = (CommandType)(BitConverter.ToInt32(buffer, 0));
                            Console.WriteLine("Receive Data Listener 3");

                            //Read the sender IP size.
                            buffer = new byte[4];
                            readBytes = cManager.socket.Receive(buffer, 0, 4, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            int senderIPSize = BitConverter.ToInt32(buffer, 0);

                            //Read the sender IP.
                            buffer = new byte[senderIPSize];
                            readBytes = cManager.socket.Receive(buffer, 0, senderIPSize, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            IPAddress cmdSenderIP = IPAddress.Parse(System.Text.Encoding.ASCII.GetString(buffer));

                            //Read the sender name size.
                            buffer = new byte[4];
                            readBytes = cManager.socket.Receive(buffer, 0, 4, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            int senderNameSize = BitConverter.ToInt32(buffer, 0);

                            //Read the sender name.
                            buffer = new byte[senderNameSize];
                            readBytes = cManager.socket.Receive(buffer, 0, senderNameSize, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            string cmdSenderName = System.Text.Encoding.Unicode.GetString(buffer);

                            //Read target IP size.
                            string cmdTarget = "";
                            buffer = new byte[4];
                            readBytes = cManager.socket.Receive(buffer, 0, 4, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            int targetIPSize = BitConverter.ToInt32(buffer, 0);

                            //Read the command's target.
                            buffer = new byte[targetIPSize];
                            readBytes = cManager.socket.Receive(buffer, 0, targetIPSize, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            cmdTarget = System.Text.Encoding.ASCII.GetString(buffer);

                            //Read the command's MetaData size.
                            string cmdMetaData = "";
                            buffer = new byte[4];
                            readBytes = cManager.socket.Receive(buffer, 0, 4, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            int metaDataSize = BitConverter.ToInt32(buffer, 0);

                            //Read the command's Meta data.
                            buffer = new byte[metaDataSize];
                            readBytes = cManager.socket.Receive(buffer, 0, metaDataSize, SocketFlags.None);
                            if (readBytes == 0)
                                break;
                            cmdMetaData = System.Text.Encoding.Unicode.GetString(buffer);

                            //Create the command object
                            Command cmd = new Command(cmdType, cmdSenderIP, cmdSenderName, IPAddress.Parse(cmdTarget), cmdMetaData);
                            this.onCommandReceived(cmd);
                        }
                    }
                    catch (ObjectDisposedException) {/*Le socket s'est déconnectée pendant le for each. Ignore l'érreur et retourne dans le while*/ }
                    catch (InvalidOperationException) { /* clientsList a été modifié pendant le foreach et délanche une exception. Retour while*/}
                }                
            }
            Console.WriteLine("Receive data listener closed");
        }
        #endregion

        #region Events
        public delegate void OnClientConnectEventHandler(ClientManager client);
        /// <summary>
        /// Events invoked when a client connect to the server
        /// </summary>
        public event OnClientConnectEventHandler onClientConnect = delegate { };

        public delegate void OnClientDisconnectEventHandler(ClientManager client);
        /// <summary>
        /// Events invoked when a client disconnect from the server
        /// </summary>
        public event OnClientDisconnectEventHandler onClientDisconnect = delegate { };

        public delegate void OnCommandReceivedEventHandler(Command cmd);
        /// <summary>
        /// Events invoked when a command has been sent to the server
        /// </summary>
        public event OnCommandReceivedEventHandler onCommandReceived = delegate { };
        #endregion
    }
}
  1. There are multiple threads but I don't see any synchronization. That's incorrect. Use a lock to protect mutable shared state.
  2. Instead of having this central management of all sockets, the polling and the checking of DataAvailable , why don't you just use either one thread per socket or async IO? That way you only ever manage one socket at a time. No polling necessary. You just call Read (or its async version) and wait for the data to arrive. This is a much better paradigm to deal with sockets. Basically, if your socket code contains DataAvailable or polling, you are going against best-practices (and likely have a bug somewhere). Think about how you would solve this without using the two. It is possible, and better.
  3. In ReceiveDataListener you assume, that if data is available that an entire message is available. That's wrong because TCP is stream-oriented. You can receive the sent data in arbitrarily small chunks. Going with my point (2) fixes this.

To elaborate on (2): this is basically an actor model. One actor per socket. Whether you implement the actor using a thread, using async/await or using legacy async IO does not matter.

Hope this helps. Feel free to ask follow-up questions in the comments below.

The collection is being modified by multiple threads, so the count may very every time it is interrogated. As such, you should set it to a fixed amount; ie before the loop and then iterate through the list. Also, a count down rather than a count up is a better option since you are removing the elements.

Consider the following code :

private void CheckIfClientStillConnectedThread(object sender, DoWorkEventArgs e)
{
    while (socketServer != null)
    {
        int count = clientsList.Count -1;
        for (int i=count; i >= 0 ; i--)
        {
            if (clientsList[i].socket.Poll(10, SelectMode.SelectRead) && clientsList[i].socket.Available == 0)
            {
                clientsList[i].socket.Close();
                onClientDisconnect.Invoke(clientsList[i]);
                clientsList.Remove(clientsList[i]);
            }
        }
        Thread.Sleep(5);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM