多线程和套接字的C#问题

时间:2013-10-07 15:55:11

标签: c# multithreading list sockets

首先,我只是想让你知道我不是编程的新手,应该更容易帮助我:)

我在使用Socket在C#中制作多线程聊天时出现问题。

我有3个帖子:

  • void ListenSocketConnection:检查可以连接的Socket。已连接的套接字已添加到列表<>
  • void CheckIfClientStillConnectedThread:检查Socket是否已断开连接。已从List<>
  • 中删除已断开连接的套接字
  • void ReceiveDataListener:检查Socket是否收到数据
    • 这是问题所在。如果第一个或第二个线程从List<>中删除Socket,则'foreach(ClientManager中的ClientManager cManager)'将引发异常。
    • 这是第二个问题。如果套接字在该foreach期间断开连接,则“在clientList中预设ClientManager cManager”将引发异常:DisposedException

你对我如何解决这个问题有什么建议吗?

这是我的代码:

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
    }
}

2 个答案:

答案 0 :(得分:4)

  1. 有多个线程,但我没有看到任何同步。那是不对的。使用锁来保护可变共享状态。
  2. 不是对所有套接字进行集中管理,轮询和检查DataAvailable,为什么不在每个套接字或异步IO中使用一个线程?这样你一次只能管理一个套接字。不需要民意调查。您只需调用Read(或其异步版本)并等待数据到达。这是处理套接字的更好的范例。基本上,如果您的套接字代码包含DataAvailable或轮询,那么您将违反最佳实践(并且可能在某处出现错误)。想想如何在不使用这两者的情况下解决这个问题。这是可能的,而且更好。
  3. ReceiveDataListener中假设,如果数据可用,则整个消息可用。这是错误的,因为TCP是面向流的。您可以以任意小块的形式接收发送的数据。按照我的观点(2)解决这个问题。
  4. 详细说明(2):这基本上是一个演员模型。每个插槽一个演员。无论是使用线程实现actor,使用async/await还是使用传统异步IO都无关紧要。

    希望这会有所帮助。请随时在下面的评论中提出后续问题。

答案 1 :(得分:0)

该集合正在被多个线程修改,因此每次查询时都可以计数。因此,您应该将其设置为固定数量;即在循环之前然后遍历列表。此外,由于您要移除元素,因此倒计时而不是倒计时是更好的选择。

请考虑以下代码:

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);
    }
}