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

本文关键字:问题 套接字 多线程 | 更新日期: 2023-09-27 18:34:11

首先,我只是想让你知道我对编程并不陌生,应该更容易帮助我:)

我在使用 C# 中使用套接字进行的多线程聊天时遇到问题。

我有 3 个线程:

  • void ListenSocketConnection :检查可以连接的套接字。连接的套接字将添加到列表中<>
  • void CheckIfClientStillConnectedThread:检查套接字是否断开连接。断开连接的套接字将从列表中删除<>
  • void 接收数据
  • 侦听器:检查套接字是否接收到数据
    • 这就是问题所在。如果第一个或第二个线程从列表中删除套接字<>,则"foreach (clientList 中的 ClientManager cManager("将引发异常。
    • 这是第二个问题。如果套接字在此期间断开连接,"foreach ClientManager cManager in clientsList("将引发异常: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
    }
}

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

  1. 有多个线程,但我没有看到任何同步。这是不正确的。使用锁来保护可变共享状态。
  2. 与其集中管理所有套接字,轮询和检查DataAvailable,为什么不只使用每个套接字一个线程或异步IO?这样,您一次只能管理一个套接字。无需轮询。您只需调用Read(或其异步版本(并等待数据到达。这是处理套接字的更好范例。基本上,如果您的套接字代码包含DataAvailable或轮询,那么您将违反最佳实践(并且可能在某处存在错误(。想想如何在不使用两者的情况下解决这个问题。这是可能的,而且更好。
  3. ReceiveDataListener中,您假设,如果数据可用,则整个消息可用。这是错误的,因为TCP是面向流的。您可以以任意小块的形式接收发送的数据。按照我的观点(2(解决这个问题。

详细说(2(:这基本上是一个演员模型。每个套接字一个演员。无论是使用线程、使用 async/await 还是使用旧版异步 IO 实现执行组件都无关紧要。

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

集合正在由多个线程修改,因此每次询问时计数都可能非常多。 因此,您应该将其设置为固定金额;即在循环之前,然后遍历列表。 此外,倒计时而不是向上倒计时是更好的选择,因为您要删除元素。

考虑以下代码:

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