TCP 就在我们身边,他的每一个概念都会在我们的手机上、电脑上重复执行几万、甚至几百万次每天,但我们阅读那些概念的时候,比如 SYN,三次握手,还是感觉那么遥远。
这个时候,不如抓个包,让整个通信的过程真实的铺在自己的屏幕上,而不是一个文档中,或者一本书里。
google 一下 TCP 抓包工具,很多推荐都指向 wireshark。我们就利用 wireshark 抓一个完整流程。
我利用 http 客户端工具往 http://100.66.225.13:4568/ping
发送了一个 http 请求:
GET /ping HTTP/1.1
Host: 100.66.225.13:4568
Connection: close
User-Agent: Paw/3.1.7 (Macintosh; OS X/10.13.6) GCDHTTPRequest
得到的回复是:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Wed, 17 Oct 2018 14:33:29 GMT
Content-Length: 18
Connection: close
{"message":"pong"}
从 http 这个应用层看来,通讯就是一去,一回(一个 request 和一个 response),但在通讯层看来,可不是这样的:
Wireshark 抓到的 TCP 包我利用 wireshark,对这个 http 通讯抓了一下包,得到的结果是 11 个包。这 11 个包都是什么呢?
握手流程
这11个包,每个在细节上都不一样。这些细节上的不同,是为了实现他们通讯功能上的不同。我们先从整体入手,看看这 11 个包在功能上的区别。
先说前三个包,他们构成了著名的「三次握手」:
- 第一个包是 client -> server,客户端要求服务器回答一下我们可不可以通讯
- 第二个包是 server -> client,服务器说我可以通讯,并问客户端可不可以
- 第三个包是 client -> server,客户端说我依然可以
但握手不是最终目的,如果不是为了交换数据,我们也没必要让服务器握手。他们在握手的过程中,除了确认了对方的存在,为通讯做了第一层保障,还为接下来交换数据做了准备,包括可靠性方面的,交换过程方面的。这些设计是为了让 TCP 的传输能够可靠,同时速度也可以提高(至少得比什么都不做高一点)。带上这个设计,我们再来看「三次握手」:
- client -> server:服务器在不在?在的话记住几件事——我向你(客户端向服务器)通讯的起始序列号是 xxx,我当前的窗口大小是 65535 bytes (仅为举例),我建议的最大消息长度是 1040 bytes(仅为举例)
- server -> client:服务器说我在!收到你的 xxx 包。并询问客户端在不在,在的话记住我向你(服务器向客户端)通讯的起始序列号是 yyy,我当前的窗口大小是 2000 bytes,建议最大消息长度 532 bytes (仅为举例)
- client -> server:客户端说我还在,收到你的 yyy 包,接下来可以用商量好的最大消息长度(取两者中较小的那个[1])开始通讯了。
我们上面的描述是非常拟人化的描述,但在实际编程的过程中,处理的是具体的二进制编码,比如:
TCP 首部- 如何知道服务器收到的这个包是在询问服务器在不在(请求握手):是把 TCP 首部中第 111 个 bit(SYN 包的标志位)设为 1,这个位可以被处理 TCP 请求的程序读取,并理解为请求握手。
- 描述中提到的序列号其实叫 Sequence Number(Seq),客户端和服务器生成一个随机数作为自己的序列号,窗口大小叫 Window Size,最大消息大小是 Maximum Message Segment(MSS),他们都可以在 TCP 的首部中找到,wireshark 也可以帮我们从 TCP 首部的二进制编码中解析出来:
通过拟人的描述,我们了解了握手的基本过程,为了更具体一点,我们画一个具体的过程图。但事先说明:
- 我们说的请求建立连接的包叫 SYN 包,SYN 是 synchronize(同步),意思是在说:我们来同步一下序列号吧。
- 确认应答包叫 ACK 包,ACK 是 acknowledgement,也是通过 TCP 首部的一个标志位标记的。
握手过程中的为什么
以上就是简单握手的一个基本通讯过程,但这其中有很多为什么值得讨论。
首先说,为什么要加上序列号?
有了这个序列号,所有通过此链接传输的 TCP 包就都有了身份,如果有一个包没有收到,可以找回;如果因此数据包再不怕丢失,那么进而可以实现一些新特性,比如滑动窗口(这个我们后面说)。
我们在图中画的 xxx 和 yyy,分别是客户端的包和服务器端端包的其实序列号,两端有不一样的序列号;而序列号都是随机数[2],所以你抓包看到的值和我基本上不会一样。我利用 wireshark 抓到的包中,第一个 SYN 包的序列号是二进制数 01000010011111110101110101100010
(十六进制的 427f5d62
):
(提示,wireshark 会显示 Sequence number: 0 (relative sequence number)
,但实际是 Seq 并不是 0,只是软件帮我们取了相对于起始序列号的相对值)
握手完成之后,序列号的真正作用开始更明显的展示出来:
在真正交换数据的过程中,ACK 包会指向下一个想要接收的 bytes 的序列号,这个序列号是通过接收到的 TCP 包的 Seq number 加上这个包的 TCP payload 的长度,算出来的。但如果有一个包迟迟为收到,比如序列号为 xxx + 1000 的包(代表从 TCP 要传输的第 1000 个字节开始的包),那么即使 TCP 利用滑动窗口,已经把下面的包发过来了(比如 xxx + 2000,xxx + 3000,xxx + 4000 的包),那么确认应答的 ACK 包,还是会把 ACK number 设为 xxx + 1000,也就是说,接收方会一直重复:请给我 xxx + 1000 那个包。如此,如果在一定时间范围内发送方收到了 3 个 xxx + 1000 的 ACK number,他会真的重发一遍 xxx + 1000[3]。如果 xxx + 1000 的 ACK 没有收到,但是 xxx + 2000 的 ACK 已经收到了,发送方可以间接确认 xxx + 1000 已经收到了,因此可以不用重发[4]
这就是 Seq number 的作用,可以帮忙确认那些包丢了,也可以帮忙确认一些消息的到达。
接下来,我们探究一下,TCP 是如何选择要发出的包的。
就像我们上面说的,xxx + 1000 包的确认消息可能还没有被发送方收到,发送方可能已经把假下来的三个包发出了(比如,xxx + 2000,xxx + 3000,xxx + 4000)。为什么要这样呢?
因为,不论 xxx + 1000 这个包是否真的失败了,还是正在路上,或是接收方的 ACK 丢了,发送方如果只是等待一段时间之后重发,那么有很多时间就被浪费掉了。在这段时间里明明可以发送更多的包(如果接收方表示可以接收的话),能成功几个算几个,总比什么都不做更好。而且,就像上面说的后面包如果提前返回了 ACK 应答,也可以帮忙判断 xxx + 1000 是不是真的丢了。
这种同时发出多个包的机制叫做窗口滑动机制(Sliding window acknowledgement system)[5]。
但,这种机制是如何决定发多少个包,发哪些包的呢?
首先 TCP 是知道自己总共要发出多少个 bytes 的,当他接受到接受方返回的 ACK 包时,可以通过其中的 window size 字段知道接收方已经准备好了接受多少个 bytes,比如 3000 个 bytes。TCP 就可以从自己发送到数据中,取出 3000 个bytes,封装成包。但这 3000 个 bytes 可能不是一个包,因为每个包的 payload 大小不超过三次握手时商量 MSS (最大消息长度),所以 TCP 程序要把 3000 个 bytes 分装成多个包,然后就可以将这些包发出了。
顺便说一句, MSS 的理想状态是,数据包不用在 IP 层继续拆分[6],但具体如何确定这个值,我还太清楚。
发送方收到的下一个 ACK 包,会有一个新的 window size,这代表它又可以发送新的包了。如此循环,指导把所有的 bytes 发完。
最后,我们再来问一个问题:为什么是三次握手?
从确认对方存在的角度说:
- 两次握手无法让服务器指导客户端的存在,因为别人可以伪造客户端 IP 地址。如果没有收到第三次握手的 ACK,服务器就开始长时间等待客户端的消息,那会消耗很多资源。SYN 攻击(可自行google)已经可以消耗资源了,如果不去确认第三次握手,资源消耗会更多。
- 但如果是四次握手,客户端和服务器在第三次的时候就已经确认了对方的存在,所以没必要。
从序列号的角度说:
- 服务器也需要客户端同步知道自己的序列号,如果没有第三次握手的 ACK,无法保证这个序列号真的同步给了客户端,接下来如果服务器向客户端发数据,比如发 http 的 response,那么客户端可能不知道这个 response 数据的头是哪里。
- 同理,如果有第四次握手,没有需要序列号不需要同步了。
发送方、接收方状态
我们上面介绍了 TCP 三次握手的基本流程。作为发送方或接收方的程序,他们的内部状态也随着这个握手的过程而发生改变:
TCP handshake with states程序的状态确定了接下来可能会发生的事件(events)的种类,以及可能会执行到哪些动作(actions)[7]
后话
断开通讯的过程也有一些细节需要梳理,也有一些为什么值得讨论,我们接下来再说。
网友评论