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