C# 中的简单 Web 服务器






4.91/5 (12投票s)
引言
大家好,在这篇文章中,我们将讨论如何在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()
- 提供端口号和内容路径
- 从浏览器发送请求