C#中的异步套接字服务器,客户端通过套接字服务器进行客户端通信

Posted

技术标签:

【中文标题】C#中的异步套接字服务器,客户端通过套接字服务器进行客户端通信【英文标题】:Asynchronous Socket Server in C#, client to client communication through socket server 【发布时间】:2018-06-30 03:18:28 【问题描述】:

我正在尝试使用 c# 开发服务器/客户端异步套接字。我已按照MSDN Link 上的指南进行操作。就我而言,套接字服务器在特定端点上侦听,许多客户端可以一次连接到服务器,客户端可以与服务器通信,服务器可以与客户端通信。假设客户端 1 和客户端 2 与服务器连接,客户端 1 可以向服务器发送消息,服务器可以向客户端 1 发送消息,客户端 2 的情况也是如此。现在我希望客户端应该能够通过服务器相互通信。例如;客户端 2 想与客户端 1 通信,因为客户端 2 将向服务器发送消息(此消息将包含一些预设字符;),然后服务器将从客户端 2 接收文本并获取客户端 1 的处理程序并发送这条消息给客户端 1,客户端 1 将响应服务器,现在我想将客户端 1 对该消息的响应发送给客户端 2,但我不知道该怎么做,因为客户端 1 通过它自己的处理程序与服务器通信,我在这里感到震惊,我们将不胜感激! 我的代码如下:

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



namespace SocketServer

    // State object for reading client data asynchronously  
    public class StateObject
    
        // Client  socket.  
        public Socket workSocket = null;
        // Size of receive buffer.  
        public const int BufferSize = 1024;
        // Receive buffer.  
        public byte[] buffer = new byte[BufferSize];
        // Received data string.  
        public StringBuilder sb = new StringBuilder();

        public int clientNumber;
    
    public class AsyncSocketServer
    
        public static ManualResetEvent allDone = new ManualResetEvent(false);

        public static Dictionary<int, StateObject> Clients = new Dictionary<int, StateObject>();

        public static int connectedClient = 0;




        public AsyncSocketServer()
        


         
        public static void startListening() 

            Byte[] bytes = new Byte[1024];
            int Port = 1122;

            IPAddress IP = IPAddress.Parse("127.0.0.1");
            IPEndPoint EP = new IPEndPoint(IP, Port);
            Socket listner = new Socket(IP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);



            try
            
                listner.Bind(EP);
                listner.Listen(100);

                while (true)
                
                   allDone.Reset();

                    Console.WriteLine("Waiting for the Connection......");

                    listner.BeginAccept(new AsyncCallback(AcceptCallBack), listner);

                    allDone.WaitOne();
                


            
            catch(Exception e)
            
                Console.WriteLine("Exception Occured ! in start listening method "+e.ToString());
            

             Console.WriteLine("\nPress ENTER to continue...");  
            Console.Read();  

        

        public static void AcceptCallBack(IAsyncResult ar)
        
            connectedClient++;
            Console.WriteLine("client number " + connectedClient);
            allDone.Set();



            Socket listner = (Socket)  ar.AsyncState;
            Socket handler = listner.EndAccept(ar);

            StateObject state = new StateObject();
            state.clientNumber = connectedClient;

            Clients.Add(connectedClient, state);
           Console.WriteLine("total clients 0",Clients.Count());

            state.workSocket = handler;
            handler.BeginReceive(state.buffer, 0, StateObject.BufferSize,0,new AsyncCallback(ReadCallBack),state);

        
        public static void ReadCallBack(IAsyncResult ar)
          

        String content = String.Empty;



        // Retrieve the state object and the handler socket  
        // from the asynchronous state object.  
        try  
        StateObject state = (StateObject) ar.AsyncState;
        state.sb.Clear();
        Socket handler = state.workSocket;  

        // Read data from the client socket.   
        int bytesRead = handler.EndReceive(ar);  

        if (bytesRead > 0)   
            // There  might be more data, so store the data received so far.  
            state.sb.Append(Encoding.ASCII.GetString(  
                state.buffer,0,bytesRead));  

            // Check for end-of-file tag. If it is not there, read   
            // more data.  

            content = state.sb.ToString();

            if (content.Substring(0, 3) == "cmd") 
                foreach (StateObject Client in Clients.Values) 
                    if (Client.clientNumber == 1)  
                        Console.WriteLine("value is "+Client.clientNumber);
                        if (isClientConnected(Client.workSocket))
                            Send(Client.workSocket, "did you receive my message");
                            //now client number 1 will response through its own handler, but i want to get response of 
                            //client number 1 and return this response to client number 2

                        
                        else 
                            string responsemsg = "client number " + Client.clientNumber + " is disconnected !";
                            Console.WriteLine(responsemsg);
                            Send(handler,responsemsg);
                        
                    

                
            

            Console.WriteLine("Read 0 bytes from client 1 socket. \n Data : 2",
                    content.Length, state.clientNumber,content);
            // Echo the data back to the client.  

            if (isClientConnected(handler))
            
                Send(handler, content);
            
            handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallBack), state);

        
        
        catch (SocketException e)
        
            //once if any client disconnected then control will come into this block
            Console.WriteLine("Socket Exception Occured in Read Call Back : " + e.Message.ToString());

        
        catch (Exception e)
        
            //once if any client disconnected then control will come into this block
            Console.WriteLine("Exception Occured in Read Call Back : " + e.Message.ToString());

        
        
        private static void Send(Socket handler, String data)
        
            // Convert the string data to byte data using ASCII encoding.  
            byte[] byteData = Encoding.ASCII.GetBytes(data);

            // Begin sending the data to the remote device.  
            handler.BeginSend(byteData, 0, byteData.Length, 0,
                new AsyncCallback(SendCallback), handler);
        

        private static void SendCallback(IAsyncResult ar)
        
            try
            
                // Retrieve the socket from the state object.  
                Socket handler = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.  
                int bytesSent = handler.EndSend(ar);
                Console.WriteLine("Sent 0 bytes to client.", bytesSent);



                //handler.Shutdown(SocketShutdown.Both);
                //handler.Close();

            
            catch (Exception e)
            
                Console.WriteLine(e.ToString());
            
        

        public static bool isClientConnected(Socket handler)

            return handler.Connected;
        
        public static int Main(string[] args)
        

            startListening();


            return 0;


        
    

【问题讨论】:

对于未来的读者:.Net Core 团队建议不要在这个问题中使用“旧”链接,而是使用 Async 后缀方法。 github.com/dotnet/core/issues/4828#issuecomment-643619106 【参考方案1】:

对于任何基于套接字的复杂应用程序,我建议使用像DotNetty 这样的套接字库来抽象传输层并让您专注于应用程序逻辑。查看他们的SecureChat example,它可能与您想要实现的目标非常相似。

我整理了一个 DotNetty 服务器的快速示例,它允许您通过让客户端向服务器注册,然后让服务器在客户端之间路由消息来在客户端之间发送命令。

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using DotNetty.Transport.Channels;
using Newtonsoft.Json;
using System.IO;

namespace MultiClientSocketExample

    public enum Command
    
        Register = 1,  // Register a new client
        SendToClient = 2, // Send a message from one client to antoher
        DoClientAction = 3 // Replace this with your client-to-client command
    

    // Envelope for all messages handled by the server
    public class Message
    
        public string ClientId  get; set; 
        public Command Command  get; set; 
        public string Data  get; set; 
    

    // Command for seding a message from one client to antoher.   
    // This would be serialized as JSON and stored in the 'Data' member of the Message object.
    public class SendToClientCommand
    
        public string DestinationClientId  get; set;   // The client to receive the message

        public Command ClientCommand  get; set;  // The command for the destination client to execute

        public string Data  get; set;  // The payload for the destination client
    

    // An object for storing unhandled messages in a queue to be processed asynchronously
    // This allows us to process messages and respond to the appropriate client,
    // without having to do everything in the ChannelRead0 method and block the main thread
    public class UnhandledMessage
    
        private readonly Message message;
        private readonly IChannelHandlerContext context;

        public UnhandledMessage(Message message, IChannelHandlerContext context)
        
            this.message = message;
            this.context = context;
        

        public Message Message => message;
        public IChannelHandlerContext Context => context;

        public Command Command => message.Command;
        public string ClientId => message.ClientId;
        public string Data => message.Data;
    

    // A representation of the connected Clients on the server.  
    // Note:  This is not the 'Client' class that would be used to communicate with the server.
    public class Client
    
        private readonly string clientId;
        private readonly IChannelHandlerContext context;

        public Client(string clientId, IChannelHandlerContext context)
        
            this.clientId = clientId;
            this.context = context;
        

        public string ClientId => clientId;
        public IChannelHandlerContext Context => context;
    

    // The socket server, using DotNetty's SimpleChannelInboundHandler
    // The ChannelRead0 method is called for each Message received
    public class Server : SimpleChannelInboundHandler<Message>, IDisposable
    
        private readonly ConcurrentDictionary<string, Client> clients;
        private readonly ConcurrentQueue<UnhandledMessage> unhandledMessages;
        private readonly CancellationTokenSource cancellation;
        private readonly AutoResetEvent newMessage;

        public Server(CancellationToken cancellation)
        
            this.clients = new ConcurrentDictionary<string, Client>();
            this.newMessage = new AutoResetEvent(false);
            this.cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
        

        // The start method should be called when the server is bound to a port.
        // Messages will be received, but will not be processed unless/until the Start method is called
        public Task Start()
        
            // Start a dedicated thread to process messages so that the ChannelRead operation does not block
            return Task.Run(() =>
            
                var serializer = JsonSerializer.CreateDefault();  // This will be used to deserialize the Data member of the messages

                while (!cancellation.IsCancellationRequested)
                
                    UnhandledMessage message;
                    var messageEnqueued = newMessage.WaitOne(100);  // Sleep until a new message arrives

                    while (unhandledMessages.TryDequeue(out message))  // Process each message in the queue, then sleep until new messages arrive
                    
                        if (message != null)
                        
                            // Note: This part could be sent to the thread pool if you want to process messages in parallel
                            switch (message.Command)
                            
                                case Command.Register:
                                    // Register a new client, or update an existing client with a new Context
                                    var client = new Client(message.ClientId, message.Context);
                                    clients.AddOrUpdate(message.ClientId, client, (_,__) => client);
                                    break;
                                case Command.SendToClient:
                                    Client destinationClient;
                                    using (var reader = new JsonTextReader(new StringReader(message.Data)))
                                    
                                        var sendToClientCommand = serializer.Deserialize<SendToClientCommand>(reader);
                                        if (clients.TryGetValue(sendToClientCommand.DestinationClientId, out destinationClient))
                                        
                                            var clientMessage = new Message  ClientId = message.ClientId, Command = sendToClientCommand.ClientCommand, Data = sendToClientCommand.Data ;
                                            destinationClient.Context.Channel.WriteAndFlushAsync(clientMessage);
                                        
                                    
                                    break;
                            
                        
                    
                
            , cancellation.Token);
        

        // Receive each message from the clients and enqueue them to be procesed by the dedicated thread
        protected override void ChannelRead0(IChannelHandlerContext context, Message message)
        
            unhandledMessages.Enqueue(new UnhandledMessage(message, context));
            newMessage.Set(); // Trigger an event so that the thread processing messages wakes up when a new message arrives
        

        // Flush the channel once the Read operation has completed
        public override void ChannelReadComplete(IChannelHandlerContext context)
        
            context.Flush();
            base.ChannelReadComplete(context);
        

        // Automatically stop the message-processing thread when this object is disposed
        public void Dispose()
        
            cancellation.Cancel();
        
    

【讨论】:

TcpListener 有什么问题?至少它支持async/await。而不是为本质上是 I/O 绑定的东西启动 CPU-bound 线程 DotNetty 是一个异步的、事件驱动的 Socket IO 库。 SecureChat example server 是一个好的开始,但它只是将传入的消息广播到所有客户端,所以我想我会演示一个将消息路由到特定客户端的示例。我介绍了生产者-消费者模式,有一个专门的线程来处理消息,以防服务器逻辑不仅仅是消息路由。 Here's a thread 讨论 DotNetty 与 TcpListener 和其他替代方案。 DotNetty 是 Netty 的一个端口,由 Microsoft Azure 团队维护。它用于 Azure IoT 网关协议,并作为Akka.net 等分布式框架的网络层。 感谢您的链接。我看了一下您提到的 SecureChat 示例,它看起来更像是一个高级 EAI 库(通常与传输无关)而不是 TCP 套接字库。这没有错,但这不是苹果与苹果的比较。无论如何,您的示例可能有效,但它类似于产生线程和在句柄上休眠的老式重叠 I/O 技术。这不是使用Tasks 的推荐方法,而且还浪费了一个线程,否则在 IOCP 在后台进行时会更好地做其他事情。祝你好运+1 我从 github.com/Azure/DotNetty 下载了 DotNetty,但是在打开解决方案后,它给出了命名空间问题的错误,我认为这个 DotNetty 解决方案将在 Visual Studio 2017 中打开,嗯,我正在下载 Visual Studio 2017 社区版并运行该项目,并将尝试根据我的需要获得结果。【参考方案2】:

我也尝试过这样做,同样基于 MSDN 的相同代码 一个可能的解决方案是使用套接字列表:

List<Socket> clients = new List<Socket>();

然后,当客户端连接时,将客户端添加到列表中:

public void AcceptCallback(IAsyncResult ar)
    
        // Signal the main thread to continue.  
        allDone.Set();

        // Get the socket that handles the client request.  
        Socket listener = (Socket)ar.AsyncState;
        Socket handler = listener.EndAccept(ar);

        clients.Add(handler);
        ...
    

你必须知道每个连接的客户端的id(句柄),然后你可以向特定的客户端发送一些消息:

 public void SendToOne(string id,string message)
    
        foreach (Socket s in clients)
        
            if (s.Handle.ToString() == id)
            
                Send(s, message);
            
        

    

【讨论】:

以上是关于C#中的异步套接字服务器,客户端通过套接字服务器进行客户端通信的主要内容,如果未能解决你的问题,请参考以下文章

C#中的客户端套接字

在 C# 中获取套接字对象的流

绑定时 C# 异步 SocketException

异步服务器套接字缺少第一个缓冲流

如何在 C# 应用程序中取消异步发送套接字

如何使用套接字编程定义客户端服务器并将数据库传递给 C# 中的客户端