65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的简单 Web 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (12投票s)

2014年5月5日

CPOL

3分钟阅读

viewsIcon

63495

downloadIcon

4749

引言

大家好,在这篇文章中,我们将讨论如何在C#中实现Web服务器。这个项目是用.Net 4.0和Visual Studio 2012完成的。为了更好地理解和轻松实现,我还上传了完整的源代码。所有必要的设置(例如端口号)都在代码中提到。我把整个项目分为六个部分,我们将在实现部分讨论这些部分。最重要的是,这是我的第一篇在线文章,所以如果有什么错误,特别是语法错误,请原谅我。

背景

代码并不复杂,但为了更好地理解这篇文章,读者应该具备套接字和多线程编程的基础知识。如有任何疑问,您可以直接谷歌搜索或给我发邮件,我一定会尽力解决您的问题。

实现

如前所述,我把这个项目分成了六个部分,我们将逐一详细讨论各个部分。

1)第一部分是服务器。

public class WebServer
    {
        // check for already running
        private bool _running = false;
        private int _timeout = 5;
        private Encoding _charEncoder = Encoding.UTF8;
        private Socket _serverSocket;

        // Directory to host our contents
        private string _contentPath;
        
        //create socket and initialization
        private void InitializeSocket(IPAddress ipAddress, int port, string contentPath) //create socket
        {
            _serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            _serverSocket.Bind(new IPEndPoint(ipAddress, port));
            _serverSocket.Listen(10);    //no of request in queue
            _serverSocket.ReceiveTimeout = _timeout;
            _serverSocket.SendTimeout = _timeout;
            _running = true; //socket created
            _contentPath = contentPath;
        }
        public void Start(IPAddress ipAddress, int port, string contentPath)
        {
            try
            {
                InitializeSocket(ipAddress, port, contentPath);
            }
            catch
            {
                Console.WriteLine("Error in creating server socker");
                Console.ReadLine();

            }
            while (_running)
            {
                var requestHandler = new RequestHandler(_serverSocket, contentPath);
                requestHandler.AcceptRequest();
            }
        }
        public void Stop()
        {
            _running = false;
            try
            {
                _serverSocket.Close();
            }
            catch
            {
                Console.WriteLine("Error in closing server or server already closed");
                Console.ReadLine();

            }
            _serverSocket = null;
        }

    }

在这个部分,我创建了一个服务器套接字,它将持续监听Web请求 (while(running))。这个部分包含三个方法。**使用时,您必须使用 Start(IPAddress ipAddress, int port, string contentPath) 和 Stop() 函数**

注意:contentPath 是您要在服务器上托管的路径或目录

  • InitializeSocket:创建服务器套接字,它将持续监听请求 [while(_running)],并具有必要的属性,例如端口号、接收和发送超时等。
  • Start:以监听模式启动套接字
  • Stop:停止套接字监听

2)第二部分是RequestHandler。

class RequestHandler
    {
        private Socket _serverSocket;
        private int _timeout;
        private string _contentPath;
        private Encoding _charEncoder = Encoding.UTF8;

        public RequestHandler(Socket serverSocket, String contentPath)
        {
            _serverSocket = serverSocket;
            _timeout = 5;
            _contentPath = contentPath;
        }

        public void AcceptRequest()
        {
            Socket clientSocket = null;
            try
            {
                // Create new thread to handle the request and continue to listen the socket.
                clientSocket = _serverSocket.Accept();

                var requestHandler = new Thread(() =>
                {
                    clientSocket.ReceiveTimeout = _timeout;
                    clientSocket.SendTimeout = _timeout;
                    HandleTheRequest(clientSocket);
                });
                requestHandler.Start();
            }
            catch
            {
                Console.WriteLine("Error in accepting client request");
                Console.ReadLine();
                if (clientSocket != null)
                    clientSocket.Close();
            }
        }

        private void HandleTheRequest(Socket clientSocket)
        {
            var requestParser = new RequestParser();
            string requestString = DecodeRequest(clientSocket);
            requestParser.Parser(requestString);

            if (requestParser.HttpMethod.Equals("get", StringComparison.InvariantCultureIgnoreCase))
            {
                var createResponse = new CreateResponse(clientSocket, _contentPath);
                createResponse.RequestUrl(requestParser.HttpUrl);
            }
            else
            {
                Console.WriteLine("unemplimented mothode");
                Console.ReadLine();
            }
            StopClientSocket(clientSocket);
        }

        public void StopClientSocket(Socket clientSocket)
        {
            if (clientSocket != null)
                clientSocket.Close();
        }

        private string DecodeRequest(Socket clientSocket)
        {
            var receivedBufferlen = 0;
            var buffer = new byte[10240];
            try
            {
                receivedBufferlen = clientSocket.Receive(buffer);
            }
            catch (Exception)
            {
                //Console.WriteLine("buffer full");
                Console.ReadLine();
            }
            return _charEncoder.GetString(buffer, 0, receivedBufferlen);
        }
    }

这一部分接受请求并创建新线程来处理请求。由于我们只处理GET方法,因此会检查请求的类型。这一部分包含三个重要的方法。

  • AcceptRequest:此函数接受Web请求并创建一个新线程来处理请求。
  • HandleTheRequest:此方法检查Web请求的类型,然后将其传递给create response。
  • DecodeRequest:此方法获取请求数据,对其进行解码,并将其传递给请求解析器。

3)第三部分是RequestParser。

 public class RequestParser
    {
        private Encoding _charEncoder = Encoding.UTF8;
        public string HttpMethod;
        public string HttpUrl;
        public string HttpProtocolVersion;


        public void Parser(string requestString)
        {
            try
            {
                string[] tokens = requestString.Split(' ');

                tokens[1] = tokens[1].Replace("/", "\\");
                HttpMethod = tokens[0].ToUpper();
                HttpUrl = tokens[1];
                HttpProtocolVersion = tokens[2];
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                Console.WriteLine(ex.InnerException.Message);
                Console.WriteLine("Bad Request");
            }
        }
    }

这一部分从请求处理程序获取解码后的请求数据。它只有一个方法,该方法在解析后返回一个字符串数组作为请求。我认为为每个请求创建一个字符串数组并不是一个好方法,因为目前我只使用了数组中的前三个字符串,所以很多空间都被浪费了。

4)第四部分是CreateResponse。

public class CreateResponse
    {
        RegistryKey registryKey = Registry.ClassesRoot;
        public Socket ClientSocket = null;
        private Encoding _charEncoder = Encoding.UTF8;
        private string _contentPath ;
        public FileHandler FileHandler;

        public CreateResponse(Socket clientSocket,string contentPath)
        {
            _contentPath = contentPath;
            ClientSocket = clientSocket;
            FileHandler=new FileHandler(_contentPath);
        }

        public void RequestUrl(string requestedFile)
        {
            int dotIndex = requestedFile.LastIndexOf('.') + 1;
            if (dotIndex > 0)
            {
                if (FileHandler.DoesFileExists(requestedFile))    //If yes check existence of the file
                    SendResponse(ClientSocket, FileHandler.ReadFile(requestedFile), "200 Ok", GetTypeOfFile(registryKey, (_contentPath + requestedFile)));
                    else
                        SendErrorResponce(ClientSocket);      // We don't support this extension.
            }
            else   //find default file as index .htm of index.html
            {
                if (FileHandler.DoesFileExists("\\index.htm"))
                    SendResponse(ClientSocket, FileHandler.ReadFile("\\index.htm"), "200 Ok", "text/html");
                else if (FileHandler.DoesFileExists("\\index.html"))
                    SendResponse(ClientSocket, FileHandler.ReadFile("\\index.html"), "200 Ok", "text/html");
                else
                    SendErrorResponce(ClientSocket);
            }
        }

        private string GetTypeOfFile(RegistryKey registryKey,string fileName)
        {
            RegistryKey fileClass = registryKey.OpenSubKey(Path.GetExtension(fileName));
            return fileClass.GetValue("Content Type").ToString();
        }

        private void SendErrorResponce(Socket clientSocket)
        {
            SendResponse(clientSocket, null, "404 Not Found", "text/html");
        }


        private void SendResponse(Socket clientSocket, byte[] byteContent, string responseCode, string contentType)
        {
            try
            {
                byte[] byteHeader = CreateHeader(responseCode, byteContent.Length, contentType);
                clientSocket.Send(byteHeader);
                clientSocket.Send(byteContent);
                
                clientSocket.Close();
            }
            catch
            {
            }
        }

        private byte[] CreateHeader(string responseCode, int contentLength, string contentType)
        {
            return _charEncoder.GetBytes("HTTP/1.1 " + responseCode + "\r\n"
                                  + "Server: Simple Web Server\r\n"
                                  + "Content-Length: " + contentLength + "\r\n"
                                  + "Connection: close\r\n"
                                  + "Content-Type: " + contentType + "\r\n\r\n");
        }
    }

这一部分检查请求的文件,创建响应并将其发送到客户端套接字。

  • RequestUrl:此方法将响应发送到客户端套接字。
  • GetTypeOfFile:此方法从注册表 ClassesRoot 获取请求文件的类型 [例如:对于 .html,文件类型为 text/html 等]
  • SendResponse:此方法将响应发送到客户端套接字。
  • CreateHeader:此方法创建响应头。

5)第五部分是FileHandler。

public class FileHandler
    {
        private string _contentPath;

        public FileHandler(string contentPath)
        {
            _contentPath = contentPath;
        }

        internal bool DoesFileExists(string directory)
        {
            return File.Exists(_contentPath+directory);
        }

        internal byte[] ReadFile(string path)
        {
            //return File.ReadAllBytes(path);
            if (ServerCache.Contains(_contentPath+path))
            {
                Console.WriteLine("cache hit");
                return ServerCache.Get(_contentPath+path);
            }
            else
            {
                byte[] content = File.ReadAllBytes(_contentPath+path);
                ServerCache.Insert(_contentPath+path, content);
                return content;
            }

        }
    }

这一部分检查请求的文件,如果找到该文件,则将其作为字节数组返回。

  • DoesFileExists:此方法检查请求的文件是否存在。
  • ReadFile:此方法读取文件内容并将其作为字节数组返回。

6)第六部分是ServerCache。

class ServerCache
    {
        public struct Content
        {
            internal byte[] ResponseContent;
            internal int RequestCount;
        };
        private static readonly object SyncRoot = new object();
        private static int _capacity = 15;
        private static Dictionary<string, content=""> _cache = new Dictionary<string,>(StringComparer.OrdinalIgnoreCase) { };

        public static bool Insert(string url, byte[] body)
        {
            lock (SyncRoot)
            {
                if (IsFull())
                    CreateEmptySpace();

                var content = new Content {RequestCount = 0, ResponseContent = new byte[body.Length]};
                Buffer.BlockCopy(body, 0, content.ResponseContent, 0, body.Length);
                if (!_cache.ContainsKey(url))
                {
                    _cache.Add(url, content);
                    return false;
                }

                return true;
            }

        }

        public static bool IsFull()
        {
            return _cache.Count >= _capacity;
        }

        public static byte[] Get(string url)
        {
            if (_cache.ContainsKey(url))
            {
                Content content = _cache[url];
                content.RequestCount++;
                _cache[url] = content;
                return content.ResponseContent;
            }

            return null;
        }

        public static bool Contains(string url)
        {
            return _cache.ContainsKey(url);
        }

        private static void CreateEmptySpace()
        {
            var minRequestCount = Int32.MaxValue;
            var url = String.Empty;
            foreach (var entry in _cache)
            {
                Content content = entry.Value;
                if (content.RequestCount < minRequestCount)
                {
                    minRequestCount = content.RequestCount;
                    url = entry.Key;
                }
            }

            _cache.Remove(url);
        }

        public static int CacheCount()
        {
            return _cache.Count;
        }
    }
</string,>

这一部分是可选的。这是为了减少文件读取时间而提供的附加功能。这里我使用了 Dictionary 来缓存最常请求的文件。Dictionary 的键将是请求文件的 URL,值是一个包含两个属性的对象,一个是用于存储文件内容的字节数组,另一个是用于维护相应文件计数的整数。我没有使用任何标准的缓存算法。我们也可以使用缓存对象来代替这个。

用法

  • 使用时,您必须使用 Start(IPAddress ipAddress, int port, string contentPath) 和 Stop()
  • 提供端口号和内容路径
  • 从浏览器发送请求
© . All rights reserved.