📜 ⬆️ ⬇️

Multithreading server in C # in 15 minutes

C # is a fairly simple and flexible language. With .NET comes quite a few ready-made classes, which makes it even easier. So much so that it is quite possible to write a simple multi-threaded HTTP server to render static content in just 15 minutes. It would be possible to use the ready-made HttpListener class and manage it even faster, but the goal of this article is to show how you can do something similar in C #.

First, create a new console project:
Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  1. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  2. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  3. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  4. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  5. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  6. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  7. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  8. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  9. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  10. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  11. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }
  12. Copy Source | Copy HTML using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main( string [] args) { } } }

In .NET, you can very easily create a TCP server using the TcpListener class, which we will use:
Copy Source | Copy HTML
  1. class server
  2. {
  3. TcpListener Listener; // Object accepting TCP clients
  4. // Start the server
  5. public Server ( int Port)
  6. {
  7. // Create a "listener" for the specified port
  8. Listener = new TcpListener (IPAddress.Any, Port);
  9. Listener.Start (); // Run it
  10. // In an infinite loop
  11. while ( true )
  12. {
  13. // Accept new customers
  14. Listener.AcceptTcpClient ();
  15. }
  16. }
  17. // Stop Server
  18. ~ Server ()
  19. {
  20. // If the "listener" was created
  21. if (Listener! = null )
  22. {
  23. // stop it
  24. Listener.Stop ();
  25. }
  26. }
  27. static void Main ( string [] args)
  28. {
  29. // Create a new server on port 80
  30. new Server ( 80 );
  31. }
  32. }

If you start the application now, you can already connect to port 80 and ... everything. The connection will only be idle for nothing, since its handler is missing and it does not close on the server side.
Let's write the easiest handler:
Copy Source | Copy HTML
  1. // class handler client
  2. class Client
  3. {
  4. // Constructor class. He needs to transfer the received client from TcpListener
  5. public Client (TcpClient Client )
  6. {
  7. // Simple HTML Page Code
  8. string Html = "<html> <body> <h1> It works! </ h1> </ body> </ html>" ;
  9. // Required headers: server response, content type and length. After two blank lines - the content itself
  10. string Str = "HTTP / 1.1 200 OK \ nContent-type: text / html \ nContent-Length:" + Html.Length.ToString () + "\ n \ n" + Html;
  11. // Let's bring the string to the form of the byte array
  12. byte [] Buffer = Encoding.ASCII.GetBytes (Str);
  13. // Send it to the client
  14. Client .GetStream (). Write ( Buffer , 0 , Buffer .Length);
  15. // Close the connection
  16. Client .Close ();
  17. }
  18. }

To transfer a client to it, you need to change one line in the Server class:
Copy Source | Copy HTML
  1. // Accept new clients and pass them on to the new instance of the Client class.
  2. new Client (Listener.AcceptTcpClient ());

Now you can run the program, open 127.0.0.1 in the browser and see in capital letters “It works!”
Before we start writing the HTTP request parser, let's make our server multithreaded. There are two ways to do this: manually create a new thread for each client or use a thread pool . Both methods have their advantages and disadvantages. If you create a stream for each client, then the server can not withstand high loads, but you can work with an almost unlimited number of clients at the same time. If you use a thread pool, the number of threads that are simultaneously running will be limited, but you cannot create a new thread until the old ones are completed. Which method is more suitable for you, I do not know, so I’ll give an example of both.
Let's write a simple flow procedure that will only create a new instance of the Client class:
Copy Source | Copy HTML
  1. static void ClientThread ( Object StateInfo)
  2. {
  3. new Client ((TcpClient) StateInfo);
  4. }

To use the first method, you need to replace only the contents of our endless customer reception cycle:
Copy Source | Copy HTML
  1. // Accept new customer
  2. TcpClient Client = Listener.AcceptTcpClient ();
  3. // Create a stream
  4. Thread Thread = new Thread ( new ParameterizedThreadStart (ClientThread));
  5. // And launch this stream, passing it the received client
  6. Thread .Start (Client);

For the second method you need to do the same:
Copy Source | Copy HTML
  1. // Accept new customers. After the client has been accepted, it is transferred to the new thread (ClientThread)
  2. // using the thread pool.
  3. ThreadPool.QueueUserWorkItem ( new WaitCallback (ClientThread), Listener.AcceptTcpClient ());

Plus it is necessary to set the maximum and minimum number of simultaneously running threads. We do this in the Main procedure:
Copy Source | Copy HTML
  1. // Determine the desired maximum number of threads
  2. // Let it be 4 per processor
  3. int MaxThreadsCount = Environment .ProcessorCount * 4 ;
  4. // Set the maximum number of worker threads
  5. ThreadPool.SetMaxThreads (MaxThreadsCount, MaxThreadsCount);
  6. // Set the minimum number of worker threads
  7. ThreadPool.SetMinThreads ( 2 , 2 );

The maximum number of threads must be at least two, since this number includes the main thread. If you set the unit, then client processing will be possible only when the main thread has suspended work (for example, it is waiting for a new client or the Sleep procedure has been called).
So, now let's switch entirely to the Client class and start processing the HTTP request. Get the request text from the client:
Copy Source | Copy HTML
  1. // Declare a line in which the client request will be stored
  2. string Request = "" ;
  3. // Buffer for storing data received from the client
  4. byte [] Buffer = new byte [ 1024 ];
  5. // Variable to store the number of bytes received from the client
  6. int Count;
  7. // Read from the client's stream until data from it is received
  8. while ((Count = Client.GetStream (). Read ( Buffer , 0 , Buffer .Length))> 0 )
  9. {
  10. // Convert this data to a string and add it to the Request variable
  11. Request + = Encoding.ASCII.GetString ( Buffer , 0 , Count);
  12. // The request must be terminated by the sequence \ r \ n \ r \ n
  13. // Either stop receiving the data themselves if the length of the Request string exceeds 4 kilobytes
  14. // We do not need to receive data from a POST request (and so on), but a regular request
  15. // in theory should not be more than 4 kilobytes
  16. if (Request.IndexOf ( "\ r \ n \ r \ n" )> = 0 || Request.Length> 4096 )
  17. {
  18. break ;
  19. }
  20. }

Next, we carry out the parsing of the data:
Copy Source | Copy HTML
  1. // Parsing the query string using regular expressions
  2. // At the same time cut off all the variables of the GET request
  3. Match ReqMatch = Regex . Match (Request, @ "^ \ w + \ s + ([^ \ s \?] +) [^ \ S] * \ s + HTTP /.* |" );
  4. // If the request failed
  5. if (ReqMatch == Match .Empty)
  6. {
  7. // Pass error 400 to the client - invalid request
  8. SendError (Client, 400 );
  9. return ;
  10. }
  11. // Get the query string
  12. string RequestUri = ReqMatch.Groups [ 1 ] .Value;
  13. // Bring it to its original form, converting escaped characters
  14. // For example, "% 20" -> ""
  15. RequestUri = Uri .UnescapeDataString (RequestUri);
  16. // If the line contains a colon, pass error 400
  17. // This is needed to protect against URLs like http://example.com/../../file.txt
  18. if (RequestUri.IndexOf ( ".." )> = 0 )
  19. {
  20. SendError (Client, 400 );
  21. return ;
  22. }
  23. // If the query string ends in "/", then add to it index.html
  24. if (RequestUri.EndsWith ( "/" ))
  25. {
  26. RequestUri + = "index.html" ;
  27. }

Finally, let's work with the files: check if there is a necessary file, determine its content type and transfer it to the client.
Copy Source | Copy HTML
  1. string FilePath = "www /" + RequestUri;
  2. // If this file does not exist in the www folder, send a 404 error
  3. if (! File .Exists (FilePath))
  4. {
  5. SendError (Client, 404 );
  6. return ;
  7. }
  8. // Get the file extension from the query string
  9. string Extension = RequestUri.Substring (RequestUri.LastIndexOf ( '.' ));
  10. // Content type
  11. string ContentType = "" ;
  12. // Trying to determine the content type by file extension
  13. switch (Extension)
  14. {
  15. case ".htm" :
  16. case ".html" :
  17. ContentType = "text / html" ;
  18. break ;
  19. case ".css" :
  20. ContentType = "text / stylesheet" ;
  21. break ;
  22. case ".js" :
  23. ContentType = "text / javascript" ;
  24. break ;
  25. case ".jpg" :
  26. ContentType = "image / jpeg" ;
  27. break ;
  28. case ".jpeg" :
  29. case ".png" :
  30. case ".gif" :
  31. ContentType = "image /" + Extension.Substring ( 1 );
  32. break ;
  33. default :
  34. if (Extension.Length> 1 )
  35. {
  36. ContentType = "application /" + Extension.Substring ( 1 );
  37. }
  38. else
  39. {
  40. ContentType = "application / unknown" ;
  41. }
  42. break ;
  43. }
  44. // Open the file, insuring against errors
  45. FileStream FS;
  46. try
  47. {
  48. FS = new FileStream (FilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
  49. }
  50. catch ( Exception )
  51. {
  52. // If an error occurs, send error 500 to the client
  53. SendError (Client, 500 );
  54. return ;
  55. }
  56. // Send Headers
  57. string Headers = "HTTP / 1.1 200 OK \ nContent-Type:" + ContentType + "\ nContent-Length:" + FS.Length + "\ n \ n" ;
  58. byte [] HeadersBuffer = Encoding.ASCII.GetBytes (Headers);
  59. Client.GetStream (). Write (HeadersBuffer, 0 , HeadersBuffer.Length);
  60. // Until the end of the file is reached
  61. while (FS.Position <FS.Length)
  62. {
  63. // Read data from file
  64. Count = FS.Read ( Buffer , 0 , Buffer .Length);
  65. // And pass them on to the client.
  66. Client.GetStream (). Write ( Buffer , 0 , Count);
  67. }
  68. // Close The File And Connection
  69. FS.Close ();
  70. Client.Close ();

Also in the code, the not yet described procedure SendError was mentioned. We will write it too:
Copy Source | Copy HTML
  1. // Send the page with an error
  2. private void SendError ( TcpClient Client, int Code)
  3. {
  4. // Get a string like "200 OK"
  5. // HttpStatusCode stores all HTTP / 1.1 status codes
  6. string CodeStr = Code.ToString () + "" + ((HttpStatusCode) Code) .ToString ();
  7. // Simple HTML Page Code
  8. string Html = "<html> <body> <h1>" + CodeStr + "</ h1> </ body> </ html>" ;
  9. // Required headers: server response, content type and length. After two blank lines - the content itself
  10. string Str = "HTTP / 1.1" + CodeStr + "\ nContent-type: text / html \ nContent-Length:" + Html.Length.ToString () + "\ n \ n" + Html;
  11. // Let's bring the string to the form of the byte array
  12. byte [] Buffer = Encoding.ASCII.GetBytes (Str);
  13. // Send it to the client
  14. Client.GetStream (). Write ( Buffer , 0 , Buffer .Length);
  15. // Close the connection
  16. Client.Close ();
  17. }

This is the end of writing a simple HTTP server. It works in several threads, gives statics, has a simple protection against bad requests and swears at the missing files. Additional gadgets can be added to all of this: configurable, domain processing, changing addresses like mod_rewrite, even CGI support. But it will be a completely different story :-)

Source (via ThreadPool)
Source (via Thread)
The archive with the source (via ThreadPool, the option via Thread is commented out)
Archive with compiled version (via ThreadPool)

')

Source: https://habr.com/ru/post/120157/


All Articles