Elijah

治愈系代码研究基地
  首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

[传智播客学习日记]手写Web服务器

Posted on 2011-11-30 21:53  Elijah  阅读(1024)  评论(0编辑  收藏  举报

花了两天的时间搞的这个,写这个东西目的就是要搞清楚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 }