传输层则更进一步负责 “进程到进程” 或 “应用到应用” 的通信。你的电脑上可能同时运行着浏览器、微信、音乐播放器等多个程序它们都在通过网络收发数据。传输层要确保浏览器的数据交给服务器的Web服务而不是别的。由于UDP本身不保证可靠性所以我们需要在应用层实现这些机制。这就是为什么在需要可靠传输时我们通常使用TCP或者使用基于UDP的可靠传输协议如QUIC的原因。但是这种方法可能会遇到丢包和乱序的问题。我们可以通过以下方式改进接收端收到包后可以发送一个确认包ACK给发送端告知收到了哪些包。发送端可以根据ACK决定重传哪些包。或者我们可以使用前向纠错FEC的方式发送一些冗余包使得接收端在丢失部分包的情况下也能恢复数据。接收端接收包根据序列号区分不同的大数据将同一个序列号的包收集起来。根据当前包序号和总包数判断是否接收完整。如果接收完整则按照包序号排序然后组合成原始数据。注意由于UDP包最大为64K我们每个包的实际数据部分需要减去我们添加的头部信息序列号、总包数、当前包序号等。但是实际上我们通常不会让UDP包接近64K因为超过MTU通常1500字节会导致IP分片而IP分片会降低传输效率和增加丢包率。因此我们通常会在应用层将数据分成更小的包比如每个包1KB左右避免IP分片。步骤发送端将数据分成多个小块每个小块加上我们自定义的协议头序列号、总包数、当前包序号。依次发送这些包。下面是一个简单的示例说明如何在应用层实现分包和组装假设我们要传输一个大型数据我们将其分成多个包每个包包含序列号用于标识同一个的传输接收端通过序列号来区分不同的大数据总包数当前包序号从0开始数据因此UDP不需要发送缓冲区是因为其无连接和不可靠的特性所决定的。它不需要重传也不需要流量控制所以没有必要缓存发送的数据。2.4 UDP使用注意事项我们注意到UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。 然而64K在当今的互联网环境下是一个非常小的数字。 如果我们需要传输的数据超过64K就需要在应用层手动分包多次发送并在接收端手动拼装。但是我们也要注意网络环境是复杂的我们并不能保证发送的多个包都会按照顺序到达甚至可能丢包。 因此在应用层实现分包和组装时需要考虑以下问题如何分组每个分组应该包含哪些信息如何确保接收端能够正确组装例如需要包含序列号、总包数、当前包序号等如何处理丢包和乱序是否需要进行重传如何重传由于没有发送缓冲区应用程序调用sendto时数据会立即被送出或者因为某些原因被丢弃而不会在UDP层被缓存。这意味着如果网络条件不好数据包可能会被大量丢弃而应用程序可能无法及时得知。实际影响UDP的设计目标就是简单高效。省略发送缓冲区减少了UDP的实现复杂性和内存开销。UDP的简单性TCP是面向连接的、可靠的协议。它使用发送缓冲区来存储已发送但尚未被确认的数据以便在超时或收到重复确认时进行重传。TCP还需要实现流量控制和拥塞控制这些都需要缓冲区来配合。TCP的对比由于UDP不需要保证可靠性因此它不需要维护发送缓冲区来存储已发送但未确认的数据因为不需要重传。同时UDP是无连接的所以它不需要维护连接状态包括发送缓冲区。UDP的无连接和不可靠特性当应用程序调用sendto发送UDP数据报时UDP不会对数据进行缓存而是立即将数据封装成IP数据报并发送到网络。如果应用程序发送数据的速度超过了网络接口的处理能力UDP不会像TCP那样将数据缓存在发送缓冲区中而是直接丢弃数据并返回错误或者取决于具体实现可能不会返回错误但不会保留数据。为什么udp不需要发送缓冲区呢我们可以从UDP的工作方式来理解。UDP的发送过程应用层处理由于UDP不提供可靠性如果应用需要可靠性则必须在应用层实现序列号用于检测丢失和乱序。确认和重传确保数据到达。流量控制防止发送方发送过快导致接收方无法处理。UDP数据报的丢失由于UDP的不可靠性数据报可能会因为以下原因丢失接收缓冲区满如果应用程序读取速度跟不上接收速度导致缓冲区满新到的数据报被丢弃。网络拥堵路由器或交换机的队列满数据报被丢弃。校验和错误如果UDP数据报在传输过程中发生错误校验和不匹配数据报会被丢弃。UDP socket是全双工的一个UDP socket可以同时进行读和写操作。这意味着同一个socket既可以接收数据也可以发送数据而不需要像TCP那样建立连接后才能通信。全双工存在接收缓冲区UDP协议在内核中维护了一个接收缓冲区用于存储接收到的UDP数据报。每个UDP socket都有自己独立的接收缓冲区。不保证顺序由于UDP数据报是独立传输的可能因为网络路径不同而导致到达顺序与发送顺序不一致。接收缓冲区内数据报的顺序是不确定的应用程序必须能够处理乱序到达的数据。缓冲区大小有限接收缓冲区的大小是有限的。当缓冲区满时新到达的UDP数据报会被丢弃并且不会通知发送方。因此如果应用程序不能及时读取数据就会导致丢包。接收缓冲区无真正的发送缓冲区UDP协议本身并不维护一个发送缓冲区。当应用程序调用sendto发送数据时数据会被直接封装成UDP数据报然后交给网络层IP层处理。可能因网络层拥堵而阻塞虽然UDP本身没有发送缓冲区但在数据交给网络层后如果网络层例如由于出口队列已满无法立即发送则可能会阻塞后续的发送操作。但是UDP不会像TCP那样进行重传或拥塞控制它只是尽可能快地发送。对比 TCP 的流式传输TCP 像水管流水没有明确边界UDP 像邮寄包裹每个包裹都是独立的2.3 UDP的缓冲区UDP缓冲区详解发送缓冲区2.2 UDP的特点UDP 核心特点详解无连接 就像寄信不需要先打电话确认没有握手过程不需要像 TCP 那样进行三次握手建立连接直接发送知道目标地址(IP)和门牌号(端口)就直接发送数据无状态服务器不会维护与客户端的连接状态信息优势开销小速度快适合短平快的通信不可靠 就像寄平信不保证对方一定能收到无确认机制发送后不知道对方是否成功接收无重传机制如果数据丢失不会自动重新发送无错误通知网络故障导致发送失败应用层不会收到任何错误报告不保证顺序后发送的数据包可能先到达影响应用层需要自己处理可靠性问题如果需要的话面向数据报 就像寄送包裹每个都是独立完整的数据有明确边界每个 UDP 数据报都是一个完整的消息单元一次性读写应用层每次 sendto() 发送一个完整的数据报每次 recvfrom() 接收一个完整的数据报大小限制每个 UDP 数据报最大约 64KB包含头部举例说明假设你收到一个UDP报文其“16位UDP长度”字段的值经解析后为 1028单位是字节。分离报头直接取前8个字节。这8个字节就包含了所有的控制信息端口号、长度、校验和。计算数据长度有效载荷长度 1028总长度 - 8报头长度 1020字节。分离有效载荷从第9个字节开始连续取1020个字节这部分就是发送方应用程序真正要发送的数据。所以udp报文的报头和有效载荷怎么分离呢具体分离步骤定位报头的开始 UDP报文是从网络层IP层交付上来的。你拿到的是一个完整的UDP报文即 IP 数据包中的数据部分。这个报文的起始位置就是UDP报头的开始。截取固定长度的报头 UDP报头的长度是固定不变的 8个字节64位。所以你只需要UDP报头 UDP报文的前8个字节解析报头字段以获取信息 将这8个字节的报头按图片所示的结构进行解析可以获得关键信息前2个字节16位源端口号。紧接着的2个字节16位目的端口号。再接着的2个字节16位UDP长度。这个字段至关重要它指明了整个UDP报文报头数据的总长度。分离有效载荷 知道了总长度假设为 L又知道报头固定长度为 8字节那么有效载荷的长度就是 L - 8字节。 因此有效载荷的起始位置是第9个字节结束位置是第 L个字节。有效载荷 UDP报文的第9个字节 到 第L个字节作用承载实际要传输的应用层信息。详解这是UDP协议最终要运送的“货物”比如DNS查询内容、语音通话的音频数据、视频流数据等。数据字段的长度是可变的最大为 65535 - 8 65527字节减去8字节的首部。数据作用用于检测UDP首部和数据在传输过程中是否发生错误如比特翻转。详解发送方会根据UDP伪首部一个包含IP地址等信息的结构、UDP首部和数据计算出一个校验值并填入此字段。接收方会进行同样的计算如果计算结果与接收到的检验和不匹配则表明数据在传输中已损坏接收方会静默地丢弃这个包而不会要求重传。注意这个字段在IPv4中是可选的如果不用可置为0但在IPv6中是强制使用的。它为UDP提供了一层最基本的可靠性保障。16位UDP检验和作用指示整个UDP数据包的总长度。详解这个长度包括了UDP首部8字节和数据部分。因为是16位2字节所以最大可以表示 2^16 - 1 65535字节。这意味着一个UDP数据包的最大长度是64KB。接收方通过这个字段可以知道应该从网络层接收多少数据。16位UDP长度作用标识接收数据包的目标应用程序或服务。详解这是最关键字段之一告诉网络设备如路由器、交换机和操作系统这个数据包最终应该交给哪个正在“监听”的应用程序。例如目的端口号是53设备就知道这个包是发给DNS服务的是123则是发给NTP时间同步服务的。16位目的端口号作用标识发送数据包的应用程序或进程。详解这个字段告诉接收方数据是来自发送方设备的哪个“门牌号”端口。接收方在回复数据时就可以将数据发送到这个源端口号从而确保回复能准确送达最初发送数据的那个应用程序。此字段是可选的如果发送方不需要接收回复可以将其置为0。2. UDP协议2.1 UDP协议端格式在这里插入图片描述UDP首部长度固定为8字节包含4个16位字段。各字段详细作用解析16位源端口号进程 A 可以绑定到 (IP地址1, 端口80)进程 B 可以绑定到 (IP地址2, 端口80) 这是完全允许的因为它们仍然是两个不同的端点。绑定到不同的 IP 地址 如果一台机器有多个 IP 地址例如多个网卡或配置了多个虚拟 IP那么例如一个 DNS 服务器进程可以同时在 TCP 53 端口和 UDP 53 端口上提供服务。情况 B特殊情况 —— 允许在某些特定技术手段下可以实现多个进程绑定同一个端口。SO_REUSEADDR / SO_REUSEPORT 套接字选项 这是最常见的实现方式。通过设置套接字选项可以允许多个套接字绑定到相同的地址和端口。SO_REUSEADDR主要用于解决“TIME_WAIT”状态下的端口快速重用问题。在某些系统如 Windows和特定条件下它也能允许多个绑定。SO_REUSEPORTLinux 3.9 引入明确设计用于允许多个进程或线程绑定到完全相同的 IP 地址和端口号。操作系统内核会在内核层面进行负载均衡将传入的连接均匀地分配给这些监听了同一端口的进程。应用场景多进程网络服务器如 Nginx可以使用 SO_REUSEPORT 来实现让多个 worker 进程同时监听 80 端口提高性能并避免“惊群”问题。多播 / 组播 在组播中多个进程可以加入同一个组播组并绑定到同一个端口来接收发送到该组播地址的数据包。这是一种一对多的通信模式。不同的传输协议 一个端口号可以同时被一个 TCP 进程和一个 UDP 进程绑定。因为 (IP, Port, TCP) 和 (IP, Port, UDP) 被视为两个完全不同的端点。问题二一个端口号是否可以被多个进程bind?答案通常情况下不行但在特定条件下可以。这是一个更复杂的问题我们需要分情况讨论。情况 A默认情况 —— 不允许在绝大多数情况下操作系统不允许两个进程绑定到同一个端口号。如果你尝试这样做第二个进程在调用 bind() 时会失败并得到一个类似 “Address already in use” 的错误。原因当数据包到达时操作系统需要通过 IP地址 端口号 协议TCP/UDP 这个三元组来唯一确定应该将数据交付给哪个进程的哪个套接字。如果两个进程绑定了同一个端口操作系统将无法做出唯一决策导致数据混乱。应用场景举例FTP 服务器通常使用两个端口。端口 21 用于控制连接传输命令。另一个随机或指定的端口如 20用于数据连接传输文件内容。自定义服务一个后台进程可能同时提供管理接口绑定端口 9000和用户数据接口绑定端口 8080。负载监听一个进程可以同时监听多个端口以处理不同类型的请求。执行下面的命令可以看到知名端口号代码语言javascriptAI代码解释cat /etc/services我们自己写一个程序使用端口号时要避开这些知名端口号1.4 两个问题下面两个问题其实我们在之前的文章中就已经提过下面我们再来提一下问题一一个进程是否可以bind多个端口号?答案可以。一个进程可以创建多个网络套接字并将每个套接字绑定到不同的端口号上。这是非常常见的技术。工作原理每个 socket 是一个独立的通信端点。一个进程可以调用多次 socket() 系统调用创建多个套接字描述符。然后对每个套接字描述符调用 bind()并指定不同的端口号。1.3 认识知名端口号(Well-Know Port Number)有些服务器是非常常用的为了使用方便人们约定一些常用的服务器都是用以下这些固定的端口号ssh服务器使用22端口ftp服务器使用21端口telnet服务器使用23端口http服务器使用80端口https服务器使用443端口在这里插入图片描述在TCP/IP协议中用 “源IP”“源端口号”“目的IP”“目的端口号”“协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看);在这里插入图片描述1.2 端口号范围划分0 - 1023知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议他们的端口号都是固定的.1024 - 65535操作系统动态分配的端口号客户端程序的端口号就是由操作系统从这个范围分配的.如何实现—— 通过端口号1.1 再谈端口号传输层使用端口号来标识主机上的不同应用程序。发送端知道目标服务器的IP地址和目标应用程序的目标端口号如Web服务通常是80/443。接收端通过数据包中的目标端口号就知道该把这个数据交给哪个应用程序处理。