TCP是TCP/IP协议族中一个最核心的协议,它向下使用网络层IP协议,向上为应用层HTTP、FTP、SMTP、POP3、SSH、Telnet等协议提供支持。
本文给出TCP报文格式的详细说明,介绍网络数据包传递中如何进行地址解析、建立TCP连接的三次握手过程以及断开TCP连接的四次挥手过程。
传输控制协议(英语:Transmission Control Protocol,缩写:TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由国际互联网工程任务组(The Internet Engineering Task Force, IETF)的 RFC793 定义。在简化的计算机网络 OSI 模型中,它完成传输层所指定的功能。
在TCP定义中,有以下3点需要特别说明,图解:TCP 和 UDP 协议
面向连接是相对于另一个传输层协议UDP(User Datagram Protocol, 用户数据报协议)而言的。TCP在开始传输数据前要先经历三次握手建立连接,并通过连接一对一发送消息,传输结束后通过四次挥手断开连接。
而UDP是无连接的,发送方在发送数据之前不需要与接收方建立连接,即刻可以传输数据,每个UDP数据包都是独立的,相互之间没有关联,因此UDP可以一对一、一对多或多对多发送消息。
是否可靠也是相对于UDP而言的。TCP自身有三次握手和超时重传等机制确保数据的可靠传输,发送方在发送数据包后会等待接收方发送确认(ACK)消息。如果发送方在一定时间内未收到确认消息,它将假定数据丢失,并重新发送数据。接收方收到重复的数据包时会发送冗余的ACK消息来通知发送方,以避免数据丢失。
同时TCP还提供流量控制和拥塞控制,以保持网络的稳定性和性能。因此无论网络如何变化,只要不是主机宕机等原因都可以保证一个报文可以到达目标主机。
相对于TCP的可靠传输,UDP是不可靠的。UDP数据包的传输过程中不提供确认、重传、流量控制和拥塞控制等机制,因此UDP数据包可能丢失、重复、乱序或损坏。
TCP是面向字节流的传输,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。
与面向字节流相对的是UDP的面向报文。UDP对应用层交下来的报文,既不合并也不拆分,而是保留这些报文的边界,即应用层交给UDP多长的报文,UDP就照样发送,一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率。若太短,会使IP报文太小。
了解报文格式是搞懂一个通信协议的必经之路。TCP报文由TCP首部(报头)和应用数据构成,其中TCP首部是TCP 协议的核心所在,应用数据部分是TCP报文的负载,如下图所示。
以下详细介绍各字段含义:
我们曾经在公众号其他文章中介绍了IP报头中“源地址”和“目的地址”,与本文TCP报头中的“源端口”和“目的端口”共同确定了数据包传递过程中需要的地址,如下图所示。
类比日常工作中邮寄信件,我们装在信封里的信件相当于要传递的数据,标准的信件格式是要在信封上写“收信人地址”和“寄信人地址”,相当于IP地址,其中,“收信人地址”对应数据包里IP报头中的“目的IP地址”,“寄信人地址”对应数据包里IP报头中的“源IP地址”,写上寄信、收信两个地址就可以保证信件可以邮寄到目的地了。
但信件邮寄到目的地址后由谁来收?从上面这封信的收件人地址检索到这个地址是位于上海市浦东新区张江“A公司B部门”的,这个部门可能有成百上千人,收件人不明确,即使把信件送到这个地址,也没办法投递到具体的收信人。
因此,邮件信件需要填写“收件人姓名”、“收件人地址”和“寄件人姓名”、“寄件人地址”的组合,才能保证信件能准确投递到具体的收件人手中。这里的收信人姓名相当于TCP报头的目的端口,寄信人姓名相当于TCP报头的源端口。
对比传递信件,我们来看网络数据包传递过程的例子。位于北京的李四(电脑IP地址: 106.54.28.25)给上海的张三(电脑IP地址: 114.92.67.193)通过QQ(端口: 80)发送一条消息,如下图所示:
首先,李四电脑将消息打包成TCP数据报后,添加IP报头和以太网报头形成网络数据包,发送到计算机网络中。计算机网络通过数据包中IP报头的目的IP地址(114.92.67.193)把该数据包准确传递到张三电脑。
张三电脑收到了李四电脑发送过来的数据包后,由于张三电脑上同时运行有多个程序(例如图中的QQ、微信、Foxmail等),虽然张三电脑知道这个数据包是传输给它的,但是它不知道该把这个数据包中的数据交给哪个程序。
针对这个问题,使用数据包中TCP报头的源端口和目的端口,根据不同的程序使用不同端口号来确定应用程序并发送和接受数据,这样数据包就能像邮寄信件一样准确投递到具体电脑上指定的程序了。例如我们指定张三电脑上QQ、微信、Foxmail使用的端口分别是80、8900和110,那么当收到数据包里目的端口80就是传输给QQ的。
上述例子还可以引申出数据包结构中的其他字段的作用,例如我们收到信后可以简单地通过信封是否完整,来检查该信件是否被别人在传输途中拆开并篡改过信件内容。对于网络数据包,TCP报头的“校验和”(Checksum)可以验证收到数据包数据是否在途被别人拆开修改过。
为什么需要建立TCP连接?首先,IP协议是无连接的,IP并不维护任何关于后续数据报的状态信息,每个数据报的处理相互独立。这种无连接的优点是不占用线路,降低了对网络线路的要求。
此外,IP协议是不可靠的,不能保证IP数据报能成功到达目的地,是一种尽力而为的传输服务,路由器对IP报文出现错误的处理方式是丢包,并发送ICMP(Internet Control Message Protocol,互联网控制协议)控制消息给源地址。因为IP协议是无连接、不可靠的,
因此,需要上层TCP来建立连接和差错重传,实现面向连接的、可靠的、基于字节流的传输层通信协议。
4.1 三次握手过程详解
由于建立TCP连接的过程需要来回3次,所以将这个过程形象的叫做三次握手(Three-Way Handshake),一旦建立连接,两台主机就可以进行全双工的通信。
下面是三次握手的详细过程,包括发送的报文段内容(以女朋友为例讲解 TCP/IP 三次握手与四次挥手):
首先客户端发起连接请求,向服务器发送一个SYN(同步)报文段,段中包含了目的端口和本机端口,设置SYN 标志位为1,即SYN=1,并设置序号字段(Sequence Number)为一个随机选择的x,即seq=x,也就是初始序号(Initial Sequence Number, ISN),如果是第一个连接,很可能是0。此时服务器对应的端口要处于监听状态,客户端发起请求后进入 SYN_SENT 状态,等待服务器的确认。
服务端收到客户端发来的 SYN 报文段,对这个SYN报文段进行确认。服务器向客户端发送一个SYN-ACK报文段作为回应,报文段中的标志位设置为SYN=1和ACK=1,表示同时作为确认和同步;序号字段设置为服务器的随机选择的初始序号y(服务端的TCP段序号),即seq=y;确认号字段(Acknowledgment Number)设置为客户端的初始序号加1,即ack=x+1。服务器端将上述所有信息放到一个TCP段(即SYN+ACK段)中,一并发送给客户端,此时服务器进入SYN_RECV状态。
客户端接收到服务端发来的 SYN+ACK 报文段后,要向服务端发送一个ACK(确认)报文段,对连接请求的确认进行确认。报文段中的标志位设置为ACK=1,确认号字段设置为服务器的初始序号加1,即ack=y+1,序号字段设置为客户端的初始序号加1,即seq=x+1。此时客户端进入 ESTABLISHED(已连接)状态,服务端接收到此 TCP段,也将进入 ESTABLISHED 状态,也就标志着三次握手结束,连接成功建立。
三次握手完成之后,TCP连接就正式建立起来了,双方可以开始进行数据的可靠传输。三次握手的目的是确保双方的初始序号和确认号的同步,并验证双方的可达性。通过这个过程,TCP可以建立一个可靠的双向通信通道,在后续的数据传输中保证数据的可靠性和顺序性。
四次挥手是TCP断开连接的过程。
客户端数据发送完成,则向服务端发送连接释放请求的FIN报文(请求连接终止:FIN=1),主动关闭TCP连接。报文中会指定一个序列号seq=u,并停止再发送数据,但依然能够接收数据。此时客户端处于 FIN_WAIT_1 状态,等待服务端确认。TCP规定,FIN报文即使不携带数据,也要消耗一个序号。
服务端收到FIN报文之后,通知相应的高层应用进程,告诉它客户端向服务端这个方向的连接已经释放了。此时服务端向客户端发出连接释放的应答ACK报文,并进入了CLOSE_WAIT(关闭等待)状态。ACK报文头包含:ACK=1,ack=u+1,并且带上自己的序列号seq=v。这里ack=u+1是第一次挥手的序列值+1,表示希望收到从第u+1个字节开始的报文段,并且已经成功接收了前u个字节。
客户端收到服务端的确认后,进入 FIN_WAIT_2 状态,等待服务端发出的连接释放报文段。
前两次挥手既让服务端知道了客户端想释放连接,也让客户端知道了服务端已了解自己想要释放连接的请求。
如果服务端也想断开连接,就向客户端发送连接释放报文。由于在CLOS_WAIT状态,服务端很可能又发送了一些数据,假定此时连接释放报文的序列号为seq=w,ack也是取第一次挥手的seq +1,即ack=u+1,这和第二次挥手时是一样的。
此时服务端就进入了LAST_ACK(最后确认)状态,等待客户端的确认,并停止向客户端发送数据,但服务端仍能够接收从客户端传输过来的数据。
客户端收到服务器的连接释放报文后,一样发送一个 ACK 报文作为应答(ack=w+1,seq=u+1), 此时客户端处于TIME_WAIT(时间等待)状态,并在这个状态等待 2MSL(Two Maximum Segment Lifetime, 最大报文生存时间)。
服务端收到从客户端发出的 TCP 报文之后结束 LAST-ACK 阶段,进入 CLOSED 阶段。客户端等待完 2MSL之后,结束 TIME-WAIT 阶段,进入 CLOSED 阶段,由此完成四次挥手。
为什么客户端在TIME_WAIT阶段要等 2MSL? 主要有以下两点:
一是为了保证客户端发送的最后一个ACK报文段能够到达服务器端,确保服务端能正常进入CLOSED状态。服务端在 1MSL 内没有收到客户端发出的 ACK 确认报文,就会再次向客户端发出 FIN 报文。
二是为了避免新旧连接混淆。由于网络滞留,客户端可能发送了多次请求建立连接的请求,经过时间2MSL,就可以使本链接持续时间内所产生的所有报文段都从网络中消失,这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。