上一篇博客中介绍了怎样使用socket访问web服务器。关键有两个:
- 熟悉Socket编程;
- 熟悉HTTP协议。
上一篇主要是通过socket来模拟浏览器向(任何)Web服务器发送(HTTP)请求,重点在浏览器端。本篇博客则反过来讲一下怎样使用socket来实现Web服务器,怎样去接收、分析、处理最后回复来自浏览器的HTTP请求。
HTTP协议是浏览器和Web服务器都需要遵守的一种通信规范,如果我们编写一个程序,正确遵守了HTTP协议,那么理论上讲,这个程序可以具备浏览器、甚至Web服务器的功能。
图1
如上图1所示,Web服务器和浏览器之间无论是发送数据还是接收(解析)数据均遵守了HTTP协议。可以很确定地讲,只要我们充分熟悉HTTP协议结构,那么无论浏览器的实现还是Web服务器的实现,均只是“简单的”Socket程序的开发过程,除此之外,无其它神秘高深的东西。而Socket程序开发,稍微知道一点socket的有关知识,均能写得出一个大概demo。
从系统架构来讲,Web架构形式的系统均符合“生产者-消费者”模式(实质上,现实生活中大部分系统均属于该模式)。浏览器端不断产生数据(请求),而Web服务器端不断处理请求,长时间持续如此。
图2
如上图2所示,图中左边部分为Web服务器中的“泵”结构,所谓泵,就是指它能够持续长时间循环运作。图中右边显示“来自浏览器请求”部分即为“生产者”,生产者不断发出请求,由左边(Web服务器)不断进行处理,最后回复给浏览器。注意图2中显示,Web服务器中处理数据在循环体内部,换句话说,前一次HTTP请求处理结束之前,后一次HTTP请求不能开始,也就是每次请求处理均会阻塞循环的执行。这种串行处理数据的方式明显效率不高,为了解决该问题,我们可以在接收到浏览器端的HTTP请求后,并不马上在当前线程中进行处理,而是开辟独立线程来处理请求(在.NET中可以使用异步编程实现)。这样一来,请求处理并不会阻塞当前循环过程,见下图3
图3
如上图3所示,接收到请求后,开辟其它线程来处理,这种并行处理数据的方式不会影响后续请求处理。
如果对Socket编程比较熟悉,以上所说的完全可以轻松实现(完全按照Socket编程去做)。现在难点是,Web服务器端怎样解析来自浏览器的请求数据(一串字符串文本),以及应该以怎样的格式去回复浏览器?答案就是必须充分了解HTTP协议格式。上一篇博客中已经提到过,有关HTTP协议格式请参见http://www.cnblogs.com/riky/archive/2007/04/09/705848.html。我们必须读懂浏览器发送的请求数据,并按照正确格式发送回复。下图4显示浏览器请求数据格式:
图4
图中红色部分即为数据传输方式(post或get)、请求路径(url中不含主机地址部分)以及HTTP协议版本号。下面以“键:值”格式的文本均为浏览器发送给服务器的一系列数据信息(注意这些项可选),如果浏览器以post方式提交数据,那么数据会紧跟在下面(图中没显示)。Web服务器读懂浏览器发送的请求数据,并处理完毕后,必须按照图5的格式将结果回复给浏览器:
图5
如上图5所示,最上面的以“键:值”的格式文本是Web服务器发送给浏览器的一些数据信息(这些项部分可选),紧接着,下面便是需要发送给浏览器的HTML文档(如果返回的是页面)。浏览器必须读懂Web服务器发送的回复数据,然后进行渲染(显示)。
图6
图6显示了浏览器发起的一次HTTP请求,显示展示了Web服务器端处理该请求的过程。我们可以看到,Web服务器在一次Socket连接过程中只处理一个HTTP请求。多次HTTP请求会伴随着Socket不断的连接与断开。
文章最后上传一个使用Socket编写的简单Web服务器,能够实现以下功能:
- 运行Web服务器后,可以绑定端口,接收来自任何浏览器的HTTP请求;
- 能够显示一个默认首页,如index.html;
- 首页提供“登录”功能,按照Post方式传递数据到处理页面“login.zsp”(后缀名可自定义);
- Web服务器端接收接收浏览器发送的数据,能够解析(解析方式很随意)出post传递的参数,并模拟访问数据库检查登录情况、模拟耗时等待等;
- Web服务器生成登录成功后的静态页,回复给浏览器。页面显示登录名和当前时间。
整个demo完全就是一个Socket程序,只是增加了“HTTP协议”的环节,服务器端无论是接收(解析)数据还是发送数据,均需要遵守HTTP协议。Web服务器中最终的请求处理泵代码如下:
static Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //侦听socket
static void Main(string[] args)
{
_socket.Bind(new IPEndPoint(IPAddress.Any, ));
_socket.Listen();
_socket.BeginAccept(new AsyncCallback(OnAccept), _socket); //开始接收来自浏览器的http请求(其实是socket连接请求)
Console.Read();
}
static void OnAccept(IAsyncResult ar)
{
try
{
Socket socket = ar.AsyncState as Socket;
Socket new_client = socket.EndAccept(ar); //接收到来自浏览器的代理socket
//NO.1 并行处理http请求
socket.BeginAccept(new AsyncCallback(OnAccept), socket); //开始下一次http请求接收 (此行代码放在NO.2处时,就是串行处理http请求,前一次处理过程会阻塞下一次请求处理) byte[] recv_buffer = new byte[ * ];
int real_recv = new_client.Receive(recv_buffer); //接收浏览器的请求数据
string recv_request = Encoding.UTF8.GetString(recv_buffer, , real_recv);
Console.WriteLine(recv_request); //将请求显示到界面 Resolve(recv_request,new_client); //解析、路由、处理 //NO.2 串行处理http请求
}
catch
{ }
}
注意以上代码中的NO.1和NO.2处,socket.BeginAccept()方法放在NO.1处时,服务器端会并行处理请求,而放在NO.2处时,服务器会串行处理请求。读者可以每种方式都试一下,在串行处理请求时,请求处理过程会阻塞后续请求的处理(比如登录耗时10秒钟,其它人无法访问网站)。
以下是demo效果图:
图7:Web服务器运行后,浏览器访问首页:
图7
图8:浏览器中首页显示(包含登录框):
图8
图9:用户点击“登录”按钮,以Post方式提交数据,Web服务器解析、处理,返回新页面:
图9
文章有点长,部分截图还失真了(部分图以前整理的,没有找到大图,所以就凑合看:))
源码下载:http://files.cnblogs.com/xiaozhi_5638/socket_webServer.rar