Φτίαχνουμε Multithreaded Web Server σε C#
Η C# είναι μια αρκετά απλή και ευέλικτη γλώσσα προγραμματισμού. Μαζί με το .NET έρχονται πολλά έτοιμα classes που την κάνει ακόμα πιο απλή. Τόσο, που μπορείτε να γράψετε έναν απλό multithreaded HTTP server για στατικό περιεχόμενο μέσα σε 15 λεπτά. Θα μπορούσαμε να χρησιμοποιήσουμε την class HttpListener και να τελειώσουμε ακόμα πιο γρήγορα, όμως ο στόχος αυτού του άρθρου είναι να δείτε πως γίνετε κάτι τέτοιο σε C#.
Για αρχή φτιάχνουμε νέο project:
using System; using System.Collections.Generic; using System.Text; namespace HTTPServer { class Server { static void Main(string[] args) { } } }
Στην .NET μπορούμε πολύ εύκολα να φτιάξουμε TCP-server με την βοήθεια του TcpListener class τον οποίο και θα χρησιμοποιήσουμε.
class Server { TcpListener Listener; // Object πάνω στο οποίο συνδέονται οι TCP-clients // Εκκινηση του server public Server(int Port) { // Φτιάχνουμε listener για το καθορισμένο port Listener = new TcpListener(IPAddress.Any, Port); Listener.Start(); // Τον τρέχουμε // Σε άπειρο loop while (true) { // Δεχόμαστε νέους clients Listener.AcceptTcpClient(); } } // Διακοπή του server ~Server() { // Εαν ο listener έχει δημιουργηθεί if (Listener != null) { // Τον σταματάμε Listener.Stop(); } } static void Main(string[] args) { // Φτιάχνουμε νέο server στην port 8080 new Server(8080); } }
Εαν τρέξουμε τώρα το πρόγραμμα τότε θα μπορούμε να συνδεθούμε στην port 8080 και αυτό ήταν.
Θα δημιουργηθεί μια μόνιμη σύνδεση που δεν θα κάνει τίποτα, μιας και δεν υπάρχει handler και δεν κλείνει από την μεριά του server.
Ας φτιάξουμε έναν απλό Ηandler:
// Client-Handler Class class Client { // Class constructor. Θα του στέλνουμε τους clients του TcpListener public Client(TcpClient Client) { //Μια απλή HTML σελίδα string Html = "<html><body><h1>It works!</h1></body></html>"; // Απαραίτητα Headers: server response, τύπος και μήκος περιεχομένου. Μετά από δύο κενές γραμμές, το περιεχόμενο string Str = "HTTP/1.1 200 OK\nContent-type: text/html\nContent-Length:" + Html.Length.ToString() + "\n\n" + Html; // Θα περάσουμε την γραμμή σε array byte[] Buffer = Encoding.ASCII.GetBytes(Str); // Θα το στείλουμε στον client. Client.GetStream().Write(Buffer, 0, Buffer.Length); // Κλείνουμε την σύνδεση Client.Close(); } }
Για να του στείλουμε έναν client θα πρέπει να αλλάξουμε μια γραμμή στην Server class.
// Λαμβάνουμε νέους clients και τους στέλνουμε για επεξεργασία new Client(Listener.AcceptTcpClient());
Τώρα αν τρέξουμε το πρόγραμμα και ανοίξουμε τον Browser στο 127.0.0.1:8080 θα δούμε ένα μεγάλο “It Works!”.
Πριν γράψουμε τον parser για τα HTTP-Requests θα κάνουμε τον server μας multithreaded. Για αυτο υπαρχουν 2 τροποι:
να φτιάχνουμε χειροκίνητα νέο thread για κάθε client ή να χρησιμοποιήσουμε thread pool.
Και οι 2 τρόποι έχουν τα πλεονεκτήματα και τα μειονεκτήματά τους. Εαν φτιάχνουμε νέο thread για κάθε νέο client, τότε ο server μπορεί να μην αντέξει μεγάλο φορτίο, αλλά θα μπορούμε να δουλεύουμε σχεδόν με απεριόριστο αριθμό clients ταυτόχρονα.
Εαν χρησιμοποιήσουμε το thread pool, τότε ο αριθμός των ταυτόχρονων thread θα είναι περιορισμένος, αλλά δεν θα μπορούμε να φτιάξουμε νέα thread μέχρι να τερματίσουν τα παλιά.
Δεν ξέρω τι θα προτιμήσετε, γιαυτό θα δείξω πως δουλεύουν και τα δυο.
Ο παρακάτω κώδικας θα δημιουργεί νεο instance του client class.
static void ClientThread(Object StateInfo) { new Client((TcpClient)StateInfo); }
Για να χρησιμοποιήσουμε τον πρώτο τρόπο αρκεί να αλλάξουμε το περιεχόμενο του infinite loop που λαμβάνει τους πελάτες.
// λαμβάνουμε νέο client TcpClient Client = Listener.AcceptTcpClient(); // Φτιάχνουμε νέο thread Thread Thread = new Thread(new ParameterizedThreadStart(ClientThread)); // Και τρέχουμε αυτό το thread, στέλνοντάς του τον client που λάβαμε Thread.Start(Client);
Για τον δεύτερο τρόπο πρέπει να κάνουμε το ίδιο
// Λαμβάνουμε νέους clients. Αφού ο πελάτης γίνει δεκτός, στέλνετε στο νέο thread (ClientThread) // χρησιμοποιώντας το thread pool ThreadPool.QueueUserWorkItem(new WaitCallback(ClientThread), Listener.AcceptTcpClient());
Συν πρέπει να καθορίσουμε τον ελάχιστο και μέγιστο αριθμό threads. Θα το κάνουμε στην Main function
// Καθορίζουμε τον μέγιστο αριθμό threads // Ας είναι 4 για κάθε cpu. int MaxThreadsCount = Environment.ProcessorCount * 4; // Ορίζουμε τον μέγιστο αριθμό εργαζομένων threads. ThreadPool.SetMaxThreads(MaxThreadsCount, MaxThreadsCount); // Ορίζουμε τον ελάχιστο αριθμό εργαζομένων threads. ThreadPool.SetMinThreads(2, 2);
Ο μέγιστος αριθμός threads δεν πρέπει να είναι λιγότερος από 2, γιατί σε αυτόν τον αριθμό μπαίνει και το κύριο thread. Εαν το βάλουμε 1, τότε η επεξεργασία των clients θα είναι δυνατή μόνο όταν το κύριο thread σταμάτησε την εργασία του (πχ περιμένει νέο client).
Τώρα ας δούμε πάλι το Client class και να αρχίσουμε να επεξεργαζόμαστε τα HTTP requests.
Λαμβάνουμε το κείμενο με το request από τον client:
// String στο οποίο θα αποθηκευτεί το request του client. string Request = ""; // Buffer για την αποθήκευση τον δεδομένων που λάβαμε από τον client. byte[] Buffer = new byte[1024]; // Variable για την αποθήκευση του αριθμού bytes που λάβαμε από τον client. int Count; // Διαβάζουμε το stream του client όσο λαμβάνουμε από αυτόν δεδομένα. while ((Count = Client.GetStream().Read(Buffer, 0, Buffer.Length)) > 0) { // Μετατρέπουμε αυτά τα δεδομένα σε string και το προσθέτουμε στην Request variable. Request += Encoding.ASCII.GetString(Buffer, 0, Count); // Η αίτηση πρέπει να επεξεργάζεται με αυτήν την ακολουθία \r\n\r\n // Αλλιώς σταματάμε την λήψη δεδομένων εάν το μήκος του Request string είναι μεγαλύτερο από 4KB // Αυτό επειδή δεν χρειάζεται να λαμβάνουμε δεδομένα από POST Requests. Ενα απλο request // δεν είναι μεγαλύτερο από 4KB if (Request.IndexOf("\r\n\r\n") >= 0 || Request.Length > 4096) { break; } }
Τώρα κάνουμε parsing των δεδομένων που λάβαμε:
// String parsing χρησιμοποιώντας regular expressions. // Ταυτόχρονα φιλτράρουμε όλα τα GET Requests. Match ReqMatch = Regex.Match(Request, @"^\w+\s+([^\s\?]+)[^\s]*\s+HTTP/.*|"); // Εαν το request απέτυχε. if (ReqMatch == Match.Empty) { // στέλνουμε στον client σφάλμα 400 SendError(Client, 400); return; } // Παίρνουμε το request string string RequestUri = ReqMatch.Groups[1].Value; // Την φέρνουμε στην αρχική της μορφή μετατρέποντας του χαρακτήρες. // πχ, "%20" -> " " RequestUri = Uri.UnescapeDataString(RequestUri); // Εαν το string έχει μέσα διπλές τελείας στέλνουμε σφάλμα 400. // Αυτό το κάνουμε για να προστατευτούμε από URL όπως http://example.com/../../file.txt if (RequestUri.IndexOf("..") >= 0) { SendError(Client, 400); return; } // Εαν το request string τελειώνει με "/" τότε προσθέτουμε index.html if (RequestUri.EndsWith("/")) { RequestUri += "index.html"; }
Και τέλος δουλεύουμε με αρχεία: ελέγχουμε αν υπάρχει το αρχείο που χρειάζεται, καθορίζουμε τον τύπο του αρχείου και το στέλνουμε στον client.
string FilePath = "www/" + RequestUri; // Εαν στον φάκελο www δεν υπάρχει τέτοιο αρχείο, στέλνουμε σφάλμα 404 if (!File.Exists(FilePath)) { SendError(Client, 404); return; } // Παίρνουμε την επέκταση του αρχείου string Extension = RequestUri.Substring(RequestUri.LastIndexOf('.')); // Τύπος περιεχομένου string ContentType = ""; // Προσπαθούμε να καταλάβουμε τον τύπο περιεχόμενου από την επέκταση του αρχείου. switch (Extension) { case ".htm": case ".html": ContentType = "text/html"; break; case ".css": ContentType = "text/stylesheet"; break; case ".js": ContentType = "text/javascript"; break; case ".jpg": ContentType = "image/jpeg"; break; case ".jpeg": case ".png": case ".gif": ContentType = "image/" + Extension.Substring(1); break; default: if (Extension.Length > 1) { ContentType = "application/" + Extension.Substring(1); } else { ContentType = "application/unknown"; } break; } // Ανοίγουμε το αρχείο, ελέγχοντας για σφάλματα. FileStream FS; try { FS = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read); } catch (Exception) { // Εαν είχαμε σφάλμα στέλνουμε στον client σφάλμα 500 SendError(Client, 500); return; } // Στέλνουμε τα headers string Headers = "HTTP/1.1 200 OK\nContent-Type: " + ContentType + "\nContent-Length: " + FS.Length + "\n\n"; byte[] HeadersBuffer = Encoding.ASCII.GetBytes(Headers); Client.GetStream().Write(HeadersBuffer, 0, HeadersBuffer.Length); // Μέχρι να φτάσουμε στο τέλος του αρχείου while (FS.Position < FS.Length) { // Διαβάζουμε τα δεδομένα του αρχείου Count = FS.Read(Buffer, 0, Buffer.Length); // Και τα μεταδίδουμε στον client Client.GetStream().Write(Buffer, 0, Count); } // Κλείνουμε τ αρχείο και την σύνδεση FS.Close(); Client.Close();
Στον παραπάνω κώδικα χρησιμοποιήσαμε την ανύπαρκτη SendError function. Ας την γράψουμε τώρα
// Αποστολή της σελίδας με το σφάλμα. private void SendError(TcpClient Client, int Code) { // Λαμβάνουμε string τύπου "200 OK" // Το HttpStatusCode περιέχει μέσα του όλα τα status-codes του HTTP/1.1 string CodeStr = Code.ToString() + " " + ((HttpStatusCode)Code).ToString(); // Μια απλή html σελίδα string Html = "<html><body><h1>" + CodeStr + "</h1></body></html>"; // Απαραίτητα Headers: server response, τύπος και μήκος περιεχομένου. Μετά από δύο κενές γραμμές, το περιεχόμενο string Str = "HTTP/1.1 " + CodeStr + "\nContent-type: text/html\nContent-Length:" + Html.Length.ToString() + "\n\n" + Html; // Θα περάσουμε την γραμμή σε byte array byte[] Buffer = Encoding.ASCII.GetBytes(Str); // Το στέλνουμε στον client Client.GetStream().Write(Buffer, 0, Buffer.Length); // Κλείνουμε την σύνδεση Client.Close(); }
Αυτό ήταν, ο HTTP server είναι έτοιμος. Είναι multithreaded, μπορεί να δίνει static περιεχόμενο, έχει μια απλή προστασία από malicious requests και βγάζει σφάλμα αν κάποιο αρχείο δεν υπάρχει. Επίσης πολύ εύκολα μπορούμε να του προσθέσουμε διάφορα άλλα features όπως: ρυθμίσεις, vhosts, mod_rewrite, μέχρι και υποστήριξη CGI.
Source Code – Thread Pool
Source Code – Threads





Γίνετε επεξεργασία, Παρακαλώ περιμένετε...













katebasa to arxeio kai to etreksa omws to ektelesimo arxeio pws dhmiourgeitai?
apo compile?
Epishs ekana copy-paste to programma kai dokimasa na to kanw compile se linux alla mou emfanize errors?
Ναι, κάνεις compile των κώδικα και δημιουργείται το exe αρχείο το οποίο και παραθέτω.
Σε linux δεν πρόκειται να δουλέψει, είναι γραμμένο σε Visual Studio και στοχεύει αποκλειστικά τα Windows συστήματα.
Μήπως μπορείτε να βάλετε ολοκληρωμένο τον κώδικα σε ένα αρχείο γιατι προσπάθησα να το κάνω compile σε Visual Studio C# και μου εμφάνιζε errors…
Στο τέλος του άρθρου δίνω τους ολοκληρωμένους κώδικες και για τις 2 μεθόδους. Εκεί που λέει
Source Code – Thread Pool
Source Code – Threads
Το πρόγραμμα τρέχει μόνο αν είναι εγκατεστημένο το Visual Studiο?
Επειδή το δοκίμασα σε κάποιον υπολογιστή που δεν το είχε και όταν άνοιγα το εκτελεσιμο αρχείο μου εμφάνιζε error…
Το VS δεν χρειάζεται να είναι εγκαταστημένο για να λειτουργήσει. Και το error μάλλον ήταν επειδή κάποιο άλλο service ακούει την ίδια πορτ.