使用Wireshark抓包,从一次链接看TCP与HTTP

TCPHeader

img

第一次请求包示例: 60738 -> 8081 [SYN] Seq=0 Win=64240 Len=0 Mss=1460 Ws=256 SACK_PERM=1

请求信息字段名详解

1. Win: TCP 窗口大小,是指TCP传输能接受的最大字节数,这个可以进行动态调节,也就是TCP的滑动窗口,通过动态调整窗口大小,来控制发送数据的速率。上图中占用2个字节,也就是16位,那么可以支持的最大数就是2^16=65536,所以默认情况下TCP头部标记能支持的最大窗口数是65536字节,也就是64KB。

2. Len: 消息长度就是指数据报文段,因为整个TCP报文=Header+packSize,所以这个消息长度就是指要传送的数据包总共长度,在本次分析中也就是HTTP报文的大小。

3. Mss: 最大报文段长度:这个就是规定最大的能传输报文的长度,为了达到最佳的传输效能,TCP协议在建立连接的时候通常要协商双方的MSS值,这个值TCP协议在实现的时候往往用MTU( 最大传输单元)值代替(以太网的1500减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以一般MSS值1460,而实际上的MSS值一般会比这个数字大。

TSO(TCP Segment Offload)的技术,该项技术是将部分的封包解包的处理工作交给了网卡,从而让CPU减少部分包处理的工作。

网卡支持TSO技术时,TCP层会逐渐的加大MSS(包大小超过MSS字节),当下层接收到这个大包时不会做分包而是直接下发给下一层,当到达网卡后网卡会将这个大的IP包拆分为N个包(小于等于MTU字节的包)发送出去,而接收端也是这样处理,这样就相当于把部分包的组装工作给网卡去做(硬件的处理速度会比软件快很多)。

4. Ws: 窗口缩放调整因子:在前面说TCP窗口大小中我们说到,默认情况下,TCP窗口大小最大只能支持64KB的缓冲数据,在今天这个高速上网时代,这个大小肯定不满足条件了,所以,为了能够支持更多的缓冲数据 RFC 1323中就规定了TCP的扩展选项,其中窗口缩放调整因子就是其中之一,这个是如何起作用的呢?首先说明,这个参数是在[SYN]同步阶段进行协商的,我们结合上面抓包数据分析下。我们看到第一次请求协商的结果是WS=256,然后再ACK阶段扩展因子生效,调整了窗口大小。

生效的抓包如下:

60738 ->8081 [ACK] Seq=1 ACK=1 Win=66560 Len=0

我们发现这个窗口变成了66560,比默认的窗口要大,我们查看报文详情:

img

我们发现,实际请求声明的窗口是260,WS扩展因子是256,最终计算的窗口大小是66560,所以我们知道了,这个扩展因子的作用就是,用原窗口大小乘以扩展因子,得到最终的窗口大小,也就是260*256=66560.

5. SACK_PERM:SACK选项,我们知道TCP传输有包的确认机制,默认情况下,接受端接受到一个包后,发送ACK确认,但是,默认只支持顺序的确认,也就是说,发送A,B,C个包,如果我收到了A,C的包,B没有收到,那么对于C,这个包我是不会确认的,需要等B这个包收到后再确认,那么TCP有超时重传机制,如果一个包很久没有确认,就会当它丢失了,进行重传,这样会造成很多多余的包重传,浪费传输空间。为了解决这个问题,SACK就提出了选择性确认机制,启用SACK后,接受端会确认所有收到的包,这样发送端就只用重传真正丢失的包了。

三次握手

TCP三次握手(建立连接过程)

角色发送 过程 报文首部 报文部分内容 状态
客户端 1次握手 SYN = 1 seq = x SYN_SENT
服务端 2次握手 SYN = 1, ACK= 1 ack = x +1, seq = y SYN_RECD
客户端 3次握手 ACK = 1 ack = y + 1,seq = x +1 ESTABLISHED

我们根据上面的流程梳理,可以知道,序号1-序号3是明显的三次握手,然后序号4进行了一次HTTP请求,接着序号5是对HTTP请求的一次接收确认,序号6是响应HTTP请求,序号7是对响应请求的确认。

第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;

第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

为什么要三次握手?

为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

四次挥手

TCP四次挥手(断开连接过程)

角色发送 过程 报文首部 报文部分内容 状态
客户端 1次挥手 FIN = 1 seq = u FIN_WAIT_1
服务端 2次挥手 ACK = 1 ack = u +1,seq = v CLOSE_WAIT
客户端 3次挥手 FIN = 1 ack = u + 1,seq = w LAST_ACK
服务端 4次挥手 ACK = 1 ack = w + 1,seq = u +1 TIME_WAIT

注意此时的TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才会进入CLOSED状态。

服务器收到客户端发送的确认,立即进入CLOSED状态。

可以从上面发现,服务端结束连接要比客户端早一些

PS:

seq:序列号 ack:确认号

SYN:请求同步标志 ACK:确认标志 FIN : 结束标志

“四次挥手”在抓包里没有出现,这是因为 HTTP/1.1 长连接特性,默认不会立即关闭连接。

正常情况下,连接断开是4次挥手的,4次挥手过程如下图:

img

我们分析这图,挥手流程是这样的:

第一次挥手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;

第二次挥手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;

第三次挥手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;

第四次挥手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。

为什么要四次挥手?

TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

上述序号8,9,10是我关闭浏览器后抓到的包,既然是关闭浏览器,我们肯定知道就是TCP连接的断开了。这里有同学应该已经发现了问题了,我们的断开是4次挥手,你这抓的包只有三条记录,是你写错了吧?我要告诉你的是,我没有写错,这是真实的抓包抓的,至于为什么是三次,我们来分析一下:

在 RFC 2581中的4.2节有提到,ack可以延迟确认,只要求保证在500ms之内保证确认包到达即可。在这样的标准下,TCP确认是有可能进行合并延迟确认的,所以,根据这一点,我们推断下面这个包:

9) 8081 -> 60738 [FIN ACK] Seq=120 Ack=397 Win=30336 Len=0

合并了对客户端的ack确认以及服务端发送的FIN断开信号包。我们点击该包详情如下: 这里红框中体现了,这个9号包是对Frame500的ACK确认,我们根据最开始的截图可以知道,这个包就是8号包

8) 60738 -> 8081 [FIN ACK] Seq=396 Ack=120 Win=66560 Len=0

img

并且9号包本身自己是发送的FIN信号包,所以,我们可以认为9号包合并了ACK和FIN的内容,所以通常的4次挥手,经过合并后变成了3次挥手。

Keep-Alive

答案是:目前的协议是不用的

在HTTP0.9版本和HTTP1.0版本中,每次请求响应都是要三次握手的, 但是HTTP1.0开始尝试持续连接,也就是Keep-Alive参数,但是官方还没有正式支持,在HTTP1.1协议中,官方默认就是支持Keep-Alive参数的,默认是持续连接。Keep-Alive的作用主要有两点:

1.检查死节点

主要是为了让连接快速失败被发现,可以进行重新连接,比如A和B两端已经建立了连接,B节点因为 异常原因挂掉了,同时A节点并不知道,这时候有两种情况:

1.假设B节点还没有恢复,那么B节点不会回复ACK,A节点就会一直重试,重试到一定次数才能知道B节点是死节点。

2.B节点在A发送数据之前重启成功了,这个时候A节点发送数据,B节点并不会接受,而是会发送一个RST信号(在一个已关闭的socket上收到数据时,将发送RST数据包,要求对端关闭异常连接且对端不需要回复ACK),然后A才知道B节点需要重连了。

以上两种情况,都会导致只有到发送数据的时候才知道对方已经出异常了。而Keep-Alive每隔一段时间就会发送心跳,就可以很快的知道服务端节点的情况。

2.防止连接由于不活跃而断开

我们知道,网络连接的建立和维持是消耗资源的,一个服务器上能建立的连接是有限的,所以像防火墙或者操作系统中会为了节省资源会释放掉不活跃的连接,而Keep-Alive每隔一段时间发送一个心跳包,就是告诉防火墙或者操作系统,我这个连接是活跃的,不要杀我。

后来重新抓了一次带有Keep-Alive的包,截图如下:

img

在上图中最后两个包就是发的Keep-Alive包,然后服务端进行ACK确认,我们看到keep-alive包,实际上是会发带有一个字节的包,这就是keep-alive的实现。

宏观上的输入网址后发生的事情

如果你用的是电脑台式机,那么你可能会使用带水晶头的双绞线连上网口,由交换机接入固定网络。如果你用的是手机、平板电脑,那么你可能会通过蜂窝网络、WiFi,由电信基站、无线热点接入移动网络。

接入网络的同时,网络运行商会给你的设备分配一个 IP 地址,这个地址可能是静态分配的,也可能是动态分配的。静态 IP 就始终不变,而动态 IP 可能你下次上网就变了。

假设你要访问的是 Apple 网站,显然你是不知道它的真实 IP 地址的,在浏览器里只能使用域名“www.apple.com”访问,那么接下来要做的必然是域名解析。这就要用 DNS 协议开始从操作系统、本地 DNS、根 DNS、顶级 DNS、权威 DNS 的层层解析,当然这中间有缓存,可能不会费太多时间就能拿到结果。

别忘了互联网上还有另外一个重要的角色 CDN,它也会在 DNS 的解析过程中“插上一脚”。DNS 解析可能会给出 CDN 服务器的 IP 地址,这样你拿到的就会是 CDN 服务器而不是目标网站的实际地址。

因为 CDN 会缓存网站的大部分资源,比如图片、CSS 样式表,所以有的 HTTP 请求就不需要再发到 Apple,CDN 就可以直接响应你的请求,把数据发给你。

由 PHP、Java 等后台服务动态生成的页面属于“动态资源”,CDN 无法缓存,只能从目标网站获取。于是你发出的 HTTP 请求就要开始在互联网上的“漫长跋涉”,经过无数的路由器、网关、代理,最后到达目的地。

目标网站的服务器对外表现的是一个 IP 地址,但为了能够扛住高并发,在内部也是一套复杂的架构。通常在入口是负载均衡设备,例如四层的 LVS 或者七层的 Nginx,在后面是许多的服务器,构成一个更强更稳定的集群。

负载均衡设备会先访问系统里的缓存服务器,通常有 memory 级缓存 Redis 和 disk 级缓存 Varnish,它们的作用与 CDN 类似,不过是工作在内部网络里,把最频繁访问的数据缓存几秒钟或几分钟,减轻后端应用服务器的压力。

如果缓存服务器里也没有,那么负载均衡设备就要把请求转发给应用服务器了。这里就是各种开发框架大显神通的地方了,例如 Java 的 Tomcat/Netty/Jetty,Python 的 Django,还有 PHP、Node.js、Golang 等等。它们又会再访问后面的 MySQL、PostgreSQL、MongoDB 等数据库服务,实现用户登录、商品查询、购物下单、扣款支付等业务操作,然后把执行的结果返回给负载均衡设备,同时也可能给缓存服务器里也放一份。

应用服务器的输出到了负载均衡设备这里,请求的处理就算是完成了,就要按照原路再走回去,还是要经过许多的路由器、网关、代理。如果这个资源允许缓存,那么经过 CDN 的时候它也会做缓存,这样下次同样的请求就不会到达源站了。

最后网站的响应数据回到了你的设备,它可能是 HTML、JSON、图片或者其他格式的数据,需要由浏览器解析处理才能显示出来,如果数据里面还有超链接,指向别的资源,那么就又要重走一遍整个流程,直到所有的资源都下载完。

评论