端口号
任何时候,多个进程可能同时使用 TCP、UDP、SCTP 这三种传输层协议中的任何一种。这三种协议都使用 16 位整数的 端口号 来区分这些进程。
当一个客户想要和一个服务器联系时,它必须标识想要与之通信的这个服务器。
一方面,TCP、UDP、SCTP 定义了一组众所周知的端口,用于标识众所周知的服务。举例说明,支持 FTP 的任何 TCP/IP 实现都将 21 端口赋予 FTP 服务器。支持 TFTP 的任何 UDP 实现都将 69 端口赋予 TFTP 服务器。
另一方面,客户通常使用短期存活的临时端口,这些由传输层协议自动分配。
IANA 维护着一张端口号分配状况清单。其中端口号被划分为以下三段:
- 众所周知的 0 ~ 1023。这些端口由 IANA 分配和控制。可能存在这样一种情况:相同端口号分配给 TCP、UDP、SCTP的同一给定服务。
- 已登记的端口 1024 ~ 49151。这些端口不受 IANA 控制,不过由 IANA 登记并提供它们的使用情况清单,以方便整个群体。同样的也存在相同端口号分配给 TCP、UDP、SCTP 的同一给定服务。
- 49152 ~ 65535 是动态或私用的。IANA 不管这些端口,它们也就是我们常说的临时端口。
套接字对
一个 TCP 连接的套接字对是一个定义该连接的两个端点的四元组:本地 IP 地址、本地 TCP 端口号、外地 IP 地址、外地 TCP 端口号。套接字对唯一标识一个网络上的每一个 TCP 连接。
标识每个端点的两个值 (IP 地址和端口号) 通常称为一个套接字。
TCP 端口号和并发服务器
并发服务器种主服务器循环通过派生一个子进程来处理每一个新的连接。如果一个子进程继续使用服务器众所周知的端口来服务一个长时间的请求,那将会发生什么?
举一个例子说明。
在主机 freebsd 上启动服务器,该主机是多宿的,其 IP 地址为 12.106.32.254 和 192.168.42.1。服务器在它众所周知的端口 21 上执行被动打开,从而等待可u的请求。
我们使用记号 {*:21,*: *}指示服务器的套接字对。服务器在任何本地接口的端口 21 上等待连接请求。任何外地 IP 地址和任何外地端口请求连接。
接下来在 IP 地址为 206.168.112.219 的主机上启动第一个客户,它对服务器的 IP 地址之一 12.106.32.254 执行主动打开,假定客户主机的 TCP 选用的临时端口为 1500。
当服务器接收到这个客户的连接请求时,它 fork 一个自身的副本,让子进程来处理这个客户的请求。
假设现在在客户主机上另外有一个客户请求连接到同一个服务器,客户主机的 TCP 选择的临时端口为 1501。
通过这个例子应该注意:TCP 无法仅通过查看目的端口号来分离外来的节点到不同的端点。它必须查看套接字对的所有 4 个元素才能确定哪个端点接收某个到位的分节。
缓冲区大小及限制
首先介绍一下影响 IP 数据报大小的限制。
- IPv4 数据报的最大大小为 65535 字节,包含 IPv4 首部。这是由 IPv4 数据报报文格式中报文长度字段决定的。
- IPv6 数据报的最大大小为 65575 字节,包含 IPv6 首部。这是由 IPv6 数据报报文格式中报文长度字段决定的。
IPv6有一个特大净荷的选项,它将净荷长度扩展到32位,不过这需要 MTU 超过65535 的数据链路提供支持。 - 许多网络有一个可由硬件规定的 MTU。例如:以太网的 MTU 是 1500 字节。
IPv4 要求的最小链路 MTU 是 68 字节。这允许最大的 IPv4 首部 (包括 20 字节的固定长度和最多 40 字节的选项部分) 拼接最小的片段 (IPv4 首部中片段偏移字段以 8 个字节为单位)。
IPv6 要求的最小链路 MTU 是 1280 字节。 - 在两个主机之间的路径中最小的 MTU 称为 路径MTU 。
- 当一个 IP 数据报将从某个接口送出时,如果它的大小超过相应链路的 MTU,IPv4 和 IPv6 将执行分片。这些片段在到达最终目的地之前通常不会被重组。
IPv4 和 IPv6 主机或路由器都会对其产生的数据报执行分片操作。IPv4路由器会对其转发的数据报执行分片操作,而 IPv6路由器则不会。 - IPv4 首部的 “不分片” 位若被设置,那么不管是发送这些数据报的主机还是转发它们的路由器,都不允许对它们分片。当路由器接收到一个超过其外出链路的 MTU 大小且 “不分片” 位被设置的 IPv4 数据报时,它将产生一个 ICMPv4 出错消息 (“destination unreachable, fragementation needed but DF bit set”)。
IPv6路由器接收到一个超过其外出链路的 MTU 大小的 IPv6 数据报时,它将产生一个 ICMPv6 出错消息 (“packet too big”)。 - IPv4 和 IPv6 都定义了 最小重组缓冲区大小 ,它是 IPv4 或 IPv6 的任何实现都必须保证支持的最小数据报大小。
- TCP 有一个 MSS(最大分节大小),用于向对端 TCP 通告对端在每个分节中能发送的最大 TCP 数据量。MSS 的目的就是告诉对端其重组缓冲区大小的实际值,从而试图避免分片。
TCP 输出
如图展示了某个应用进程写数据到一个 TCP套接字中时发生的步骤。
每一个 TCP 套接字都有一个发送缓冲区。当某个应用进程调用 write 时,内核从该应用进程的缓冲区中复制所有的数据写到套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据,该应用进程将被投入睡眠。内核将不从 write 系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个 TCP 套接字的 write 调用成功返回仅仅标识我们可以重新使用原来的应用进程缓冲区,而不代表对端的 TCP 或应用程序已经接收到数据。
本端的 TCP 提取套接字发送缓冲区中的数据并将其发送给对端 TCP,其过程基于 TCP 数据传送的所有规则。对端 TCP 必须确认收到的数据,伴随来自对端的 ACK 不断到达,本端 TCP 至此才从套接字发送缓冲区中丢弃已确认的数据。TCP 必须为已发送的数据保留一个副本,直至它被对端确认。
本端 TCP 以 MSS 大小的或更小的块把数据传递给 IP,同时给每个数据块安上一个 TCP 首部以构成 TCP 分节,其中 MSS 或是由对端通告的值,或是最小重组缓冲区大小;IP 给每个 TCP 分节安上一个 IP 首部以构成 IP 数据报,并按照其目的地址查找路由表以确定外出接口,然后将数据报传送给相应的数据链路。
UDP 输出
如图展示了某个应用进程写数据到一个 UDP 套接字中时发生的步骤。
我们使用虚线框展示套接字发送缓冲区,因为实际上它并不存在。任何 UDP 套接字都有发送缓冲区,不过它仅仅是写到该套接字的 UDP 数据报的大小上限。如果一个应用进程写一个大于套接字发送缓冲区大小的数据报,内核将返回该进程一个 EMSGSIZE 错误。既然 UDP 是不可靠的,它不必保存应用程序数据的一个副本,因此无需一个真正的发送缓冲区。(应用进程的数据在沿协议栈向下传递时,通常被复制到某种格式的一个内核缓冲区中,然后当该数据被发送后,这个副本就被数据链路层丢弃了)
本端的 UDP 简单地给来自用户的数据报安上它的 8 字节的首部以构成 UDP 数据报,然后递给 IP。IPv4 或 IPv6 给 UDP 数据报安上相应的 IP 首部以构成 IP 数据报,执行路由操作确定外出接口,然后直接将数据报或分片后加入数据链路层输出队列。如果某个 UDP 应用进程发送大数据报,那么它们相比 TCP 应用数据更有可能被分片,因为 TCP 会把应用数据划分成 MSS 大小的块,而 UDP 没有对等的方法。
从写一个 UDP 套接字的 write 调用成功返回标识所写的数据报或其所有片段已被加入到数据链路层的输出队列。如果该队列没有足够的空间存放着数据报或其中分片,内核会返回一个 ENOBUFS 错误给应用进程。