深入理解HTTP协议
关于网络数据模型
说到http,我们不得不提的就是网络数据模型,说到这里我们就需要了解一下两个概念,一个是网络协议,一个是网络分层。
协议:
为了使数据可以在网络上从源传递到目的地,网络上所有设备需要“讲”相同的“语言”
描述网络通信中“语言”规范的一组规则就是协议
例如:两个人交谈,必须使用相同的语言,如果你说汉语,他说阿拉伯语……
数据通信协议的定义
决定数据的格式和传输的一组规则或者一组惯例
分层:
网络通信的过程很复杂:
数据以电子信号的形式穿越介质到达正确的计算机,然后转换成最初的形式,以便接收者能够阅读
为了降低网络设计的复杂性,将协议进行了分层设计
分层设计的意义:
1) 用户服务层的模块设计可相对独立于具体的通信线路和通信硬件接口的差别
2) 而通信服务层的模块设计又可相对独立于具体用户应用要求的不同
例如:文件传输或电子邮件服务模块的设计,不必关心底层通信线路是光纤还是双绞线
OSI七层数据模型
TCP/IP是20世纪70年代中期美国国防部为ARPANET开发的网络体系结构, TCP/IP是一组用于实现网络互连的通信协议。Internet网络体系结构以TCP/IP为核心。基于TCP/IP的参考模型将协议分成四个层次,它们分别是:网络访问层、网际互连层、传输层(主机到主机)、和应用层。
各个层的协议包括如下:
我们主要记住应用层和传输层的几个协议就行了。
TCP的三次握手
HTTP协议
通过上面部分的讲解,我们明白了其实HTTP协议就是基于TCP协议的封装,因此我们完全可以通过TCP协议来模拟发送HTTP请求。本文章将使用C#来模拟。
HTTP协议格式:
有了这个格式,我们就可以按照这个格式通过TCP来模拟发送HTTP请求了,代码如下:
static void Main(string[] args) { TcpClient clientSocket = new TcpClient(); Uri URI = new Uri("http://localhost:35061/api/Ywlb/biz_check"); clientSocket.Connect(URI.Host, URI.Port); StringBuilder sb = new StringBuilder(); sb.Append("GET " + URI.PathAndQuery + " HTTP/1.1\r\n"); sb.Append("Host:" + URI.Host + "\r\n"); sb.AppendLine(); byte[] request = Encoding.UTF8.GetBytes(sb.ToString()); clientSocket.Client.Send(request); }
说明:
1.这里我们使用的GET请求,因此只需要两行数据就可以模拟HTTP请求,切记最后面的两个换行符
2.如果我们使用POST请求,则必须要加入“Content-Length”属性
上面我们使用的是TcpClient来请求,当然我们也可以通过Socket来请求,说到Socket我们有的小伙伴可能会想,这个Socket到底对应七层数据模型中的哪一层呢?其实一层都不对应,因为Socket(套接字)并不是网络编程协议,而是一个编程接口。那么我们下面用Socket来实现如上这段代码的功能
static void Main(string[] args) { Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Uri URI = new Uri("http://localhost:35061/api/Ywlb/biz_check"); clientSocket.Connect(URI.Host, URI.Port); StringBuilder sb = new StringBuilder(); sb.Append("GET " + URI.PathAndQuery + " HTTP/1.1\r\n"); sb.Append("Host:" + URI.Host + "\r\n"); sb.AppendLine(); byte[] request = Encoding.UTF8.GetBytes(sb.ToString()); clientSocket.Send(request); }
其实我们发现总体上代码差不多,只是通过Socket来实例化一个数据传输对象。
上面的代码只是模拟了请求,并没有获取数据的代码,下面提供一段获取数据的代码
static void Main(string[] args) { TcpClient clientSocket = new TcpClient(); Uri URI = new Uri("http://localhost:8009/"); clientSocket.Connect(URI.Host, URI.Port); StringBuilder sb = new StringBuilder(); sb.Append("GET " + URI.PathAndQuery + " HTTP/1.1\r\n"); sb.Append("Host:" + URI.Host + "\r\n"); sb.AppendLine(); byte[] request = Encoding.UTF8.GetBytes(sb.ToString()); clientSocket.Client.Send(request); Console.WriteLine("数据发送成功"); Thread.Sleep(4000); //接收数据 Console.WriteLine("准备接收数据"); IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0); EndPoint Remote = (EndPoint)sender; byte[] data = new byte[10000]; int len = clientSocket.Client.ReceiveFrom(data, ref Remote); Console.WriteLine("接收到的数据是:" + Encoding.UTF8.GetString(data, 0, len)); }
输出的结果如下:
这里也需要解释一下,上面的ReceiveFrom方法是阻塞方法,必须等服务器那边有返回数据才会继续后面的执行,但是上面我们定义的长度是10000,如果内容太多而超过10000就有可能接收不全,因此我们一般会在这里使用while(true)循环获取来得到数据。
TCP数据交互存在粘包和半包的问题,具体可参考:https://www.cnblogs.com/panchanggui/p/9748204.html
发生TCP粘包、拆包主要是由于下面一些原因:
1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
2.应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
3.进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包。
4.接收方法不及时读取套接字缓冲区数据,这将发生粘包。