January 26, 2019
阅读原文:http://www.saminiir.com/lets-code-tcp-ip-stack-4-tcp-data-flow-socket-api/
这篇文章中,我们将研究 TCP 数据通信及其管理方式。
除此之外,我们还会介绍可用于网络通信的网络堆栈接口,我们的示例程序将使用 socket API 向网站发送简单的 HTTP 请求。
我们先从定义数据流状态的变量开始讨论,这样更有助于对 TCP 数据管理的理解。
简而言之,TCP 必须追踪他发送出去的数据和接收到的数据,为了维护追踪的情况,TCP 协议会为每个连接创建一个传输控制块(Transaction Control Block)。
发送方的变量定义如下:
Send Sequence Variables
SND.UNA - send unacknowledged
SND.NXT - send next
SND.WND - send window
SND.UP - send urgent pointer
SND.WL1 - segment sequence number used for last window update
SND.WL2 - segment acknowledgment number used for last window update
ISS - initial send sequence number
接收方记录状态的变量定义如下:
Receive Sequence Variables
RCV.NXT - receive next
RCV.WND - receive window
RCV.UP - receive urgent pointer
IRS - initial receive sequence number
用来辅助处理当前数据段的变量定义如下:
Current Segment Variables
SEG.SEQ - segment sequence number
SEG.ACK - segment acknowledgment number
SEG.LEN - segment length
SEG.WND - segment window
SEG.UP - segment urgent pointer
SEG.PRC - segment precedence value
这些变量会一起用来控制 TCP 的数据流动状态。
连接建立完成后,就直接开始进行数据流处理了。TCB 中有 3 个变量对状态跟踪至关重要:
等到 TCP 处理完全部数据通信,并且再没有数据需要传输时,这 3 个变量的值会相等。
举个例子,比如 A 要发送数据给 B,都需要经历哪些过程呢?
变量值增加的量,等于数据段的长度。
这是 TCP 数据通信的基础。我们借助 tcpdump 这个工具来看看传输过程的实际情况。
[saminiir@localhost level-ip]$ sudo tcpdump -i any port 8000 -nt
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [S], seq 1525252, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [S.], seq 825056904, ack 1525253, win 29200, options [mss 1460], length 0
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 1, win 29200, length 0
10.0.0.4:12000 (A) 和 10.0.0.5:8000 (B) 建立了 TCP 连接。
三次握手之后,连接建立并且 TCP socket 状态更新为 ESTABLISHED。A 的初始化序列号为 1525252,B 的为 825056904。
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [P.], seq 1:18, ack 1, win 29200, length 17
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [.], ack 18, win 29200, length 0
A 发送了一个 17 bytes 大小的数据段给 B,而 B 响应了一条 ACK 消息给 A。为了可读性,tcpdump 中使用相对序列号,因此 ack 18 实际上是 1525253 + 17。
B 内部也把 RCV.NXT 增加了 17 。
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 1, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [P.], seq 1:18, ack 18, win 29200, length 17
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 18, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [P.], seq 18:156, ack 18, win 29200, length 138
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 156, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [P.], seq 156:374, ack 18, win 29200, length 218
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 374, win 29200, length 0
发送数据和确认回执在不断发生着,从上面的数据中可以看到,长度为 0 的数据段仅仅发送了 ACK 信息,但是这个数据段的序号却基于上次接收到的段长度精确的递增着。
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [F.], seq 374, ack 18, win 29200, length 0
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 375, win 29200, length 0
B 将携带 FIN 的数据段发送给 A,以告知通信结束。同时,A 也会把确认终止的消息发送回来。
为了断开连接,A 也要发送同样的信号给 B。
关闭 TCP 连接同样是个复杂的操作,可以强制终止(RST)或者相互发送 FIN 信号。
基本流程如下:
很明显,关闭一个 TCP 连接需要 4 步。而创建一个 TCP 连接 3 步就够了。
补充一点,TCP 是一个双向协议,因此可以让其中一端宣布他没有数据要发送了,但保持连接状态,以获取传入数据。这成为 TCP 连接的半关闭状态。
分组交换网络的不可靠,给终端的连接带来了更多的复杂度 —— FIN 信号可能丢失或者故意没发送,这让连接处于一个很尴尬的状态。因此,在 Linux 中,增加了内核参数 tcpfintimeout 用来控制 TCP 在强行关闭前等待最终 FIN 信号的时间。这样做虽然违反了规范,但是对于预防拒绝服务攻击来说是有好处的。
终止连接时会发送 RST 信号,有很多原因可能会导致连接终止,常见的有以下这些:
因此,发生 RST 对 TCP 来讲,是件比较悲伤的事情~~
为了能够充分利用网络协议栈,需要为应用程序提供一些接口。BSD socket API 是最著名的一个,它起源于 1983 年的 4.2BSD UNIX 版本。 Linux 中的 Socket API 与 BSD Socket API 兼容。
通过调用 socket() 并传入 socket 和 协议的类型,来申请获得网络协议栈上的 socket。socket() 有 3 个参数:
申请到 socket 后,就要开始连接远程端口了,connect() 就是用来启动三次握手建立连接的。
之后,我们就能调用 write() 和 read() 从 socket 中读取数据了。
网络协议栈会处理 TCP 流中的排队,重传,错误检查和重新排列。对于应用程序来讲,这是一个黑盒,应用程序可以完全信任 TCP 处理过后的数据流,一些意外情况也会通过 socket API 告知应用程序。
我们拿 curl 做为一个例子看看他是怎么实现的:
$ strace -esocket,connect,sendto,recvfrom,close curl -s example.com > /dev/null
...
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("93.184.216.34")}, 16) = -1 EINPROGRESS (Operation now in progress)
sendto(3, "GET / HTTP/1.1\r\nHost: example.co"..., 75, MSG_NOSIGNAL, NULL, 0) = 75
recvfrom(3, "HTTP/1.1 200 OK\r\nCache-Control: "..., 16384, 0, NULL, NULL) = 1448
close(3) = 0
+++ exited with 0 +++
我们使用 strace 观察 socket API 的调用情况,strace 是一个跟踪系统调用和信号传递的工具。步骤如下:
精明的你一定发现,这里并没有出现 read() 和 write() 的调用。这是因为 socket API 并不包含这两个操作,这两个方法属于普通的 I/O 操作,也可以在 socket 这里使用,man 一下 socket:
In addition, the standard I/O operations like write(2), writev(2), sendfile(2), read(2), and readv(2) can be used to read and write data.
socket API 提供了多个方法可以写入或者读取数据,直接用 I/O 操作也是可以的,但是会让你的程序变的更加复杂。
这一章我们基本实现了 TCP 的数据和通信管理,并提供了一个可以被应用程序使用的 socket API。
但是 TCP 数据通信不是一个简单的问题,数据包在传输过程中可能会遇到各种各样的问题。
因此 TCP 数据通信中还有一些别的更加复杂的逻辑,下一篇文章中,我们将研究 TCP 窗口管理和超时重传机制。
痕迹
没有过去,就没法认定现在的自己