网络编程—UDP与TCP

在计算机网络中,传输层承上启下,一边对接应用层的各种服务,一边利用网络层提供的主机到主机通信能力,最终实现端到端的进程间数据传输。

说到传输层,绕不开的两个核心协议就是UDP和TCP------一个追求极致效率,一个强调可靠交付。

一、传输层

1.1 定位

网络层解决的是主机之间的通信问题,IP地址可以唯一标识一台主机。

*但一台主机上同时运行着多个网络应用,数据送到主机后,该交给哪个程序处理?*这就是传输层要回答的问题。

可以看到,传输层位于应用层和网络层之间,负责将应用层的数据按照不同协议的要求封装后交给网络层传输,同时将网络层收到的数据分发给对应的应用程序。

1.2 端口号

端口号(Port)是传输层用来标识主机上不同应用程序的编号。

可以把IP地址理解为一栋楼的地址,端口号就是楼里不同房间的门牌号。数据到达主机后,根据端口号就能找到对应的应用程序。

上图中,一台主机上同时运行着FTP、SSH、SMTP等多个服务器程序,每个程序绑定了不同的端口号。

当IP层把数据送到这台主机后,传输层根据目的端口号,就能准确地将数据交给对应的应用程序处理。

端口号是一个16位的整数,取值范围是0到65535。这个范围又被划分为两部分:

  • 0 - 1023:知名端口号,分配给广泛使用的应用层协议,如HTTP的80端口、HTTPS的443端口、SSH的22端口等。这些端口号由IANA统一管理和分配。

  • 1024 - 65535:动态端口号,由操作系统动态分配给客户端程序。当我们用浏览器访问网站时,浏览器会从这个范围中获得一个临时端口号,用来和服务器的80或443端口通信。

知名端口号:

协议 端口号 说明
FTP 21 文件传输协议
SSH 22 安全远程登录
Telnet 23 远程终端协议
HTTP 80 超文本传输协议
HTTPS 443 安全超文本传输协议
DNS 53 域名系统
SMTP 25 简单邮件传输协议

在Linux系统中,可以通过 cat /etc/services命令 查看系统中记录的知名端口号列表。编写自己的网络程序时,应当避开这些知名端口号,防止和系统服务产生冲突。

1.3 五元组

在TCP/IP协议中,一条通信由五个要素唯一确定,这就是所谓的"五元组":

  • 源IP地址

  • 源端口号

  • 目的IP地址

  • 目的端口号

  • 协议号

服务器的IP地址是172.20.100.32,端口号是80。客户端B(172.20.100.33)使用端口2001访问服务器,客户端A(172.20.100.34)使用端口2002也访问同一个服务器。虽然目的IP和目的端口都相同,但源IP和源端口不同,因此是两条独立的通信。

可以用 netstat -n 命令查看当前系统中的网络连接,每一行记录都包含了这五个要素。

二、UDP协议

UDP(用户数据报协议)是传输层的另一个重要协议。与TCP相比,UDP的设计理念不同------简单、快速,不可靠。

2.1 报文格式

UDP的报文格式非常简洁,首部只有8个字节,包含四个字段:

各字段的含义:

  • 16位源端口号:发送方的端口号,标识数据从哪个进程来。

  • 16位目的端口号:接收方的端口号,标识数据要到哪个进程去。

  • 16位UDP长度:整个UDP数据报的长度,包括首部和数据部分。

  • 16位UDP校验和:用于检测数据在传输过程中是否出错。

2.2 特点

UDP的传输过程类似于寄信------写上地址直接投进邮筒,对方能不能收到、什么时候收到,寄信人都不管。UDP有三个特点:

(1)无连接

知道对端的IP和端口号就直接进行传输,不需要建立连接;

(2)不可靠

没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;

(3)面向数据报

不能够灵活地控制读写数据的次数和数量。

2.3缓冲区

UDP的缓冲区设计:

  • 发送缓冲区:UDP没有真正意义上的发送缓冲区。调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。

  • 接收缓冲区:UDP具有接收缓冲区。当数据到达时,先放入接收缓冲区,应用程序调用recvfrom时从缓冲区中读取。

**注意:**UDP的接收缓冲区不能保证收到的报文顺序和发送的顺序一致。因为IP网络本身是不保证按序交付的,UDP也不做排序处理。

另外: UDP的socket既能读也能写,这个概念叫做**全双工****:**同一个socket可以同时进行发送和接收操作。

2.4 注意事项

UDP首部中有一个16位的长度字段,这意味着一个UDP数据报的最大长度是64KB(包含UDP首部)。

在当今互联网环境下,64KB实在是一个很小的数字。一张高清图片、一段视频数据都远远超过这个大小。

如果需要传输的数据超过64KB,就需要在应用层手动分包,多次发送,并在接收端手动拼装。

2.5 应用层协议

虽然UDP不可靠,但它的简单和高效在很多场景下不可替代。以下是基于UDP的应用层协议:

  • DNS:域名解析协议。每

  • DHCP:动态主机配置协议。

  • TFTP:简单文件传输协议。

  • NFS:网络文件系统。

  • BOOTP:启动协议。

  • SNMP:简单网络管理协议。

当然,也包括你自己写UDP程序时自定义的应用层协议。很多在线游戏、实时视频传输也会选择UDP作为底层传输协议,然后在应用层实现必要的可靠性保证。


三、TCP协议

TCP(传输控制协议)是传输层中最复杂、功能最强大的协议。从名字就能看出来,它的核心是"控制"------对数据传输的方方面面进行精细的控制,以确保可靠、高效地交付。

3.1 报文段格式

TCP的报文段格式比UDP复杂得多。首部默认长度是20字节,如果包含选项字段,最长可以达到60字节。

源端口号和目的端口号:各16位,和UDP一样,用于标识发送和接收的进程。

32位序号:TCP将每个字节的数据都编了号,序号字段表示本报文段所发送数据的第一个字节的编号。

32位确认号:表示期望收到对方下一个报文段的第一个字节的序号。

4位首部长度:表示TCP首部有多少个32位字。

6位保留位:预留字段,目前未使用,置为0。

6位标志位:这是TCP报文中非常重要的字段,每个位都有独立的含义:

标志位 名称 含义
URG 紧急指针有效位 为1时表示紧急指针字段有效,有紧急数据需要优先处理
ACK 确认有效位 为1时表示确认号字段有效,建立连接后所有报文的ACK都为1
PSH 推送位 为1时提示接收端应用程序立刻从TCP缓冲区把数据读走
RST 复位位 为1时表示要求重新建立连接,携带RST的称为复位报文段
SYN 同步位 为1时表示请求建立连接,携带SYN的称为同步报文段
FIN 结束位 为1时表示通知对方本端要关闭了,携带FIN的称为结束报文段

16位窗口大小:用于流量控制,表示接收方当前还能接收多少字节的数据。窗口越大,网络吞吐量越高。

16位校验和:用于检测传输错误。但TCP的校验和是必须的,而且不仅包含首部,也包含数据部分。

16位紧急指针:只有当URG标志为1时才有效,标识紧急数据的末尾在报文段中的位置。

选项字段:最多40字节,用于支持一些额外的功能,如最大段大小、窗口扩大因子、时间戳等。

TCP首部的C语言定义,可以和上面的格式图对照着看:

cpp 复制代码
// linux kernel include/linux/tcp.h
struct tcphdr {
    __be16  source;     // 源端口
    __be16  dest;       // 目的端口
    __be32  seq;        // 序号
    __be32  ack_seq;    // 确认号
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u16   res1:4,     // 保留位
        doff:4,         // 首部长度
        fin:1,          // FIN标志
        syn:1,          // SYN标志
        rst:1,          // RST标志
        psh:1,          // PSH标志
        ack:1,          // ACK标志
        urg:1,          // URG标志
        ece:1,          // ECE标志
        cwr:1;          // CWR标志
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u16   doff:4,
        res1:4,
        cwr:1,
        ece:1,
        urg:1,
        ack:1,
        psh:1,
        rst:1,
        syn:1,
        fin:1;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif  
    __be16  window;     // 窗口大小
    __sum16 check;      // 校验和
    __be16  urg_ptr;    // 紧急指针
};

除了上面介绍的6个标志位,内核定义中还多了CWR和ECE两个标志位。这两个用于ECN(显式拥塞通知)机制,是对TCP拥塞控制的改进。

3.2 确认应答机制

TCP可靠性的核心机制之一就是确认应答(ACK)。简单来说,就是发送方发了数据之后,接收方要回复一个确认,表示"我收到了"。

基本过程:主机A向主机B发送了1-1000字节的数据,主机B收到后回复一个确认应答,确认号是1001,意思是"1000号之前的数据我都收到了,下一次你从1001开始发"。然后主机A再发送1001-2000字节,主机B再确认下一个是2001,以此类推。

TCP将每个字节的数据都进行了编号,这就是序列号。确认应答也是基于序列号的。

第1字节到第1000字节对应序号1,第1001字节到第2000字节对应序号1001,第2001字节到第3001字节对应序号2001。每个TCP报文段的序号字段就是该段第一个字节的编号。

例子:唐僧讲经,每讲一段就要问徒弟"听懂了吗?",徒弟说"听懂了",唐僧才继续讲下一段。这个一问一答的过程,就是确认应答机制。

确认应答机制保证了数据的可靠交付------只要收到了ACK,就说明对方确实收到了数据。

但如果ACK丢了怎么办?或者数据本身丢了怎么办?这就需要超时重传机制来解决。

3.3 超时重传机制

TCP通过超时重传来以上情况:发送方发送数据后,启动一个定时器,如果在特定时间内没有收到确认应答,就认为数据丢失了,重新发送一遍。

主机A发送了1-1000字节的数据,但数据在传输过程中丢失了。主机B根本没收到,自然也不会回复ACK。经过一段时间后,主机A的定时器超时了,于是重新发送一遍数据。这次数据成功到达,主机B回复了确认应答。

但还有另一种情况:数据成功到达了,但ACK丢了。

主机A等不到ACK,超时后重传了数据。这时候主机B会收到重复的数据,TCP需要能够识别出重复的报文并丢弃。

怎么识别重复报文呢?利用序列号,接收方根据序号就能判断哪些数据已经收到过,重复的直接丢弃。

超时时间应该设多长呢?这是需要权衡的问题:

  • 设得太长:重传不及时,整体传输效率低。

  • 设得太短:可能频繁重传,浪费网络带宽。

CP采用了动态调整超时时间的策略:

  • Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。

  • 如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传。

  • 如果仍然得不到应答,等待4*500ms进行重传。依次类推,以指数形式递增。

  • 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。

3.4 连接管理机制

TCP是面向连接的协议,通信之前需要建立连接,通信结束后需要释放连接。

建立连接通过三次握手完成,释放连接通过四次挥手完成。

(1)三次握手

三次握手的过程:

第一次握手:客户端向服务器发送SYN报文(SYN=1),请求建立连接。此时客户端的状态从CLOSED变为SYN_SENT。报文中包含客户端的初始序列号(ISN)。

第二次握手:服务器收到SYN后,回复一个SYN+ACK报文(SYN=1, ACK=1)。ACK表示确认收到了客户端的SYN,SYN表示服务器也请求建立连接。此时服务器的状态从LISTEN变为SYN_RCVD。报文中包含服务器的初始序列号。

第三次握手:客户端收到SYN+ACK后,回复一个ACK报文(ACK=1),确认收到了服务器的SYN。此时客户端的状态变为ESTABLISHED。服务器收到这个ACK后,状态也变为ESTABLISHED。连接建立完成。

(2)四次挥手

四次挥手的过程:

第一次挥手:客户端发送FIN报文(FIN=1),表示"我没有数据要发送了,请求关闭连接"。此时客户端状态从ESTABLISHED变为FIN_WAIT_1。

第二次挥手:服务器收到FIN后,回复一个ACK报文,表示"我收到了你的关闭请求"。此时服务器状态从ESTABLISHED变为CLOSE_WAIT。客户端收到这个ACK后,状态变为FIN_WAIT_2。

注意,此时连接处于半关闭状态------客户端已经不能发送数据了,但服务器还可以继续发送数据。服务器可能还有一些数据没发完,需要继续发送。

第三次挥手:服务器发完所有数据后,也发送一个FIN报文,表示"我也发完了,可以关闭了"。此时服务器状态从CLOSE_WAIT变为LAST_ACK。

第四次挥手:客户端收到FIN后,回复一个ACK报文,表示确认。此时客户端状态从FIN_WAIT_2变为TIME_WAIT。服务器收到ACK后,状态变为CLOSED。客户端需要等待2MSL的时间后,才会变为CLOSED状态。

3.5 TCP状态转换

下面是TCP状态转换的汇总:

较粗的虚线表示服务端的状态变化,较粗的实线表示客户端的状态变化。

CLOSED是一个假想的起始点,不是真实状态。

3.6 TIME_WAIT状态

TIME_WAIT是TCP状态中最值得深入理解的一个状态。主动关闭连接的一方在发送最后一个ACK后,会进入TIME_WAIT状态,等待2MSL的时间才会回到CLOSED状态。

MSL(报文最大生存时间)是TCP报文在网络中能够存在的最长时间。RFC 1122中规定MSL为2分钟,但实际操作系统的实现通常更短,比如CentOS 7和Ubuntu上默认是60秒。

通过以下命令查看系统的tcp_fin_timeout值:

为什么需要TIME_WAIT?

原因有两个:

  1. 保证迟到的报文段消失:网络中可能存在一些迟到的报文段。如果连接立刻关闭,这些迟到的报文段可能被新的连接(使用相同的五元组)误接收,导致数据错误。

  2. 保证最后一个ACK可靠到达:假设最后一个ACK丢失了,服务器会重发FIN。如果客户端已经关闭了,就无法重发ACK了。

3.7 CLOSE_WAIT状态

和TIME_WAIT不同,CLOSE_WAIT是被动关闭方的状态。当被动关闭方收到对方的FIN后,回复ACK,就进入了CLOSE_WAIT状态。

CLOSE_WAIT状态的含义是:等待被动关闭方的应用程序调用close关闭连接。收到FIN只是表示对方不再发送数据了,但被动关闭方自己可能还有数据要发送。等数据发完了,调用close发送FIN,才会从CLOSE_WAIT进入LAST_ACK状态。

如果服务器上出现大量的CLOSE_WAIT状态,通常意味着服务器没有正确关闭socket------收到了对方的FIN,但自己没有调用close。这是一个BUG,会导致资源泄漏。加上对应的close调用即可。

3.8 滑动窗口

确认应答机制保证了可靠性,但如果每发一段数据就要等一个ACK,那传输效率也太低了。尤其是在网络延迟比较大的情况下,大部分时间都在等待。

停等协议的工作方式:发一段,等一个ACK,再发下一段。可以看到,数据传输的时间和等待ACK的时间是交替的,大量时间浪费在等待上。

那能不能一次多发几段数据,让多个段的等待时间重叠在一起呢?这就是滑动窗口的思路。

图中的窗口大小是4000个字节(四个段)。发送前四个段的时候,不需要等待任何ACK,直接发送。收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,依次类推。

窗口越大,网络的吞吐率就越高。因为可以同时有更多的数据在网络中传输,等待时间被重叠了。

操作系统内核为了维护滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答。只有确认应答过的数据,才能从缓冲区删掉。

窗口分为已确认的部分、窗口内已发送未确认的部分、以及窗口内未发送的部分。当收到ACK后,窗口的左边界向右移动,新的数据可以被发送。

过几天再补先写到这。。。。

相关推荐
冰封之寂1 小时前
Docker 资源管理终极指南:CPU、内存与 IO 的精细化控制
linux·运维·服务器·docker·云计算
大熊猫侯佩1 小时前
WWDC26 全新 SwiftData 的 .codable 宏选项:它不是泛型专用胶水
数据库·swift·apple
treesforest1 小时前
平时上网留下的IP地址,到底能被查到什么?
网络·网络协议·tcp/ip·ip地址·ip查询·定位服务
xiaoye-duck2 小时前
《Linux系统编程》Linux 系统多线程(五):<线程同步与互斥>线程互斥(上):从条件变量到生产者消费者模型详解
linux·线程
多巴胺梦想家2 小时前
06 代数系统:运算背后的结构之美
网络·拓扑学
小柯南敲键盘2 小时前
图片翻译API接入与自动化实现指南
运维·python·自动化
小锋学长生活大爆炸3 小时前
【福利】最新免费领取云服务器和虚拟主机攻略
网络·github
952363 小时前
Docker - 基础
运维·后端·docker·容器
Promise微笑3 小时前
智能激光清障仪选型指南:高效运维与安全保障的关键考量
运维·安全
8125035333 小时前
第 60 篇:IP选项:那些被遗忘的功能
网络·tcp/ip·智能路由器