WebServer的简单实现

来源:互联网 发布:淘宝卖家有被骗的吗? 编辑:程序博客网 时间:2024/04/27 23:35

先来说一说问题。在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的实现,其具体处理流程如下:

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


从上边可以看出,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();        }    }}



原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 逆战画面卡顿怎么办 老公心里没你的怎么办 老公出轨了妻子到底应该怎么办 函调对方不复函怎么办 新车自己刮蹭了怎么办 新车被蹭了点漆怎么办 白色的车刮了漆怎么办 车蹭了一点漆怎么办 汽车被蹭了点漆怎么办 新车蹭了一点漆怎么办 手表金属刮花了怎么办 碰了别人车跑了怎么办 老赖拘留15天后怎么办 刮到别人车跑了怎么办 被班里人孤立了怎么办 游戏停服了玩家怎么办 u盘提示要格式化怎么办 微博账号冻结了怎么办 微博登录名空白怎么办 知道微博登录名怎么办 打球跳不起来了怎么办 拉杆箱把手坏了怎么办 拉杆箱拉链坏了怎么办 乳液按压头坏了怎么办 欠款人联系不上怎么办 微信登陆不上去怎么办 微信登录不上去怎么办 炖熟的鸡肉太柴怎么办 长徒了的多肉怎么办 风投失败了钱怎么办 我该怎么办啊迷茫没钱 儿子欠钱我该怎么办 想妈妈想哭了怎么办啊 怀孕初期hcg翻倍不好怎么办 买了c类的衣服怎么办 主页被360串改怎么办? 入职体检查乙肝怎么办 入职体检两对半怎么办? 入职体检肝功能异常怎么办 有乙肝怀孕建卡怎么办 重修的课有冲突怎么办