简体   繁体   中英

create a TCP socket server which is able to handle thousands of requests per second

At the fist try, I created a basic TCP server as flowing:

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

I wrote a client test code that is sending and recording number of requests per second

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

when I test it on a local machine, I get the maximum number of 4000 requests per second and when I upload it to local Lan it decreases to 200 requests per second.

The question is: how can I improve the server performance? what is the correct way of load testing socket servers?

You may have a "non-blocking listener", but when any particular client connects, it devotes itself to just that client until that client has sent a message and a response has been sent back to it. That's not going to scale well.

I'm usually not a fan of async void , but it's in keeping with your current code:

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

I've also fixed the bug I mentioned in the comments about ignoring the return value from Read and removed the "hide errors from me making bugs impossible to spot in the wild" error handling.

If you're not guaranteed that your clients will always send a 20 byte message to this code, then you need to do something else so that the server knows how much data to read. This is usually done by either prefixing the message with its length or using some form of sentinel value to indicate the end. Note that even with length-prefixing, you're not guaranteed to read the whole length in one go and so you'd need to also use a read loop, as above, to discover the length first.


If switching everything to async isn't giving you the scale you need, then you need to abandon using NetworkStream and start working at the Socket level, and specifically with the async methods designed to work with SocketAsyncEventArgs :

The SocketAsyncEventArgs class is part of a set of enhancements to the System.Net.Sockets.Socket class that provide an alternative asynchronous pattern that can be used by specialized high-performance socket applications... An application can use the enhanced asynchronous pattern exclusively or only in targeted hot areas (for example, when receiving large amounts of data).

The main feature of these enhancements is the avoidance of the repeated allocation and synchronization of objects during high-volume asynchronous socket I/O...

In the new System.Net.Sockets.Socket class enhancements, asynchronous socket operations are described by reusable SocketAsyncEventArgs objects allocated and maintained by the application...

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