目录
1. C# Socket通讯
2. HTTP 解析引擎
3. 资源读取和返回
4. 服务器测试和代码下载
Web服务器是Web资源的宿主,它需要处理用户端浏览器的请求,并指定对应的Web资源返回给用户,这些资源不仅包括HTML文件,JS脚本,JPG图片等,还包括由软件生成的动态内容。为了满足上述需求,一个完整的Web服务器工作流程:
1) 服务器获得浏览器通过TCP/ip连接向服务器发送的http请求数据包。
2) HTTP请求经过Web服务器的HTTP解析引擎分析得出请求方法、资源地址等信息,然后开始处理。
3) 对于静态请求,则在服务器上查询请求url路径下文件,并返回(如果未找到则返回404 No Found)。
4) 涉及动态请求,如CGI, Ajax, asp等,则根据http方法,采取不同处理。
Web服务器的核心由C#%20Socket通讯,http解析引擎,静态资源文件查找,动态数据接收和发送4部分组成,本节因为个人编写进度原因主要实现前3个部分(即能够查询静态资源的Web服务器),动态数据处理因为涉及的处理方式CGI,AJAX,ASP的方法不同,后续完成后在总结相关知识。
1.%20C#%20Socket通讯
C#%20Socket通过对TCP/IP协议进行封装,用于实现满足TCP通讯的API。在B/S架构中,服务器端的处理和C/S连接基本相同,主要工作包含:创建Socket套接字,监听连接,建立连接,获得请求,处理并返回数据,关闭连接等。
程序入口函数,采用轮询方式实现对客户端请求的监听。
%20%20%20%20%20%20%20%20%20%20//创建监听线程%20%20%20%20%20%20%20%20%20%20Thread%20Listen_thread%20=%20new%20Thread(socket_listen);%20%20%20%20%20%20%20%20%20%20Listen_thread.IsBackground%20=%20false;%20%20%20%20%20%20%20%20%20%20Listen_thread.Start();%20%20%20%20%20%20%20%20
监听线程,创建Socket套接字,绑定并监听指定端口,等待连接建立,连接建立后,考虑到网页请求高并发的特性,采用另开线程的方式来处理建立的连接,从而实现并发服务器模式。
%20%20%20%20%20%20%20%20%20%20%20%20Socket%20server_socket%20=%20null;%20%20%20%20%20%20%20%20%20%20%20%20//监听的IP地址和端口%20作为服务器,绑定的只能是本机Ip地址或者环回地址(不能与系统其它进程端口冲突)%20%20%20%20%20%20%20%20%20%20%20%20//如果绑定为本节IP地址,局域网下其它设备可以通过http://host:port来访问当前服务器%20%20%20%20%20%20%20%20%20%20%20%20string%20host%20=%20"127.0.0.1";%20%20%20%20%20%20%20%20%20%20%20%20int%20port%20=%203000;%20%20%20%20%20%20%20%20%20%20%20%20IPAddress%20ip%20=%20IPAddress.Parse(host);%20%20%20%20%20%20%20%20%20%20%20%20IPEndPoint%20ipe%20=%20new%20IPEndPoint(ip,%20port);%20%20%20%20%20%20%20%20%20%20%20%20//新建Socket套接字,绑定在指定的端口并开始监听%20%20%20%20%20%20%20%20%20%20%20%20server_socket%20=%20new%20Socket(AddressFamily.InterNetwork,%20SocketType.Stream,%20PRotocolType.Tcp);%20%20%20%20%20%20%20%20%20%20%20%20server_socket.Bind(ipe);%20%20%20%20%20%20%20%20%20%20%20%20server_socket.Listen(100);%20%20%20%20%20%20%20%20%20%20%20%20Console.WriteLine("Server%20Binding%20at%20"%20+%20host%20+%20":"%20+%20port.ToString()%20+"...");%20%20%20%20%20%20%20%20%20%20%20%20Console.WriteLine("Wait%20for%20connect....");%20%20%20%20%20%20%20%20%20%20%20%20while%20(true)%20%20%20%20%20%20%20%20%20%20%20%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Socket%20CurrentSocket;%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//三次握手成功,新建一个连接%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20CurrentSocket%20=%20server_socket.Accept();%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Console.WriteLine("New%20TCP%20Socket%20Create...");%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//单开一个线程用来处理服务器收发,%20并发服务器模式%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ParameterizedThreadStart%20tStart%20=%20new%20ParameterizedThreadStart(socket_process);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Thread%20process_thread%20=%20new%20Thread(tStart);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20process_thread.IsBackground%20=%20false;%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20process_thread.Start(CurrentSocket);%20%20%20%20%20%20%20%20%20%20%20%20}
连接处理,socket通讯处理主要负责接收连接产生的数据,并将http引擎处理后数据提交给客户端浏览器。
%20%20%20%20%20%20%20%20%20%20Socket%20CurrentSocket%20=%20(Socket)obj;%20%20%20%20%20%20%20%20%20%20try%20%20%20%20%20%20%20%20%20%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20string%20recvStr%20=%20"";%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20byte[]%20recvBytes%20=%20new%20byte[2000];%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20int%20length;%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//获得当前Socket连接传输的数据,并转换为ASCII码格式%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20length%20=%20CurrentSocket.Receive(recvBytes,%20recvBytes.Length,%200);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20recvStr%20=%20Encoding.ASCII.GetString(recvBytes,%200,%20length);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//http引擎处理,返回获得数据%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20byte[]%20bs%20=%20%20http_engine(recvStr,%20length);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//通过socket发送引擎处理后数据%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20CurrentSocket.Send(bs,%20bs.Length,%200);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Console.WriteLine("File%20Send%20Finish,%20Socket%20Close..../r/n");%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20//关闭socket连接%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20CurrentSocket.Close();%20%20%20%20%20%20%20%20%20%20%20%20}%20%20%20%20%20%20%20%20%20%20%20%20catch%20(Exception%20exception)%20%20%20%20%20%20%20%20%20%20%20%20{%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Console.WriteLine(exception);%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20CurrentSocket.Close();%20%20%20%20%20%20%20%20%20%20%20%20}
C#%20Socket通讯架构的实现和C/S结构没有什么区别,如果了解过Socket可以轻松实现上述socket通讯架构。%20不过下面这部分将讲述Web服务器的实现核心--http解析引擎,这也是B/S架构和C/S架构中服务器端最大的区别。
2.%20http解析引擎
%20Web服务器主要实现对浏览器请求数据包的处理,并返回指定的http资源或者数据,这些都是由http解析引擎实现,在写http解析引擎之前,我们要知晓接收到的数据才能进行后续的处理,这里提供通过WireShark抓取的http请求包:
虽然HTTP请求包的内容很多,但因为目前实现的功能较少,所以关注的只有http报文起始行就可以,而首部字段可以直接丢弃不处理,后续如果使用认证机制,如白名单,黑名单过滤,帐号/密码保护,资源权限管理等,首部仍然要处理。
对于http报文起始行, 内部以space隔开,并以'/r/n'作为结尾与首部隔开。其中GET:HTTP方法, '/' :资源路径url, HTTP/1.1:协议版本,参照http权威指南的内容, HTTP协议的常见方法有GET, PUT, DELETE, POST, HEAD这5种,本节中的静态服务器主要涉及到GET方法。了解了需要如何解析HTTP请求报文后,我们先定义一个HTTP报文解析结构,用于存储到解析的信息。
public class HTTPPrase { //http方法 public string http_method; //http资源 public string url; //http版本号 public string version; //url解析的请求网页类型 public string type; };
下面我们就要开始利用C#提供的String方法来截取http报文来实现上述结构体内参数的初始化。
int pos; //根据/r/n截断,获取http报文首部并转换为小写,方便后续处理 //Get / HTTP/1.1/r/n pos = str.IndexOf("/r/n"); string str_head = str.Substring(0, pos); str_head = str_head.ToLower(); //根据' '来截断起始行,并赋值给对应参数 string[] arr = Regex.Split(str_head, @"/s+"); HTTPServer.HTTPPrase http_head = new HTTPServer.HTTPPrase(); http_head.http_method = arr[0]; // "Get" http_head.url = arr[1]; // "/" http_head.version = arr[2]; // "HTTP/1.1" //判断是否有通过ajax要求获得或者提交的动态数据 http_head.ajax_status = str_head.IndexOf(".ajax") != -1 ? true : false; byte[] bs = http_head.ajax_status == true ? ajax_process(http_head, str) : static_process(http_head, str); return bs;
下面就可以把数据提交给后端接口,进行处理。因为动态网页处理需要网页端和后端相互的配合,工作量较大,因此本节主要阐述静态网页请求的实现。
3. 资源读取和返回
局域网Web请求一般是通过ip+port的模式直接访问服务器端,所以第一个接收到的请求的url为‘/',这时我们需要将它映射到服务端定位的访问主页,目前设置为index.html,对于其它请求,url的值一般是'/xxx/xxx.js", '/xxx/xxx.jpg"等,而在服务器中读取时我们需要定义绝对地址,所以还要在前面添加资源存储的根地址,目前将程序当前所在文件夹+html作为资源的根地址,而且操作系统存储的数据路径为/xxx/xxx.js,所以对于请求中url数据还要替换为'//'(为了保证转义符能够转变为路径符,需要用'//'表示实际的'/'),此外为了后续的http响应报文中返回正确的Content-Type字段,还有截取'.'后字段,来获取请求文件的类型。
//获得当前程序所在的文件夹 string url_str = System.AppDomain.CurrentDomain.SetupInformation.applicationBase; if (string.Compare(head.url, "/") == 0) { //对于首个请求127.0.0.1:3000/ 返回index.html url_str += "html//index.html"; head.type = "html"; } else { //其它请求 如/spring.js 替换为 .../html/spring.js便于C#查询文件路径 url_str =url_str + "html//" + head.url.Substring(1); url_str = url_str.Replace('/', '//'); int pos = url_str.IndexOf('.'); //获得当前请求的网页类型 head.type = url_str.Substring(pos + 1); }
到此为止,完成了整个http解析的过程,包括http方法, url资源地址获得并转换为windows系统路径,协议版本获得三个部分。对于静态网页请求,后续就比较简单,查询系统路径下资源,通过文件流打开,并以字符流的形式放置在内存中,作为http响应报文的正文部分。
//以文件流的方式打开指定路径内文件 using (FileStream fs = new FileStream(url_str, FileMode.Open, Fileaccess.Read)) { //StreamReader temp = new StreamReader(fs, Encoding.Default); int fslen = (int)fs.Length; byte[] fbyte = new byte[fslen]; int r = fs.Read(fbyte, 0, fslen); fs.Close(); //...... }
文件打开成功后,我们就要生成http响应报文了,http响应报文和请求报文相同,也由三部分构成。
状态码:主要为客户端提供一种理解事务处理结果的便捷方式。主要实现的有:
HTTP/1.1 200 OK 请求没有问题,实体的主体部分包含请求的资源
HTTP/1.1 400 Bad Request 通知客户端它发送了一个错误的请求
HTTP/1.1 401 Unauthorized 与适当的首部一同返回,通知客户端进行相应的认证
HTTP/1.1 404 No Found 说明服务器无法找到请求的URL
响应首部:为客户端提供额外的关于服务器的消息,本项目中实现比较简单:
Content-type:CurrentType/r/n
Server:C# Web/r/n
Content-Length:CurrentLength/r/n
Connection: close
其中Contenet-type需要根据我们上文获得的type类型来替换,这里阐述常见的替换规则。
Content-Length字段是http响应报文正文的长度,即我们获得资源的总长度(上文中fslen), 最后将状态码,响应首部和正文数据整合在一起通过socket发送到客户端,就实现了静态服务器的全部过程。
string HTTP_Current_Head = HTTPServer.HTTP_OK_Head.Replace("CurrentLength", Convert.ToString(fslen)); //根据不同url需要返回不同的首部类型 具体对比详见http://tool.oschina.net/commons switch (head.type) { case "jpg": HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "application/x-jpg"); break; case "png": HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "image/png"); break; case "html": HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "text/html"); break; case "gif": HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "image/gif"); break; case "js": HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "application/x-javascript"); break; case "asp": HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "text/asp"); break; default: HTTP_Current_Head = HTTP_Current_Head.Replace("CurrentType", "text/html"); break; } send_str = HTTPServer.HTTP_OK_Start + HTTP_Current_Head; byte[] head_byte = new byte[send_str.Length]; head_byte = Encoding.UTF8.GetBytes(send_str); //字符串流合并,生成发送文件 //之前采用的是byte[]->string, string合并, string->byte[],这种方法读取图片乱码 //因此修改为,string合并, string->byte[], byte[]合并方式,读取图片成功 byte[] send_byte = new byte[send_str.Length + fbyte.Length]; Buffer.BlockCopy(head_byte, 0, send_byte, 0, head_byte.Length); Buffer.BlockCopy(fbyte, 0, send_byte, head_byte.Length * sizeof(byte), fbyte.Length); Console.WriteLine("File Send...."); return send_byte;
4. 服务器测试和代码下载
到现在为止,一个简单的静态web服务器就实现了,将希望访问的资源文件放入当前程序文件夹/html/下, 并将首页定义为index.html, 点开服务器程序,浏览器中输入http://127.0.0.1:3000, 就可以查看返回的网页。
具体程序参考:Web服务器下载
新闻热点
疑难解答