http详解
http请求报文
http请求报文由请求行、请求头部、空行 和 请求包体 4 个部分组成,如下图所示:
- 请求行:请求行由方法字段、URL字段和HTTP协议版本3个部分组成,下面主要介绍一下GET和POST方法:
GET:当客户端要从服务器中读取某个资源时,使用GET方法。
POST:当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。
如果带参数时,在约定中,GET方法的参数应该放在URL中,POST方法参数应该放在请求体中。例如,如果参数是
name=Java, age = 25
,那么GET方法的报文是这样的:GET /updateInfo?name=JavA&age=25 HTTP/1.1
Host: localhost
POST方法的报文为:
POST /updateInfo HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
name=Javanx&age=25
如果不带参数,最大的区别就是二者请求行的方法名称不同而已。实际上两种方法都是基于TCP连接的,GET也可以有请求包体,POST也不一定要使用请求包体,只要客户端和服务端约定好规范即可。
- 请求头部:请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。常见的头部信息有:
- User-Agent:产生请求的浏览器类型;
- Accept:客户端可识别的响应内容类型列表;星号 “ * ” 用于按范围将类型分组,用 “ / ” 指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型;
- Accept-Language:客户端可接受的自然语言
- Accept-Encoding:客户端可接受的编码压缩格式;
- Host:请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机;
- connection:连接方式(close 或 keep-alive);
- Cookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie;
- content-length:表示包体数据长度,用来POST方法区分下一条请求的界限
- 空行:最后一个请求头部之后是一个空行,发送回车符和和换行符,通知服务端以下不再有请求头。
- 请求包体:请求包体不在GET方法中使用,而是在POST方法中使用。适用于需要客户填写表单的场合。
http响应报文
HTTP响应报文由状态行、响应头部、空行和响应包体4个部分组成,如下图所示:
下面对响应报文格式进行简单的分析:
- 状态行:由HTTP协议版本,状态码和状态码的描述文本3个部分组成
- 状态码由三位数字组成, 常用的有以下:
1xx:表示服务器已接收了客户端请求,客户端可继续发送请求;
2xx:表示服务器已成功接收到请求并进行处理;
3xx:表示服务器要求客户端重定向;
4xx:表示客户端的请求有非法内容;
5xx:表示服务器未能正常处理客户端的请求而出现意外错误; - 响应头部:
- Location:Location响应报头域用于重定向接受者到一个新的位置。例如:客户端所请求的页面已不存在原先的位置,为了让客户端重定向到这个页面新的位置,服务器端可以发回Location响应报头后使用重定向语句,让客户端去访问新的域名所对应的服务器上的资源;
- Connection:连接方式
对于请求来说:close(告诉 WEB 服务器或者代理服务器,在完成本次请求的响应后,断开连接,不等待本次连接的后续请求了)。keepalive(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求);
对于响应来说:close(连接已经关闭); keepalive(连接保持着,在等待本次连接的后续请求); Keep-Alive:如果浏览器请求保持连接,则该头部表明希望WEB 服务器保持连接多长时间(秒);例如:Keep-Alive:300;
- 空行:最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。
- 响应包体:服务器返回给客户端的文本信息
一些问题
- GET方法的长度限制:首先说明一点,HTTP 协议没有 Body 和 URL 的长度限制,对 URL 限制的大多是浏览器和服务器的原因。原因是指:解析URL的时候要分配内存,即分配一个buffer来保存所有要储存的数据,处理长 URL 要消耗比较多的资源,为了性能和安全(防止恶意构造长 URL 来攻击)考虑,会给 URL 长度加限制。
- POST 方法会产生两个 TCP 数据包?
有些文章中提到,post 会将 header 和 body 分开发送,先发送 header,服务端返回 100 状态码再发送 body。HTTP 协议中没有明确说明 POST 会产生两个 TCP 数据包,而且实际测试(Chrome)发现,header 和 body 不会分开发送。所以,header 和 body 分开发送是部分浏览器或框架的请求方法,不属于 post 必然行为。 - HTTP请求和响应的步骤,例如在浏览器地址栏键入URL,按下回车以后会经历以下流程:
- 浏览器向DNS服务器请求解析该URL中的域名对应的IP地址
- 解析出IP地址后,根据该IP地址和默认端口80,和服务器建立TCP连接
- 浏览器发出读取文件(URL中域名后面部分对应的文件)的HTTP请求,该请求报文作为TCP第三次握手的报文数据发送给服务器
- 服务器对浏览器请求做出响应,并把对应的html文本发送给浏览器
- 释放TCP连接:若connection 模式为close,则服务器主动关闭TCP 连接,客户端被动关闭连接,释放TCP 连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求
- 浏览器将html文本显示内容
- 服务器如何解析POST包体数据,即如何辨别包体结束?
一般有两种方式:第一种方式就是在包头中有个content-Length字段,这个字段的值的大小标识了POST数据的长度,服务器收到一个数据包后,先从包头解析出这个字段的值,再根据这个值去读取相应长度的作为http协议的包体数据。还有一个格式叫做http chunked技术(分块),大致意思是将大包分成小包 - 如果某个客户端通过程序模拟了一个连接请求,但是迟迟不发含有\r\n\r\n的数据,这路连接将一直占用,这里我们可以设置一个最大URL长度,如果超过这个长度还不包含\r\n\r\n的话我们认为连接非法,将连接断开。并且如果一个客户端连接后不发送任何数据,这样会浪费大量的连接资源,所以还需要一个定时器去定时检测哪些http连接超过这个最大时间没发送数据,找到后将连接断开。
DNS解析流程
1.首先看浏览器缓存,若没有则看操作系统缓存,如果没有则去看host文件,如果没有则开始进行对服务器的询问
2.首先客户端询问本地DNS服务器,自己要查找的url的IP地址,如果本地服务器能找到缓存则返回,否则本地服务器会去询问根服务器,根服务器返回给它一个顶级域名服务器的地址;本地服务器会再去询问顶级域名服务器,后者会给它一个权威域名服务器地址,本地服务器再去询问,直到查询到自己要找的url的IP地址,并将其返回给客户端,客户端和目标建立连接
路由表转发规则
如果有多个网卡,即有多个源IP地址,我们需要判断使用哪个来发送IP数据报
1.根据目的IP地址,和本地路由表中的多个条目的子网掩码进行与运算,得到的结果和路由表中的目的IP地址进行比对,正确的就是我们要选择的网卡,
2。如果没有匹配的网卡,那么选择默认网关,其目标IP地址和子网掩码都是0.0.0.0
3.后续将数据包发送给路由器,路由表中的Gateway就表示路由器的IP地址,即源IP地址
如何优化HTTP/1.1
- 避免发送HTTP请求:缓存技术(强制缓存和协商缓存)
- 在需要发送 HTTP 请求时,考虑如何减少请求次数
- 减少重定向请求次数:如果有代理服务器,那么重定向的过程可以由它来进行,减少HTTP请求次数(301和308表示客户端可以将重定向响应缓存到本地磁盘,之后自动用新的url来替换之前的url来访问服务器)
- 合并请求(对于多个小的资源的请求,合并成一个总的资源请求)
- 延迟发送请求:请求网页的时候,没必要把全部资源都获取到,而是只获取当前用户所看到的页面资源,当用户向下滑动页面的时候,再向服务器获取接下来的资源,这样就达到了延迟发送请求的效果。
- 减少 HTTP 响应的数据大小
- 无损压缩:br,gzip算法:将常出现的数据用较短的二进制比特序列表示,将不常出现的数据用较长的二进制比特序列表示
- 有损压缩:比如对图
TCP为什么是三次握手
1.阻止重复历史连接的初始化(主要原因)
如果是两步,那么服务端接收到之后就会建立连接,并向客户端发送数据,如果当前建立的连接并不是客户端需要的,那么就白白浪费了这次历史连接的建立,增加了开销。
2.同步双方的初始序列号:如果只有两次,那么服务端的序列号无法被确认,四次的话又可以压缩,所以是三次。
3.避免资源浪费:如果初始SYN发生了网络阻塞,客户端进行了超时重传,那么服务端由于不知道自己发送的SYN+ACK是否被接收到,只能再次建立连接。
为什么每次建立 TCP 连接时,初始化的序列号都要求不一样
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面)
客户端和服务端建立一个 TCP 连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文。紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接;在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。 - 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收
初始序列号的产生:M + F
M是一个计时器,每隔4微秒 + 1,F是根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。
既然 IP 层会分片,为什么 TCP 层还需要 MSS
如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。
经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率
为什么需要 TIME_WAIT状态
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
如果某一个网络包被延迟,并且在延迟过程中客户端和服务端连接断开,然后如果TIME_WAIT状态没有或者过短,那么如果客户端和服务端以相同的四元组重新建立连接,那么该被延迟的数据是有可能被窗口接受的,而TIME_WAIT持续2MSL时间,就是为了让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。 - 保证「被动关闭连接」的一方,能被正确的关闭;
- 如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文。
假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文。
服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。
为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK,如果服务端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。
客户端在收到服务端重传的 FIN 报文时,TIME_WAIT 状态的等待时间,会重置回 2MSL。
为什么需要CLOSE_WAIT状态
因为虽然此时服务端收到了客户端断开连接的请求,此时需要通知进程调用close和shutdown函数来断开连接,而此时可能应用层还有别的任务需要处理,所以需要一个中间状态,然后等调用close或者shutdown函数后,服务端才会变为last_ack状态
重传机制
1.超时重传:数据包丢失和确认应答丢失
其机制是每一次重传时间增加一倍
2.快速重传:数据驱动,三次同样的ACK就会触发,但是并不知道所需要的ACK后面的需不需要重传
3.SACK:选择性确认,三次同样的ACK触发,TCP头部保存已经收到的数据段信息,只选择没有接收到的数据段重新发送。
4.D-SACK:告知发送方有哪些数据被重复接收了(例如ACK丢失和网络延时),会同时包括ACK和SACK两种,其中ACK表示需要的下一个包序列,SACK表示重复接收到的包,不需要再进行发送
滑动窗口
1.概念:无需等待确认应答,而可以继续发送数据的最大值。窗口大小一般由接收方告诉发送方
2.发送窗口中分为两部分,一部分是已经发送但是还没有收到ACK的窗口大小,一部分是还没有发送的可用的发送部分,每当接收到一些ACK之后,窗口就可以向右滑动来增加可用部分
3.接收窗口表示未接收到但可以接收的数据,每次接收到数据后,窗口就可以向右滑动
涉及到的部分是流量控制:发送窗口和接收窗口所存放的数据都是放在系统内存缓冲区的,会在数据传输的过程中不断被系统调整
全连接队列满了之后只能丢弃连接吗
丢弃连接是Linux内核的默认行为,我们可以通过设置参数tcp_abort_on_overflow的值来修改他的行为
- 值为0,则表示如果队列满了,则丢弃客户端发送的最后一次ack
- 值为1,则服务端会发送一个RST给客户端,表示废弃掉这个握手过程和这个连接
通常情况下,应该将该值设置为0,来应对突发的流量,因为可能过一段时间后,全连接队列就能空出位置来接收客户端重新发送的ack,二者还能够成功建立连接
为什么需要四次挥手
因为服务端收到客户端的FIN报文时,马上会回复一个ACK报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送FIN报文,而是将该权限交给应用程序
- 如果应用程序还要发送数据,那么发送完数据后才调用关闭连接的函数
- 如果应用程序不再发送数据,那么直接就可以调用关闭连接的函数
三次挥手
如果服务端没有数据要发送而且开启了TCP延迟确认的情况,那么第二次挥手和第三次挥手就会合并成一次。
TCP延迟确认的规则:
- 如果有响应数据要发送,那么响应数据和ACK会一起发送
- 如果没有响应数据要发送,那么会等待一段时间看是否有响应数据需要发送
- 如果在等待期间又接收到了数据,那么会立即发送ACK
没有accept也可以建立连接
accept最终返回是从全连接队列中取出一个元素,在前面的过程中实际上已经构建了连接
全连接队列和半连接队列是在服务点listen阶段内核创建的,其中全连接队列是链表,半连接队列是哈希表
没有listen的情况下也可以建立连接,比如两个客户端产生连接或者客户端和自己建立连接,这是因为内核有一个全局的hash表,可以用来存放客户端的连接信息,当消息发出后,经过回环地址重新回来以后,从hash表中取出连接信息,建立连接。
TCP的异常断开连接情况
1.如果进程崩溃,那么内核会发送FIN,和另一端进行四次挥手断开连接
2.如果客户端宕机且没有重启,不会发生四次挥手:
- 如果服务端发送数据,那么会超时重传,直到达到最大重传次数断开连接
- 如果服务端不发送数据,如果服务端开启了keepalive,那么会去探测对方是否还在,如果探测不到,就会断开连接,如果没有开启保活机制,那么会一直保持在连接状态
3.如果客户端宕机且迅速重启:此时如果服务端向客户端发送数据,都会收到它发送的RST断开连接的报文
拔掉网线的问题
实际上拔掉网线不会影响TCP四元组的信息,其连接状态还是存在
1.如果有数据传输
- 在数据传输的重传次数到达最大之前插入网线,还是能保持连接
- 达到最大重传次数前没有插入网线,则断开连接
2.如果没有数据传输 - 如果二者都没有keepalive,那么二者都默认是连接状态
- 如果开启了保活机制,那么在探测期间内插入了网线,还是保持连接,如果探测失败,则断开连接