一、TCP段格式:
TCP的段格式如下图所示
源端口号与目的端口号
源端口号和目的端口号,加上IP首部的源IP地址和目的IP地址唯一确定一个TCP连接。
序列号
序号表示在这个报文段中的第一个数据字节序号。
确认号
仅当ACK标志为1时有效。确认号表示期望收到的下一个字节的序号。
头部长度
4位,TCP头部最多60个字节,最少20个字节
保留位
6位,必须为0
6个标志位
URG-紧急指针有效
ACK-确认序号有效
PSH-接收方应尽快将这个报文段交给应用层
RST-连接重置
SYN-同步序号用来发起一个连接
FIN-表示将要终止一个连接
窗口大小
通过窗口大小来达到流量控制。
校验和
对tcp表头与数据进行校验。
紧急指针
是一个正的偏移量,与序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。实际上,紧急数据跟带外数据不是一回事,tcp并没有另外建立一条逻辑连接传输数据,只是socket api 中把紧急数据叫做带外数据而已。
The confusion between TCP's urgent mode and out-of-band data is also because the predominant programming interface, the sockets API, maps TCP's urgent mode into what sockets calls out-of-band data.
C++ Code
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//开启fd能够接收带外数据的功能 void active_oobinline(int fd) { int ret; int oobinline = 1; ret = setsockopt(fd, SOL_SOCKET, SO_OOBINLINE, &oobinline, sizeof(oobinline)); if (ret == -1) { ERR_EXIT("setsockopt"); } } // 当fd 产生带外数据时会产生SIGURG信号 // 函数开启当前进程接收SIGURG信号的功能 // 在信号处理函数中去接收带外数据 void active_sigurg(int fd) { int ret; ret = fcntl(fd, F_SETOWN, getpid()); if (ret == -1) { ERR_EXIT("fcntl"); } } void handle_sigurg(int sig){char cmdline[MAX_COMMAND_LINE] ={0};int ret = readline(ctrl_fd, cmdline, MAX_COMMAND_LINE);if (ret <= 0){ERR_EXIT("readline");}...} |
选项与填充(选项为4字节整数倍,否则用0填充)
最常见的可选字段是最长报文大小MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段中指明这个选项。它指明本端所能接收的最大长度的报文段(payload)。该选项如果不设置,默认为536(20+20+536=576字节的IP数据报),其中ip首部和tcp首部各20个字节,而internet 上标准的MTU (最小)为576B。
The maximum segment size (MSS) is the largest segment that a TCP is willing to receive from its peer and, consequently, the largest size its peer should ever use when sending. The MSS value counts only TCP data bytes and does not include the sizes of any associated TCP or IP header
二、通讯时序(3次握手-->传输数据-->4次挥手)
下图是一次TCP通讯的时序图:
在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK 1001, <mss 1024>,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss选项值为1024。
建立连接的过程:
1. 客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。
2. 服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。
3. 客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。
在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为'''三方握手(three-way-handshake)'''。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。
在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused.
In general, a reset is sent by TCP whenever a segment arrives that does not appear to be correct for the referenced connection. (We use the term referenced connection to mean the connection specified by the 4-tuple in the TCP and IP headers of the reset.) Resets ordinarily result in a fast teardown of a TCP connection.
数据传输的过程:
1. 客户端发出段4,包含从序号1001开始的20个字节数据。
2. 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。
3. 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。
在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出TCP 数据段给对方之后,只有收到对方应答的ACK段才知道该数据段确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据段或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据段重发。
这个例子只描述了最简单的一问一答的情景,实际的TCP数据传输过程可以收发很多数据段,虽然典型的情景是客户端主动请求服务器被动应答,但也不是必须如此,事实上TCP协议为应用层提供了全双工(full-duplex)的服务,双方都可以主动甚至同时给对方发送数据。
如果通讯过程只能采用一问一答的方式,收和发两个方向不能同时传输,在同一时间只允许一个方向的数据传输,则称为'''半双工(half-duplex)''',假设某种面向连接的协议是半双工的,则只需要一套序号就够了,不需要通讯双方各自维护一套序号。
关闭连接的过程:
1. 客户端发出段7,FIN位表示关闭连接的请求。
2. 服务器发出段8,应答客户端的关闭连接请求。
3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
4. 客户端发出段10,应答服务器的关闭连接请求。
建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况(调用shutdown而不是close),这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。
三、滑动窗口和流量控制
如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过'''滑动窗口(SlidingWindow)'''机制解决这一问题。看下图的通讯过程。
1. 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
2. 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
3. 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
4. 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
5. 发送端发出段12-13,每个段带1K数据,段13同时还包含FIN位。
6. 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,接收端同时声明窗口大小为2K。
7. 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
8. 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
9. 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。
从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
四、TCP如何保证可靠性
1、应用数据被分割成TCP认为最适合发送的数据块,称为段传递给IP层。
2、当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
3、当TCP收到发自TCP连接另一端的数据段,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。
4、TCP将保持它首部和数据的校验和。这是一个端到端的校验和,目的是检测数据在传输过程中的任何变化。如果收到段的校验和有差错,TCP将丢弃这个报文段并且不确认(导致对方超时重传)
5、TCP承载于IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对收到的数据段进行重新排序后呈现在接收缓冲区给应用层提取。
6、IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
7、TCP还能提供流量控制。TCP连接的每一方都有一定大小的缓冲空间。
参考:
《Linux C 编程一站式学习》
《TCP/IP详解 卷一》