欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

WebServer的简单实现

程序员文章站 2024-03-14 11:41:10
...

先来说一说问题。在Web应用中,当HTTP服务器与多个客户端通信时,服务器会创建多个线程并行处理每个HTTP请求。HTTP通信又是建立在可靠的TCP连接之上,此时服务器一个端口(一般为80)就需要创建多个TCP连接,那么服务器是怎么处理?

首先我们必须清楚,一个TCP连接的唯一标识由:【源IP】+【源端口】+【目的IP】+【目的端口】四部分组成,这里需要特别注意。我当时错认为,对于服务器来说一个TCP连接只包括【源IP】+【源端口】,那么当多个客户端与服务器同一个端口创建连接时,那岂不是冲突了?后面认真的查阅了《TCP/IP详解》,才发现无论是服务器端还是客户端,一个本地唯一的TCP连接由【源IP】+【源端口】+【目的IP】+【目的端口】四部分组成。那么当某个端口已经被监听之后,我们就无法再创建一个新的TCP监听对象?这里以服务器80端口为例:

当服务创建对80端口的TCP监听之后,意味着一个端口同时只能服务于同一个本地进程。也就说我们在代码中(这里使用C#代码):

TcpListener myListener = new TcpListener(80);

如果80端口已经被占用,那么系统就会报错,实例化TcpListener对象失败。

当80端口处于监听状态时,就可以执行下一步TCP连接的建立了。TCP数据包到达服务器后,经过链路层、网络层处理之后,就送到TCP层,在Berkeley TCP/IP的实现代码中这个接口函数为:tcp_input()

这个函数的预处理流程中有个环节叫做:Locate Internet PCB,定位因特网协议控制块。

TCP maintains a one-behind cache (tcp_last_inpcb) containing the address of the PCB for the last received TCP segment. This is the same technique used by UDP. The comparison of the four element in the socket pair is in the same order as done by udp_input. If the cache entry does not match, in_pcblookup is called, and the cache is set to the new PCB entry.
TCP does not have the same problem that we encountered with UDP: wildcard entries in the cache causing a high miss rate. The only time a TCP socket has a wildcard entry is for a server listening for connection requests. Once a connection is made, all four entries in the socket pair contain nonwildcard values.

因为UDP是无状态,不需要建立连接,当有数据到来时,只需要根据【源IP】+【源端口】+【目的IP】+【目的端口】这四部分从缓存中查询PCB,如果不存在就调用in_pcblookup函数创建新的PCB入口。但是对于TCP来说,一旦连接建立之后,就不需要创建新的PCB。具体的处理是:如果PCB状态为CLOSED或者PCB不存在,那么就丢弃当前的数据。否则,就通过sonewconn函数创建一个新的socket连接。

Lwip是参考Berkeley 对TCP/IP的实现,其具体处理流程如下:

WebServer的简单实现

当TCP层接收到从IP层传来的tcp_datagram之后,统一由tcp_input()函数来处理,这里出现了三个分支:tcp_process()、tcp_timewait_input()、tcp_listen_input()。具体的处理流程如下图所示:

WebServer的简单实现

从上边可以看出,TCP协议包中维护了整个TCP的连接状态,并通过回调机制实现与上层的通信,一般不建议在回调中做复杂的操作,因为回调的具体处理由内核完成,如果处理过程复杂或有异常,会导致内核的崩溃,为确保程序的安全可靠,一般在上层应用中采用查询的机制。下面以Web server为例,简单说明整个处理流程:

WebServer.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace web
{
    class WebServer
    {
        // TCP监听对象
        private TcpListener myListener;
        // 监听端口
        private int port = 8080;

        // 实例化函数
        public WebServer()
        {
            try
            {
                // 开始兼听端口
                myListener = new TcpListener(port);

                // 开始监听
                myListener.Start();
                Console.WriteLine("Web Server 运行中.. 按 ^C 停止运行...");

                // 创建一个监听处理线程
                Thread th = new Thread(new ThreadStart(StartListen));
                th.Start();
            }
            catch (Exception e)
            {
                Console.WriteLine("兼听端口时发生错误 :" + e.ToString());
            }
        }

        /*
         * SendHeader
         * @Description : 使用指定套接字发送头部数据
         * @param sHttpVersion: string, HTTP版本号字符串
         * @param sMIMEHeader : string, MIME头字符串
         * @param iTotBytes   : int, 数据长度
         * @param sStatusCode : string, 状态码
         * @param mySocket    : ref Socket, 套接字引用
         * @return      : none
         */
        public void SendHeader(string sHttpVersion, string sMIMEHeader, int iTotBytes, string sStatusCode, ref Socket mySocket)
        {

            String sBuffer = "";
            if (sMIMEHeader.Length == 0)
            {
                sMIMEHeader = "text/html"; // 默认 text/html
            }
            sBuffer = sBuffer + sHttpVersion + sStatusCode + "\r\n";
            sBuffer = sBuffer + "Server: cx1193719-b\r\n";
            sBuffer = sBuffer + "Content-Type: " + sMIMEHeader + "\r\n";
            sBuffer = sBuffer + "Accept-Ranges: bytes\r\n";
            sBuffer = sBuffer + "Content-Length: " + iTotBytes + "\r\n\r\n";
            Byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer);
            SendToBrowser(bSendData, ref mySocket);
            Console.WriteLine("Total Bytes : " + iTotBytes.ToString());
        }

        public void SendToBrowser(String sData, ref Socket mySocket)
        {
            SendToBrowser(Encoding.ASCII.GetBytes(sData), ref mySocket);
        }

        /*
         * SendToBrowser
         * @Description : 通过套接字向浏览器发送信息
         * @param bSendData   : Byte[], 待发送信息数组
         * @param mySocket    : ref Socket, 套接字引用
         * @return            : none
         */
        public void SendToBrowser(Byte[] bSendData, ref Socket mySocket)
        {
            int numBytes = 0;

            try
            {
                if (mySocket.Connected)
                {
                    if ((numBytes = mySocket.Send(bSendData, bSendData.Length, 0)) == -1)
                        Console.WriteLine("Socket Error cannot Send Packet");
                    else
                    {
                        Console.WriteLine("No. of bytes send {0}", numBytes);
                    }
                }
                else
                    Console.WriteLine("连接失败....");
            }
            catch (Exception e)
            {
                Console.WriteLine("发生错误 : {0} ", e);

            }
        }
        
        /*
         * StartListen
         * @Description : 监听线程,监听端口TCP连接,并进行处理
         * @return      : none
         */
        public void StartListen()
        {
            // 用于字符串中定位
            int iStartPos = 0;
            // 请求字符串
            String sRequest;
            // 请求资源的目录
            String sDirName;
            // 请求的文件
            String sRequestedFile;
            // 错误信息
            String sErrorMessage;
            // 本地目录
            String sLocalDir;
            // 注意设定你自己的虚拟目录
            String sMyWebServerRoot = "C:\\Cassini\\";
            // 格式化信息
            String sFormattedMessage = "";
            // 响应字符串
            String sResponse = "";

            // 不断遍历查询
            while (true)
            {
                // 从监听的连接中获取一个新建立的连接,如果请求队列为空即还未有客户端发起TCP连接建立请求
                // 该操作会被阻塞,直到新的连接到来。当接收到连接后,会返回一个Socket实例。
                Socket mySocket = myListener.AcceptSocket();

                Console.WriteLine("Socket Type " + mySocket.SocketType);
                //如果为连接状态,说明有请求到来
                if (mySocket.Connected)
                {
                    Console.WriteLine("\nClient Connected!!\n==================\nCLient IP {0}\n", mySocket.RemoteEndPoint);

                    Byte[] bReceive = new Byte[1024];

                    //从套接字中取出数据到字节数组中
                    int i = mySocket.Receive(bReceive, bReceive.Length, 0);

                    //转换成字符串类型
                    string sBuffer = Encoding.ASCII.GetString(bReceive);

                    Console.WriteLine(sBuffer);
                    //只处理"get"请求类型
                    if (sBuffer.Substring(0, 3) != "GET")
                    {
                        Console.WriteLine("只处理get请求类型..");
                        mySocket.Close();
                        return;
                    }

                    // 查找 "HTTP" 的位置
                    iStartPos = sBuffer.IndexOf("HTTP", 1);

                    // 获取HTTP协议字符串
                    string sHttpVersion = sBuffer.Substring(iStartPos, 8);

                    // 得到请求类型和文件目录文件名
                    sRequest = sBuffer.Substring(0, iStartPos - 1);

                    // windows平台下目录分隔符
                    sRequest.Replace("\\", "/");


                    //如果结尾不是文件名也不是以"/"结尾则加"/"
                    if ((sRequest.IndexOf(".") < 1) && (!sRequest.EndsWith("/")))
                    {
                        sRequest = sRequest + "/";
                    }


                    // 获取请求的文件名
                    iStartPos = sRequest.LastIndexOf("/") + 1;
                    sRequestedFile = sRequest.Substring(iStartPos);

                    // 获取请求的文件目录
                    sDirName = sRequest.Substring(sRequest.IndexOf("/"), sRequest.LastIndexOf("/") - 3);

                    // 获取虚拟目录物理路径
                    sLocalDir = sMyWebServerRoot;
                    Console.WriteLine("请求文件目录 : " + sLocalDir);

                    // 目录不存在,返回404错误
                    if (sLocalDir.Length == 0)
                    {
                        sErrorMessage = "<H2>Error!! 请求的目录不存在</H2><Br>";
                        SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", ref mySocket);
                        SendToBrowser(sErrorMessage, ref mySocket);
                        mySocket.Close();
                        continue;
                    }


                    if (sRequestedFile.Length == 0)
                    {
                        // 取得请求文件名
                        sRequestedFile = "index.html";
                    }


                    // 取得请求文件类型(设定为text/html)
                    String sMimeType = "text/html";

                    string sPhysicalFilePath = sLocalDir + sRequestedFile;
                    Console.WriteLine("请求文件: " + sPhysicalFilePath);


                    if (File.Exists(sPhysicalFilePath) == false)
                    {

                        sErrorMessage = "<script language='javascript'>alert('你好呀,我不是IIS服务器!');</script>";
                        //SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", ref mySocket);
                        //SendToBrowser(sErrorMessage, ref mySocket);
                        byte[] bytes = Encoding.GetEncoding("GB2312").GetBytes(sErrorMessage);
                        SendHeader(sHttpVersion, sMimeType, bytes.Length, " 200 OK", ref mySocket);
                        SendToBrowser(bytes, ref mySocket);
                        //Console.WriteLine(sFormattedMessage);
                    }
                    else
                    {
                        int iTotBytes = 0;
                        sResponse = "";

                        // 打开文件资源句柄
                        FileStream fs = new FileStream(sPhysicalFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);

                        // 打开文件二进制读取流
                        BinaryReader reader = new BinaryReader(fs);
                        // 暂存数组
                        byte[] bytes = new byte[fs.Length];
                        int read;
                        // 循环读取文件内容
                        while ((read = reader.Read(bytes, 0, bytes.Length)) != 0)
                        {
                            sResponse = sResponse + Encoding.ASCII.GetString(bytes, 0, read);

                            iTotBytes = iTotBytes + read;

                        }
                        // 关闭资源句柄
                        reader.Close();
                        fs.Close();
                        // 发送给浏览器
                        SendHeader(sHttpVersion, sMimeType, iTotBytes, " 200 OK", ref mySocket);
                        SendToBrowser(bytes, ref mySocket);

                    }
                    // 关闭socket
                    mySocket.Close();
                }
            }
        }
    }
}

program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace web
{
    class Program
    {
        static void Main(string[] args)
        {
            WebServer web = new WebServer();
            Console.Read();
        }
    }
}