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

构建你自己的 Web 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (89投票s)

2012 年 9 月 3 日

CPOL

4分钟阅读

viewsIcon

414231

downloadIcon

20189

让我们构建一个简单的 Web 服务器并使其可以从互联网访问

目录

引言

我们将学习如何用 C# 编写一个简单的 Web 服务器,它可以响应最常见的 HTTP 方法(GETPOST),然后我们将使这个服务器可以通过互联网访问。这次,我们将真正地“Hello world!”。

Simple Web Server

背景

HTTP 协议

HTTP 是服务器和客户端之间的通信协议。它使用 TCP/IP 协议来发送/接收请求/响应。

有几种 HTTP 方法,我们将实现其中两种;GETPOST

GET

当我们输入一个地址到 Web 浏览器地址栏并按下回车键时会发生什么?(我们通常不指定端口号,尽管 TCP/IP 需要端口号,因为 http 有一个默认值,即 80。如果端口是 80,我们不必指定它。)

GET / HTTP/1.1\r\n
Host: atasoyweb.net\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/14.0.1\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: tr-tr,tr;q=0.8,en-us;q=0.5,en;q=0.3\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n\r\n

这就是我们的浏览器使用 TCP/IP 发送到服务器的 GET 请求。这意味着浏览器请求服务器从 “atasoyweb.net” 的根文件夹发送 “/” 的内容。

我们(或浏览器)可以添加更多头部信息。但这个请求最简化的版本如下:

GET / HTTP/1.1\r\n
Host: atasoyweb.net\r\n\r\n 

POST

POST 请求与 GET 请求相似。在 GET 请求中,变量使用 ? 字符附加到 URL。但在 POST 请求中,变量附加到请求的末尾,在 2 个换行符之后,并指定总长度(content-length)。

POST /index.html HTTP/1.1\r\n
Host: atasoyweb.net\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20100101 Firefox/15.0.1\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: tr-tr,tr;q=0.8,en-us;q=0.5,en;q=0.3\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
Referer: http://atasoyweb.net/\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 35\r\n\r\n
variable1=value1&variable2=value2

这个请求的简化版本

POST /index.html HTTP/1.1\r\n
Host: atasoyweb.net\r\n
Content-Length: 35\r\n\r\n
variable1=value1&variable2=value2 

响应

当服务器收到请求时,它会被解析并返回一个带有状态码的响应。

HTTP/1.1 200 OK\r\n
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r\n
Content-Length: {content_length}\r\n
Connection: close\r\n
Content-Type: text/html; charset=UTF-8\r\n\r\n
the content of which length is equal to {content_length}

这是响应头部。“200 OK” 表示一切都 OK,请求的内容将被返回。有很多状态码。我们将只使用 200501404

  • 501 Not Implemented”:方法未实现。我们只实现 GETPOST。因此,对于所有其他方法,我们将发送带有此代码的响应。
  • 404 Not Found”:请求的内容未找到。

内容类型

服务器必须在其响应中指定内容类型。有很多内容类型,它们也称为“MIME(多用途互联网邮件扩展)类型”(因为它们也用于标识电子邮件中的非 ASCII 部分)。以下是我们将在实现中使用的一些内容类型:(您可以修改代码并添加更多)

  • text/html
  • text/xml
  • text/plain
  • text/css
  • image/png
  • image/gif
  • image/jpg
  • image/jpeg
  • application/zip

如果服务器指定了错误的内容类型,内容将被误解。例如,如果服务器使用“image/png”类型发送纯文本,客户端会尝试将文本显示为图像。

多线程

如果我们在向另一个客户端发送响应的同时,仍然希望我们的服务器可用,我们必须为每个请求创建新线程。因此,每个线程处理单个请求,并在完成任务后退出。(多线程也加快了页面加载速度,因为如果我们请求一个使用 CSS 和包含图像的页面,对于每个图像和 CSS 文件都会发送不同的 GET 请求。)

实现一个简单的 Web 服务器

现在我们准备实现一个简单的 Web 服务器。首先,让我们定义将要使用的变量。

public bool running = false; // Is it running?

private int timeout = 8; // Time limit for data transfers.
private Encoding charEncoder = Encoding.UTF8; // To encode string
private Socket serverSocket; // Our server socket
private string contentPath; // Root path of our contents

// Content types that are supported by our server
// You can add more...
// To see other types: http://www.webmaster-toolkit.com/mime-types.shtml
private Dictionary<string, string> extensions = new Dictionary<string, string>()
{ 
    //{ "extension", "content type" }
    { "htm", "text/html" },
    { "html", "text/html" },
    { "xml", "text/xml" },
    { "txt", "text/plain" },
    { "css", "text/css" },
    { "png", "image/png" },
    { "gif", "image/gif" },
    { "jpg", "image/jpg" },
    { "jpeg", "image/jpeg" },
    { "zip", "application/zip"}
};

启动服务器的方法

public bool start(IPAddress ipAddress, int port, int maxNOfCon, string contentPath)
{
    if (running) return false; // If it is already running, exit.

    try
    {
        // A tcp/ip socket (ipv4)
        serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
                       ProtocolType.Tcp);
        serverSocket.Bind(new IPEndPoint(ipAddress, port));
        serverSocket.Listen(maxNOfCon);
        serverSocket.ReceiveTimeout = timeout;
        serverSocket.SendTimeout = timeout;
        running = true;
        this.contentPath = contentPath;
    }
    catch { return false; }

    // Our thread that will listen connection requests
    // and create new threads to handle them.
    Thread requestListenerT = new Thread(() =>
    {
        while (running)
        {
            Socket clientSocket;
            try
            {
                clientSocket = serverSocket.Accept();
                // Create new thread to handle the request and continue to listen the socket.
                Thread requestHandler = new Thread(() =>
                {
                    clientSocket.ReceiveTimeout = timeout;
                    clientSocket.SendTimeout = timeout;
                    try { handleTheRequest(clientSocket); }
                    catch
                    {
                        try { clientSocket.Close(); } catch { }
                    }
                });
                requestHandler.Start();
            }
            catch{}
        }
    });
    requestListenerT.Start();

    return true;
}

停止服务器的方法

public void stop()
{
    if (running)
    {
        running = false;
        try { serverSocket.Close(); }
        catch { }
        serverSocket = null;
    }
}

代码最重要的部分

private void handleTheRequest(Socket clientSocket)
{
    byte[] buffer = new byte[10240]; // 10 kb, just in case
    int receivedBCount = clientSocket.Receive(buffer); // Receive the request
    string strReceived = charEncoder.GetString(buffer, 0, receivedBCount);

    // Parse method of the request
    string httpMethod = strReceived.Substring(0, strReceived.IndexOf(" "));

    int start = strReceived.IndexOf(httpMethod) + httpMethod.Length + 1;
    int length = strReceived.LastIndexOf("HTTP") - start - 1;
    string requestedUrl = strReceived.Substring(start, length);

    string requestedFile;
    if (httpMethod.Equals("GET") || httpMethod.Equals("POST"))
        requestedFile = requestedUrl.Split('?')[0];
    else // You can implement other methods...
    {
        notImplemented(clientSocket);
        return;
    }

    requestedFile = requestedFile.Replace("/", @"\").Replace("\\..", "");
    start = requestedFile.LastIndexOf('.') + 1;
    if (start > 0)
    {
        length = requestedFile.Length - start;
        string extension = requestedFile.Substring(start, length);
        if (extensions.ContainsKey(extension)) // Do we support this extension?
            if (File.Exists(contentPath + requestedFile)) //If yes check existence of the file
                // Everything is OK, send requested file with correct content type:
                sendOkResponse(clientSocket,
                  File.ReadAllBytes(contentPath + requestedFile), extensions[extension]);
            else
                notFound(clientSocket); // We don't support this extension.
                                        // We are assuming that it doesn't exist.
    }
    else
    {
        // If file is not specified try to send index.htm or index.html
        // You can add more (default.htm, default.html)
        if (requestedFile.Substring(length - 1, 1) != @"\")
            requestedFile += @"\";
        if (File.Exists(contentPath + requestedFile + "index.htm"))
            sendOkResponse(clientSocket,
              File.ReadAllBytes(contentPath + requestedFile + "\\index.htm"), "text/html");
        else if (File.Exists(contentPath + requestedFile + "index.html"))
            sendOkResponse(clientSocket,
              File.ReadAllBytes(contentPath + requestedFile + "\\index.html"), "text/html");
        else
            notFound(clientSocket);
    }
}

不同状态码的响应

private void notImplemented(Socket clientSocket)
{
   
    sendResponse(clientSocket, "<html><head><meta 
        http-equiv=\"Content-Type\" content=\"text/html; 
        charset=utf-8\">
        </head><body><h2>Atasoy Simple Web 
        Server</h2><div>501 - Method Not 
        Implemented</div></body></html>", 
        "501 Not Implemented", "text/html");
}

private void notFound(Socket clientSocket)
{
  
    sendResponse(clientSocket, "<html><head><meta 
        http-equiv=\"Content-Type\" content=\"text/html; 
        charset=utf-8\"></head><body><h2>Atasoy Simple Web 
        Server</h2><div>404 - Not 
        Found</div></body></html>", 
        "404 Not Found", "text/html");
}

private void sendOkResponse(Socket clientSocket, byte[] bContent, string contentType)
{
    sendResponse(clientSocket, bContent, "200 OK", contentType);
}

发送响应给客户端的方法

// For strings
private void sendResponse(Socket clientSocket, string strContent, string responseCode,
                          string contentType)
{
    byte[] bContent = charEncoder.GetBytes(strContent);
    sendResponse(clientSocket, bContent, responseCode, contentType);
}

// For byte arrays
private void sendResponse(Socket clientSocket, byte[] bContent, string responseCode,
                          string contentType)
{
    try
    {
        byte[] bHeader = charEncoder.GetBytes(
                            "HTTP/1.1 " + responseCode + "\r\n"
                          + "Server: Atasoy Simple Web Server\r\n"
                          + "Content-Length: " + bContent.Length.ToString() + "\r\n"
                          + "Connection: close\r\n"
                          + "Content-Type: " + contentType + "\r\n\r\n");
        clientSocket.Send(bHeader);
        clientSocket.Send(bContent);
        clientSocket.Close();
    }
    catch { }
}

用法

// to create new one:
Server server = new Server();
// to start it
server.start(ipAddress, port, maxconnections, contentpath);
// to stop it
server.stop();

向全世界问好!

我们的简单 Web 服务器已准备就绪。现在我们将使其可以通过互联网访问。为了实现这一点,我们必须将到达我们调制解调器的请求重定向到我们的计算机。如果我们的调制解调器支持 UPnP,那将很简单。

  1. 下载这个 UPnP 端口转发工具 并运行它。
  2. 点击“搜索设备”按钮。如果您的调制解调器支持 UPnP,它将被添加到组合框中。
  3. 点击“更新列表”按钮以列出已转发的端口。
  4. 然后点击“添加新”按钮并填写表单。
  5. 如果您勾选“IP”复选框并输入一个 IP,则只有来自该 IP 的请求将被重定向。所以,不要填写它。
  6. 内部端口必须等于我们服务器的端口。
  7. 端口”和“内部端口”不必相等。

Adding new port forwarding entry

从现在开始,所有到达“externalip:port”的请求都将从调制解调器重定向到我们的计算机。要测试服务器是否可以从互联网访问,您可以使用 www.web-sniffer.net。只需将您的外部 IP 和端口写成“http://externalip:port”然后点击“提交”按钮...

Test result

历史

  • 2012年9月13日:解释并支持了 POST 方法。
  • 2012年9月3日:第一个版本
© . All rights reserved.