创建一个每秒能够处理数千个请求的 TCP 套接字服务器

Posted

技术标签:

【中文标题】创建一个每秒能够处理数千个请求的 TCP 套接字服务器【英文标题】:create a TCP socket server which is able to handle thousands of requests per second 【发布时间】:2017-06-14 06:04:32 【问题描述】:

在第一次尝试中,我创建了一个基本的 TCP 服务器作为流:

public class Tcp

    private  TcpListener listener  get; set; 
    private  bool accept  get; set;  = false;

    public  void StartServer(string ip, int port)
    
        IPAddress address = IPAddress.Parse(ip);
        listener = new TcpListener(address, port);

        listener.Start();
        accept = true;
        StartListener();
        Console.WriteLine($"Server started. Listening to TCP clients at ip:port");
    
    public async void StartListener() //non blocking listener
    

        listener.Start();
        while (true)
        
            try
            
                TcpClient client = await listener.AcceptTcpClientAsync().ConfigureAwait(false);
                HandleClient(client);
            
            finally  
        
    
    private void HandleClient(TcpClient client)
    
        try
        

            NetworkStream networkStream = client.GetStream();
            byte[] bytesFrom = new byte[20];
            networkStream.Read(bytesFrom, 0, 20);
            string dataFromClient = System.Text.Encoding.ASCII.GetString(bytesFrom);
            string serverResponse = "Received!";
            Byte[] sendBytes = Encoding.ASCII.GetBytes(serverResponse);
            networkStream.Write(sendBytes, 0, sendBytes.Length);
            networkStream.Flush();
        
        catch(Exception ex)
        
        
    

我写了一个客户端测试代码,每秒发送和记录请求数

public class Program

    private volatile static Dictionary<int, int> connections = new Dictionary<int, int>();
    private volatile static int fail = 0;
    private static string message = "";
    public static void Main(string[] args)
    
        ServicePointManager.DefaultConnectionLimit = 1000000;
        ServicePointManager.Expect100Continue = false;
        for (int i = 0; i < 512; i++)
        
            message += "T";
        

        int taskCount = 10;
        int requestsCount = 1000;
        var taskList = new List<Task>();
        int seconds = 0;
        Console.WriteLine($"start : DateTime.Now.ToString("mm:ss") ");

        for (int i = 0; i < taskCount; i++)
        

            taskList.Add(Task.Factory.StartNew(() =>
            
                for (int j = 0; j < requestsCount; j++)
                
                    Send();
                
            ));
        
        Console.WriteLine($"threads stablished : DateTime.Now.ToString("mm: ss")");
        while (taskList.Any(t => !t.IsCompleted))  Thread.Sleep(5000); 
        Console.WriteLine($"Compelete : DateTime.Now.ToString("mm: ss")");
        int total = 0;
        foreach (KeyValuePair<int, int> keyValuePair in connections)
        
            Console.WriteLine($"keyValuePair.Key:keyValuePair.Value");
            total += keyValuePair.Value;
            seconds++;
        
        Console.WriteLine($"succeded:total\tfail:fail\tseconds:seconds");
        Console.WriteLine($"End : DateTime.Now.ToString("mm: ss")");
        Console.ReadKey();
    

    private static void Send()
    
        try
        
            TcpClient tcpclnt = new TcpClient();
            tcpclnt.ConnectAsync("192.168.1.21", 5678).Wait();
            String str = message;
            Stream stm = tcpclnt.GetStream();

            ASCIIEncoding asen = new ASCIIEncoding();
            byte[] ba = asen.GetBytes(str);

            stm.Write(ba, 0, ba.Length);

            byte[] bb = new byte[100];
            int k = stm.Read(bb, 0, 100);
            tcpclnt.Close();
            lock (connections)
            
                int key = int.Parse(DateTime.Now.ToString("hhmmss"));
                if (!connections.ContainsKey(key))
                
                    connections.Add(key, 0);
                
                connections[key] = connections[key] + 1;
            
        
        catch (Exception e)
        
            lock (connections)
            
                fail += 1;
            
        
    

当我在本地机器上测试它时,我得到每秒 4000 个请求的最大数量,当我将它上传到本地 LAN 时,它减少到每秒 200 个请求。

问题是: 如何提高服务器性能? 负载测试套接字服务器的正确方法是什么?

【问题讨论】:

旁注 - 人们犯的一个常见错误,就像这里一样,是忽略来自Read的返回值。它告诉您缓冲区中已放置了多少字节,该值可能低至1。 TCP 不提供消息传递,它提供无休止的字节流。如果您想要消息传递(一端的 1 个发送与另一端的 1 个接收匹配),则由 来实现(或转移到已经提供消息传递的更高级别的协议)。跨度> 为了您的理智,我强烈建议您使用 SignalR 而不是原始 TCP/IP。 【参考方案1】:

您可能有一个“非阻塞侦听器”,但是当任何特定的客户端连接时,它只专注于该客户端,直到该客户端发送了一条消息并将响应发送回给它。这不会很好地扩展。

我通常不是async void 的粉丝,但它与您当前的代码保持一致:

public async void StartListener() //non blocking listener


    listener.Start();
    while (true)
    
        TcpClient client = await listener.AcceptTcpClientAsync().ConfigureAwait(false);
        HandleClient(client);
    

private async void HandleClient(TcpClient client)

    NetworkStream networkStream = client.GetStream();
    byte[] bytesFrom = new byte[20];
    int totalRead = 0;
    while(totalRead<20)
    
        totalRead += await networkStream.ReadAsync(bytesFrom, totalRead, 20-totalRead).ConfigureAwait(false);
    
    string dataFromClient = System.Text.Encoding.ASCII.GetString(bytesFrom);
    string serverResponse = "Received!";
    Byte[] sendBytes = Encoding.ASCII.GetBytes(serverResponse);
    await networkStream.WriteAsync(sendBytes, 0, sendBytes.Length).ConfigureAwait(false);
    networkStream.Flush(); /* Not sure necessary */

我还修复了我在 cmets 中提到的关于忽略来自 Read 的返回值的错误,并删除了“对我隐藏错误,使错误无法在野外发现”错误处理。

如果您不能保证您的客户端将始终向此代码发送一条 20 字节的消息,那么您需要执行其他操作,以便服务器知道要读取多少数据。这通常通过在消息前面加上它的长度或使用某种形式的标记值来指示结束来完成。请注意,即使使用长度前缀,您也不能保证一次性读取整个 length,因此您还需要如上所述使用读取循环来首先发现长度。


如果将所有内容切换到 async 并不能满足您的需求,那么您需要放弃使用 NetworkStream 并开始在 Socket 级别工作,特别是使用旨在与 @ 一起使用的异步方法987654321@:

SocketAsyncEventArgs 类是对 System.Net.Sockets.Socket 类的一组增强功能的一部分,它提供了一种可供专门的高性能套接字应用程序使用的替代异步模式...应用程序可以使用增强的异步模式仅在目标热点区域(例如,在接收大量数据时)。

这些增强的主要特点是避免在大容量异步套接字 I/O 期间重复分配和同步对象...

在新的 System.Net.Sockets.Socket 类增强中,异步套接字操作由应用程序分配和维护的可重用 SocketAsyncEventArgs 对象描述...

【讨论】:

谢谢,您对代码的看法是正确的(我有固定大小的消息),但我正在寻找可扩展性提示。你认为重写服务器代码并使用线程而不是非阻塞监听器是个好主意吗? @Jozaghi - 不,每个客户端的线程也不能很好地扩展。如上所示,尽量保持一切异步,并让 OS/CLR 将线程池或 I/O 完成线程分配给实际准备好执行的对象。 我用 SocketAsyncEventArgs 重写了服务器(使用 How To Use the SocketAsyncEventArgs Class 的示例代码),它极大地增加了本地机器上每秒 7000 个请求和本地 LAN 上 1200 个请求的最大值

以上是关于创建一个每秒能够处理数千个请求的 TCP 套接字服务器的主要内容,如果未能解决你的问题,请参考以下文章

一次可以处理多少个请求

HTTP TCP 和 WEB 套接字

基于网络利用率或每秒请求数的 Kubernetes 扩展

lwip stm32 - http请求失败

TPS QPS

Http