通过WireShark可视化一次Tcp连接过程

1,845 阅读8分钟

TCP/IP的分层模型一直比较抽象,因此大学期间一直不理解TCP的三次握手中的SYN与ACK、断连以及穿插在传输过程中的各种保障TCP稳定性、可靠性的机制:诸如流量控制机制-滑动窗口、拥塞窗口;重传机制-ARQ、SACK;因此本文主要通过一次WireShark的跟包过程将这个黑盒打开,以及我们可以在哪些方面进行优化。

主要参考:《UNIX网络编程第1卷:套接口API》、张师傅的掘金小册深入理解 TCP 协议:从原理到实战《计算机网络-谢希仁》 Java中则使用Socket封装了下列的所有细节,可以和这篇文章对比阅读:深入分析Java Socket 原理之阻塞套接字

访问百度:

image.png

三次握手建立连接:

TCP是具有全双工、面向连接、字节流特性的协议,因此使用TCP通信首先要建立双向的连接,通过WireShark进行抓包,结果如下: image.png

三次握手宏观图

image.png

首先我们要理清,三次握手的目的是建立连接,建立连接是为了建立可靠的双方通信,而可靠通信时发送方需要知道现在是否可以从何处(ACK NUM与SEQ(ISN)控制)发送多长(滑动窗口、拥塞窗口与MSS等控制)的数据包到接收方,而三次握手本质上是在通信双方交换这些通信中必要的信息。

ACK与SYN是什么?

image.png TCP首部中存在一系列标志位来标识当前TCP包的类型,而ACK与SYN即表示当前TCP包首部的ACK与SYN位是否SET,通过这些标志位来标识当前TCP包的属性;

序列号与确认号及滑动窗口

每个TCP连接的序列号是一个随机初始并随已发送字节数递增的32位数(即最大为4G,超过该大小后序列号将自0重新增长)

如下图所示,当我们一个应用层的大包按照运输层的MSS(max segment size)分片为6个小包时,MSS从0(相对)开始递增,一直到5MSS,而这6个TCP包在网络中会出现如下四种状态:

image.png

上图所示中的滑动窗口(send window)大小为4MSS,这个值是在TCP首部中通过计算窗口大小*窗口因子得来,这个值是随着接收者的ACK包中的WIN动态变化的-极端网络状况下可能会变为0-Tcp Zero Window,这种情况下发送方则不会继续发送包到接收方;但是这便会带来问题,如果网络情况转好后,发送方应该如何知晓什么时候继续发送呢?是应该由发送方主动询问还是接收方给出提醒? TCP中设计了零窗口探测机制,由发送方按照指数级退避时间依次发送询问包,主要是获取接收方的网络状况-接收窗口是否允许继续发送。

为什么TCP首部中需要增加序列号字段? 如上图所示

  1. 通过序列号,我们可以保证接收方可以按照发送方通过序列号大小规定的顺序还原应用层包;
  2. 基于序列号与ACK NUM的组合,我们可以提供重传机制:

image.png

ARQ(Auto Repeat Quest)

如基于此组合的ARQ自动重传协议,当接收方传递的ACK NUM一直为MSS时,说明接收方连续接收到的最大序列号为MSS,等到包的超时时间截止时,发送方如果仍没有收到2MSS的ACK,则发送方需要重传MSS之后的包,直至接收方传递的ACK NUM变为3MSS;

从响应时间层面上:我们可以通过快速重传解决ARQ的延迟问题,当发送方接收到接收方的ACK值一直相同时,不必等待超时而直接进行重传从而降低网络延迟。

从降低网络开销层面上:我们注意到上图中其实接收方已经接收到了2MSS:3MSS-1的包,但是发送方对接收方是否接受到这个包并不知情,而这导致了发送方可能会重复发送2MSS:3MSS-1的包,因此我们需要提供一种额外的优化机制用以提供给发送者其对应接收者所接收到包的更具体的信息而不仅是连续接收到的最大序列号-SACK机制

SACK机制(Selective ACK)

在三次握手中,建连的双方通过SACK_PERM字段标识是否支持SACK,而通过之前我们通过WireShark抓到的三次握手包来看中双方是permit SACK的。

image.png

如上图所示,SACK机制在接收者发送的TCP包的首部额外提供了2MSS:3MSS的信息给发送方,因此发送者知道接收者没有收到的是介于MSS:2MSS-1的包,便不会再重复发送2MSS:3MSS-1的包了。

但是SACK机制带来的字节开销也比较大,因为SACK机制要求体现出接收窗口中不连续的包状况,如上图所示存储了3个序列号,而一个序列号为32位4字节,3个为12字节,所以需要将这些信息放入TCP首部的扩展空间中(TCP首部扩展空间最多40字节,因此存储的数量是有限制的(不可以超过4个,因为TCP扩展字段还需要额外的描述字节))

WireShark报文分析

首先由192.168.0.100:53754发送SYN包(即TCP头中SYN标志位为Set的包)请求百度的地址180.101.49.12:80,并告知百度服务器当前客户端连接序列号Seq(raw): 4151837647(wireShark处理后转为相对序列号:0)、滑动窗口大小(WIN)为64240、传输层最大传输长度(MSS)为1460(MTU(1500)-20-20)

image.png

然后百度的服务器收到消息后需要告诉客户端我已经收到了这个请求(ACK)并告知服务端当前的序列号(Seq)以及传输会用到的滑动窗口、MSS等信息;

image.png

最后客户端发送ACK至服务端:

image.png

此时的ACK报文并不占用序列号,因为占用序列号的TCP包主要是需要得到接收方的确认,而此时的ACK包并不需要得到接收方的确认,所以此时的NEXT_SEQ与当前的SEQ相同。

此时客户端当前连接的TCP状态为ESTABLISHED,已经与服务端建立了TCP连接,而服务端在收到客户端对ACK + SYN的ACK报文时,服务端的TCP状态也会由WAIT_REVE变为ESTABLISHED

HTTP请求交互

建连后发送HTTP请求报文

image.png

建联后我们通过cmd的curlwww.baidu.com发出了一个请求类型为GET、请求地址为/,HTTP协议版本为1.1的一个HTTP请求,整个HTTP的报文作为TCP的报文的payload为77字节。

PS: 我们注意到WIN值由一开始的64240变为了131584(514*256,TCP首部中的窗口大小*扩容因子),变为了上次的2.04倍,发送方报文中滑动窗口的大小表示当前发送方可以接收来自接收方的报文字节数,而在前期WIN大小以2倍扩张,这是拥塞控制的慢开始策略,避免在较差网络环境下直接进行大字节的网络交换导致网络崩溃。

ACK被置位是因为:TCP协议规定建连后,所有发送的TCP包中ACK标志位都会被置位。

接收方处理HTTP请求并返回响应报文

image.png

此处因为HTTP响应报文比较长,接收方按照MSS(1452)在运输层对TCP报文进行了分片, image.png 第一个TCP包载荷为1452,当第二个TCP包到达后,应用层将按序解析处TCP的内容并解析出HTTP响应报文:

image.png

此处我们解析出HTTP报文的主体为一个html文档-百度的页面。

回复ACK确认报文

在正常情况下,发送方收到响应报文后返回对应的ACK至接收方即可完成数据的交换:

image.png

心跳报文

image.png 百度服务器向我们本地的主机发送了一个心跳包,我们回复了一个ACK包,这样我们与本地主机的连接就不会被关闭。TCP协议在连接双方一段时间未进行通信时会进行心跳检测,此处为15s

关闭连接(四次挥手)

image.png


当本地主机一直未回复心跳包时,百度服务器在第二个心跳包未被回复后选择通过四次挥手断开连接,但是我们的本地主机并未回复ACK,百度服务器接着重发了5次后我们仍然没有收到我们的回复,之后便通过主动发送RST报文结束掉这个连接。

当然正常的四次挥手过程如下图所示:

image.png

总结:

至此我们本地主机与百度服务器的一次完整交互流程便结束了,包含了三次握手、信息交换以及心跳机制和断开连接,不过很多细节并未展开叙述。