zoukankan      html  css  js  c++  java
  • C#开发自己的Web服务器

    介绍

    我们将学习如何写一个简单的web服务器,用于响应知名的HTTP请求(GET和POST),用C#发送响应。然后,我们从网络访问这台服务器,这次我们会说“Hello world!”

    背景

    HTTP协议

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

    有几个HTTP方法,我们将实现两个:GET和POST。

    GET

    当我们将一个地址输入到我们的Web浏览器的地址栏中,按下回车键时,会发生什么情况?(虽然我们使

    用TCP/IP,但我们不指定端口号,因为HTTP默认使用80端口,我们并不需要指定80)

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

    该GET请求使用TCP/IP通过浏览器向服务器发送,请求的是okbase.net的根目录下的内容。

    我们可以添加更多的头信息,最基本的信息如下:

    1
    2
    GET / HTTP/1.1
    Host: okbase.net

    POST

    POST请求和GET请求类似,在GET请求里,变量加到url的?下面,POST请求,变量加到两行回车的下面,并需要指定内容长度。

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

    简化版本如下:

    1
    2
    3
    4
    POST /index.html HTTP/1.1
    Host: okbase.net
    Content-Length: 35
    variable1=value1&variable2=value2

    响应

    当服务器接收到一个请求进行解析,并返回一个响应状态代码:

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

    这是一个响应头,"200 OK"便是一切OK,请求的内容将被返回。状态码有很多,我们经常使用200,501,404。

    “501 Not Implemented”方法未实现。我们将只实现GET和POST。如果通过其它方法请求,我们将发送此代码。

    “404 Not Found”:没有找到请求的内容。

    内容类型

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

    text/html

    text/xml

    text/plain

    text/css

    image/png

    image/gif

    image/jpg

    image/jpeg

    application/zip

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

    多线程

    如果我们使我们的服务器可以同时响应多个客户端,我们必须为每个请求创建新的线程。因此,每个线程处理一个请求,并退出完成它的使命。(多线程也加快了页面加载,因为如果我们请求一个页面,页面中使用了CSS和图像,每个图像和CSS文件会以GET方式发送请求)。

    一个简单的Web服务器的实现

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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...
    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"}
    };

    启动服务器的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    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;
    }

    停止服务器的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public void stop()
    {
        if (running)
        {
            running = false;
            try { serverSocket.Close(); }
            catch { }
            serverSocket = null;
        }
    }

    最重要的部分代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    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);
        }
    }

    不同的状态代码的响应:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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);
    }

    将响应发送到客户端的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 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 + " "
                              + "Server: Atasoy Simple Web Server "
                              + "Content-Length: " + bContent.Length.ToString() + " "
                              + "Connection: close "
                              + "Content-Type: " + contentType + " ");
            clientSocket.Send(bHeader);
            clientSocket.Send(bContent);
            clientSocket.Close();
        }
        catch { }
    }

    用法

    1
    2
    3
    4
    5
    6
    // to create new one:
    Server server = new Server();
    // to start it
    server.start(ipAddress, port, maxconnections, contentpath);
    // to stop it
    server.stop();

    向全世界说"Hello"

    我们简单的Web服务器已准备就绪。现在,我们将从Internet访问它。为了实现这一目标,我们必须将请求从Modem重定向到我们的计算机。如果我们的调制解调器支持UPnP那就很简单。

    1. 下载UPnp Port Forwarder ,并运行它。

    2. 点击“Search For Devices”按钮。如果您的调制解调器支持UPnP,它会被添加到ComboBox。

    3. 点击“Update List”按钮,列出转发端口。

    4. 然后点击“Add New”按钮,填写表格。

    5. 如果选中“IP”复选框,并输入一个IP地址,只有从这个IP的请求将被重定向。所以,千万不要填写。

    6. 内部端口必须等于我们服务器的端口。

    7.“Port”和“ Internal port”可以不相同。

    这样,所有来自externalip:port的请求都将通过modem转发到我们的电脑上,你可以用http://www.web-sniffer.net/ 来测试连接的有效性,输入外部IP和端口号 http://外部IP:端口号并点击Submit按钮...

    (全文完)
  • 相关阅读:
    cron表达式
    mybatis中的#{}和${}
    mysql免安装版使用
    关于jpa添加一对多数据时外键值为空的问题
    关于时间格式化和格式转换
    属性和属性节点
    ErrorException: ORA-00904: : 标识符无效
    345. 反转字符串中的元音字母
    java 解决nginx代理的跨域访问问题
    633. 平方数之和
  • 原文地址:https://www.cnblogs.com/lhuan/p/3829158.html
Copyright © 2011-2022 走看看