• 手写web服务器


    花了两天的时间搞的这个,写这个东西目的就是要搞清楚ASP.Net的运作原理。

    这个山寨服务器的界面很简单,三个文本框,写IP、端口,还有一个显示报文。一个连接按钮。窗体嘛...就叫Form1吧。代码比较冗长...
    第一步:

     1 //搭建好窗口,为了防止意外,先: 2 public Form1()  3 {  4     Control.CheckForIllegalCrossThreadCalls = false;  5     InitializeComponent();  6 }  7 //全局线程th用于监听,当窗口关闭时, 8 private void Form1_FormClosing(object sender, FormClosingEventArgs e)  9 { 10     if (th != null) 11     { 12         th.Abort(); 13     } 14 } 15 //另外定义ShowMsg方法:16 void ShowMsg(string msg) 17 { 18     txtLog.Text += msg + "\r\n"; 19 }

    第二步: 在线程中进行循环监听,不多解释了,还是Socket那一套(可以看我上一篇博文):

     1 Thread th;  2 private void btnStart_Click(object sender, EventArgs e)  3 {  4     IPAddress ip = IPAddress.Parse(txtIp.Text);  5     IPEndPoint endpoint = new IPEndPoint(ip, int.Parse(txtPort.Text));  6     Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  7   8     try  9     { 10         socket.Bind(endpoint); 11     } 12     catch (Exception ex) 13     { 14         ShowMsg(ex.Message); 15         return; 16     } 17     socket.Listen(10); 18     ShowMsg("开始运行....."); 19  20     btnStart.Enabled = false; 21  22     th = new Thread(Listen); 23     th.IsBackground = true; 24     th.Start(socket); 25 } 26 //监听用的方法27 void Listen(object o) 28 { 29     Socket socket = o as Socket; 30     while (true) 31     { 32         Socket connect = socket.Accept(); 33     DataConnection conn = new DataConnection(connect, ShowMsg); 34     } 35 }

    第三步: 由于每次传输完信息连接就可以断开了,所以没必要用循环来接受客户端请求。注意到上面有个叫DataConnection的类,这个类是我们自定义的,为了不让代码显得臃肿。它的构造函数是DataConnection(Socket conn,DelShowMsg del),第一个参数是我们通过监听用的Socket生成的负责传输的Socket,第二个参数是委托,进行报文显示。

     1 //首先我们在类外定义回显用的委托: 2 public delegate void DelShowMsg(string msg);  3 //下面是这个类的定义: 4 class DataConnection  5 {  6     //定义委托类的对象 7     private DelShowMsg del;  8     //以及负责通信的socket 9     private Socket connection; 10          11     //之后在构造函数里初始化传进来的Socket和委托12     public DataConnection(Socket conn,DelShowMsg del) 13     { 14         this.connection = conn; 15         this.del = del; 16  17         //用一个字符串接收浏览器发来的请求报文 18 //这个方法也是自己写的,解释在后面19         string msg = RecMsg(); 20  21         //然后解析请求头,这个类还是我们自己写的22         Request req = new Request(msg); 23  24         //根据解析后的请求头中的地址,判断请求文件的类型,并向浏览器做出响应 25 //这个方法也是自己写的,为了防止代码臃肿26         Judge(req.Path); 27     }

    (类中的方法还没写完)
    第四步: 第三步中我们留下了RecMsg方法、Judge方法和Request类没有写。先来写RecMsg方法。

     1 //RecMsg方法很简单,用来接收消息 2 string RecMsg()  3 {  4     //定义缓冲区 5     byte[] buffer = new byte[1024 * 1024 * 5];  6     //服务器获取请求报文,并返回长度 7     int length = connection.Receive(buffer);  8   9     //得到请求报文字符串10     string msg = System.Text.Encoding.UTF8.GetString(buffer, 0, length); 11     //显示报文12     del(msg); 13  14     del("连接关闭"); 15     return msg; 16 }

    第五步: 第三步中的Request类用来解析请求报文获得请求的路径,原理就是切割字符串。

     1 class Request  2 {  3     public Request(string msg)  4     {  5         //这里根据换换行符切割报文获取每一行 6         string[] arrLines = msg.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);  7         //然后获取请求行 8         string[] firstLine = arrLines[0].Split(' ');  9     //以及各个属性10         method = firstLine[0]; 11         path = firstLine[1]; 12         protocol = firstLine[2]; 13     } 14  15     //山寨版服务器很简陋,这里只封装三个属性16     private string method; 17     public string Method 18     { 19         get { return method; } 20         set { method = value; } 21     } 22  23     private string path; 24     public string Path 25     { 26         get { return path; } 27         set { path = value; } 28     } 29  30     private string protocol; 31     public string Protocol 32     { 33         get { return protocol; } 34         set { protocol = value; } 35     } 36 }

    第六步: 接下来是第三步在DataConnection类中遗留的Judge方法,它接受上一步Request req = new Request(msg);之后得到的对象中的path属性,也就是地址,进行文件类型的判断。即Judge(req.Path);。

     1 void Judge(string path)  2 {  3     //首先拿到地址的扩展名, 4     string ext = Path.GetExtension(path);  5     //根据扩展名判断到底是静态页面还是动态页面  6 //并分别处理 7     switch (ext)  8     {  9         case ".gif": 10         case ".jpg": 11         case ".png": 12         case ".html": 13         case ".htm": 14         case ".css": 15         case ".js": 16             ProcessStaticPage(path); 17             break; 18         case ".aspx": 19         case ".jsp": 20             ProcessDyPage(path); 21             break; 22         default: 23             break; 24     } 25 }

    第七步: ProcessStaticPage和ProcessDyPage是处理静态和动态页面的两个方法,我们要通过服务器返回响应头和响应体。响应体好说,定义一个buffer就行了,问题是响应头很复杂,我们需要定义一个Response类来生成和拿到它,然后再处理ProcessStaticPage和ProcessDyPage这两个函数。这一步就是写Response类。

     1 class Response  2 {  3     //200是连接成功的状态字,由于大部分连接都是成功的,所以设置成默认 4     private int status = 200;  5     private string contentType;  6     private int contentLength;  7   8     //把状态字对应的消息放到字典里 9     private Dictionary<int, string> dic; 10  11     //写字典12     void FillDic() 13     { 14         dic = new Dictionary<int, string>(); 15         dic.Add(200,"OK"); 16         dic.Add(404, "Object Not Found"); 17         dic.Add(302, "Found"); 18     } 19  20     //默认构造函数(200的情况)21     public Response(string ext, int contentLength) 22     { 23         this.contentLength = contentLength; 24         this.contentType = GetContentType(ext); 25         FillDic(); 26     } 27  28     //不是200的情况的构造函数,比如404了29     public Response(int status,string ext,int contentLength) 30         :this(ext,contentLength) 31     { 32         this.status = status; 33     } 34  35     //由于不同的后缀名对应不同的contentType 36 //就要根据后缀名生成contenttype37     private string GetContentType(string ext) 38     { 39         string contentType = ""; 40         switch (ext) 41         { 42             case ".htm": 43             case ".html": 44                 contentType = "text/html"; 45                 break; 46             case ".css": 47                 contentType = "text/css"; 48                 break; 49             case ".js": 50                 contentType = "text/javascript"; 51                 break; 52             case ".jpg": 53                 contentType = "image/jpeg"; 54                 break; 55             case ".gif": 56                 contentType = "image/gif"; 57                 break;   58             default: 59                 contentType = "text/html"; 60                 break; 61         } 62         return contentType; 63     } 64  65     //上面折腾半天就是拼接字符串呢 66 //接下来获得响应头并返回67     public byte[] GetHeaders() 68     { 69         StringBuilder sb = new StringBuilder(); 70         sb.Append("HTTP/1.1 "+status+" " + dic[status] + "\r\n"); 71         sb.Append("Content-Length: " + contentLength+"\r\n"); 72     //这里一定要换行,因为响应头和响应体之间有空行,否则无法解析73         sb.Append("Content-Type: " + contentType + ";charset=utf-8\r\n\r\n"); 74  75         byte[] buffer = System.Text.Encoding.UTF8.GetBytes(sb.ToString()); 76         return buffer; 77     } 78 }

    第八步: 现在该写ProcessStaticPage和ProcessDyPage这两个方法来处理静态和动态页面了。先弄静态的。

     1 void ProcessStaticPage(string path)  2 {  3     //首先找到静态文件的绝对路径,擦掉多出来的斜杠 4     path =AppDomain.CurrentDomain.BaseDirectory + path.Remove(0,1);  5     //先生成获取响应头的类 6     Response res = null;  7     //定义好响应体 8     byte[] buffer;  9  10     //然后判断请求的文件是否存在11     if (!File.Exists(path)) 12     { 13         //如果文件不存在,读取404.html14         path = AppDomain.CurrentDomain.BaseDirectory + "404.html"; 15     //然后把404页面写进响应体16         using(FileStream fs = new FileStream(path,FileMode.Open)) 17         { 18             buffer = new byte[fs.Length]; 19             fs.Read(buffer, 0, buffer.Length); 20             res = new Response(404,Path.GetExtension(path), buffer.Length); 21         } 22     } 23     else 24     { 25         //如果文件存在26         using (FileStream fs = new FileStream(path, FileMode.Open)) 27         { 28             buffer = new byte[fs.Length]; 29             fs.Read(buffer, 0, buffer.Length); 30             res = new Response(Path.GetExtension(path), buffer.Length); 31         } 32     } 33     //发送响应头34     connection.Send(res.GetHeaders()); 35     //发送响应体36     connection.Send(buffer); 37     //关闭连接38     connection.Close(); 39 }

    第九步: 处理动态页面麻烦一些,因为我们要根据请求的文件名来找对应同名的类,所以要用到反射技术。

     1 void ProcessDyPage(string path)  2 {  3     //根据请求的文件名,创建对应的类的对象  4 //获得文件名 5     string fileName = Path.GetFileNameWithoutExtension(path);  6     //获得类所在的命名空间 7     string nameSpace = System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Namespace;  8     //获得类的全名称 9     string fullName = nameSpace + "." + fileName; 10  11     //注意这里,IHttpHandler是个接口,这里用了李氏替换原则 12 //保证实现了这个接口的类都能处理http请求13     IHttpHandler hander = Assembly.GetExecutingAssembly().CreateInstance(fullName,true) as IHttpHandler; 14  15     if (hander != null) 16     { 17       //用ProcessRequest方法处理请求18         byte[] buffer = hander.ProcessRequest(); 19         Response response = new Response(Path.GetExtension(path), buffer.Length); 20  21         connection.Send(response.GetHeaders()); 22         connection.Send(buffer); 23  24         connection.Close(); 25     } 26     else 27     { 28         //处理404,不写了29     } 30 }

    第十步: 上面有一个IHttpHandler接口,接口里面有一个byte[] ProcessRequest();方法,因为这里是用反射去找字符串对应的同名类名的,这里之所以不写判断逻辑或者简单工厂,是因为一旦判断,我每增加一个页面都要改一次代码,所以采用反射机制。接口是在动态页面对应的类中实现的,里面就是在拼html代码。比如:

     1 class MyPage : IHttpHandler  2 {  3     public byte[] ProcessRequest()  4     {  5         StringBuilder sb = "<html><body>";  6         sb.Append("当前时间:" + DateTime.Now.ToString());  7         sb.Append("</body></html>");  8         string html = sb.ToString();  9         return Encoding.UTF8.GetBytes(html); 10     } 11 }
  • 相关阅读:
    解决sql2008附加不了2005的数据库文件的问题
    方阵
    台阶问题
    螺旋矩阵
    兔子问题
    九乘九乘法口诀
    选猴王
    拿鸡蛋问题
    软工个人作业
    小学四则运算法则训练
  • 原文地址:https://www.cnblogs.com/houzhitong/p/2403908.html
Copyright © 2020-2023  润新知