用TCP/IP实现自己简单的应用程序协议:最后再返回来看HTTP协议
之前我们实现了一个自己的应用层的协议,功能非常简单,只包括了最基本的成帧和解析功能。不过有了这些基础,我们再返回来看看现在在互联网上最通行的http协议,就会容易懂得许多。
http具体是做什么的,网上面讲解很多,比如:
我们知道,Internet的基本协议是TCP/IP协议,然而在TCP/IP模型最上层的是应用层(Application layer),它包含所有高层的协议。高层协议有:文件传输协议FTP、电子邮件传输协议SMTP、网络新闻传输协议NNTP和HTTP协议等。
HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传输协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机 正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。这就是你为什么在浏览器中看到的网页地址都是以 http://开头的原因。
当决定使用超文本作为WWW文档的标准格式后,于是在1990年,科学家们立即制定了能够快速查找这些超文本文档的协议,即 HTTP协议。经过几年的使用与发展,得到不断的完善和扩展,目前在WWW中使用的是HTTP/1.1,持久连接被默认采用。支持以管道方式同时发送多个请求,以便降低线路负载,提高传输速度。在代理服务器上1.0版本较多。 简而言之,Http是一种基于Request/Response模式的,无状态的协议。理解这两个关键词是重中之重,下面会有详细解释。
HTTP报文由从客户机到服务器的请求和从服务器到客户机的响应构成。请求报文格式如下:
请求行 - 通用信息头 - 请求头 - 实体头 - 报文主体
请求行以方法字段开始,后面分别是 URL 字段和 HTTP 协议版本字段,并以 CRLF 结尾。SP 是分隔符。除了在最后的 CRLF 序列中 CF 和 LF 是必需的之外,其他都可以不要。
应答报文格式如下:
状态行 - 通用信息头 - 响应头 - 实体头 - 报文主体
状态码元由3位数字组成,表示请求是否被理解或被满足。原因分析是对原文的状态码作简短的描述,状态码用来支持自动操作,而原因分析用来供用户使用。客户机无需用来检查或显示语法。
我们还是来自己动手写个最基本例子,用来说明http协议。
注意我们除了正文之外,还发送了head,分隔行,和状态信息。还记得我们在自己实现得协议里的消息长度吧。其实这些就是为了处理数据和告诉应该的服务器响应动作而提供的元数据。
publicclass SimpleSocketListener { publicvoid Run() { // 取得本机的 loopback 网络地址,即 127.0.0.1 IPAddress address = IPAddress.Loopback; IPEndPoint endPoint =new IPEndPoint(address, 9527);
// 创建一个 socket,传输协议为TCP Socket socket =new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 将 socket 绑定到一个端点上 socket.Bind(endPoint); socket.Listen(10); Console.WriteLine("开始监听, 端口号:{0}.", endPoint.Port); while (true) { Socket client = socket.Accept(); Console.WriteLine(client.RemoteEndPoint); byte[] buffer =newbyte[1028]; int length = client.Receive(buffer, 1028, SocketFlags.None); System.Text.Encoding utf8 = System.Text.Encoding.UTF8; string requestString = utf8.GetString(buffer, 0, length); Console.WriteLine(requestString); // 状态行string statusLine ="HTTP/1.1 200 OK\r\n"; byte[] statusLineBytes = utf8.GetBytes(statusLine); // 准备发送到客户端的相应内容string responseBody =@"<html> <body><h1>Content from server!</h1></body> </html>"; byte[] responseBodyBytes = utf8.GetBytes(responseBody); // 回应的头部string responseHeader =string.Format( "Content-Type: text/html; charset=UTF-8\r\nContent-Length: {0}\r\n", responseBody.Length //看到长度是否很熟悉? ); byte[] responseHeaderBytes = utf8.GetBytes(responseHeader);
// 向客户端发送状态信息 client.Send(statusLineBytes); // 向客户端发送回应头 client.Send(responseHeaderBytes); // 头部与内容的分隔行 client.Send(newbyte[] { 13, 10 }); // 向客户端发送内容部分 client.Send(responseBodyBytes);
// 断开与客户端的连接 client.Close(); if (Console.KeyAvailable) break; } // 关闭服务器 socket.Close(); } }
其他剩下的工作浏览器都帮我们做好了,在浏览器中输入地址,发送请求
有人也许会问,Http不是基于TCP/IP的吗?而这个是可以保持状态的。怎么Http就是无状态了的呢?搞清楚这个问题,对以后我们WCF中选择协议也有帮助。
Http是属于最高层的应用协议,基于TCP/IP,也就是说它在TCP/IP的基础上引入了新的概念和规定。因此,无状态是Http规定的,是为了适应Web的要求而规定的。Web应用经常面对大量的访问,如果都保持TCP的连接状态那么将会消耗大量的资源。就会演变成了类似“客户端/服务端”一对一模型。因此,Http规定了它是无状态的,也就是说,处理完一个请求并返回以后,服务器端就要直接关闭掉TCP连接,不管相同的客户端是否再次发送请求。以这样的形式来实现单向的Request/response模式的。
上面我说的是Http 1.0 的规定。用图来表示就是:
注意,服务端关闭连接这个动作不可少。这样才是无状态。也就是说HTTP 1.0的协议的无状态是由这4个步骤组成,缺一不可。一个连接请求就在一次处理后关闭。
看到这里如果对浏览器有些了解的人就会知道,其实这个模型并不高效。在当初网页主要是HTML文本的时候还可以,但是现在一个的网页里都包含了非常多的诸如img,javascript,css等元素,这使得浏览器在解析由服务器发回的HTML网页以后,如果解析HTML时发现了上面这些元素的标签,那么还要多次的发起连接请求。而每次建立连接请求都需要三次握手,比较耗资源。因此,HTTP 1.0对于现在的网页来说并不很适合。 浏览器生成一个完整的页面,一共发送了N+1条请求。1就是指返回的HTML网页字符串,N是其中包含的资源,由浏览器解析HTML标签后发出。
如图:
这个问题应该怎么解决呢?要是能够在一条已建立的连接上,多处理几次资源请求就好了。也就是连接能够复用,更直接了当的说就是当服务器处理完一条资源请求时,不要像1.0那样马上关闭连接,而是等一段时间。
这个等待的动作,就是Keep alive(看上面黑色的那张截图最下面一行),在HTTP 1.1中默认被采用。
因此,这时服务器对于关闭TCP连接就会采用两种方式,一种是当收到浏览器发送的关闭连接请求后关闭该TCP请求(意味着浏览器已经把该网页所需的请求资源已经拿完了,无需再从这条连接里获取其他资源),另一种就是当经过一段时间后,就算浏览器没有发送关闭连接的请求,服务端也会因为超时而自动关闭该条TCP连接。
因此我们知道为什么我们的HTTP请求中还包含了HTTP的版本信息,因为这会影响到双发对于关闭请求的理解。
当然HTTP 1.1除了这条改进之外,还增加了诸如PUT,DELETE等动作。在接下来讲REST的文章的时候会讲到。