• [传智播客学习日记]手写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 //就要根据后缀名生成contenttype
    37 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.html
    14 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 }
  • 相关阅读:
    字体图标的制作
    vs code 本地调试配置
    瀑布流
    web组件化开发第一天
    超时调用和间歇调用
    递归 闭包
    继承
    面向对象的程序设计
    function类型
    Date类型
  • 原文地址:https://www.cnblogs.com/Elijah/p/2269678.html
Copyright © 2020-2023  润新知