tags:
- Networks
Socket API and Transport Layer Protocol
我们先要明确传输层的作用是什么。网络协议栈有五层:应用层、传输层、网络层、数据链路层和物理层,每一层的职责都是为上层提供服务。所以,传输层存在的意义就是封装网络层,为应用层提供逻辑上的通信服务。
应用层进程会使用一个虚拟端口来请求使用传输层所提供的逻辑通信,规避掉底层传递信息的实现细节,使得应用开发者可以专注于应用逻辑。极大地简化了网络编程和应用开发。
在互联网中,我们用 IP 地址来标识一台主机,用端口号来标识主机上的某个特定服务或应用程序的编号,这里的端口号是虚拟的。端口号与设备的IP地址一起工作,IP地址负责定位设备,而端口号则负责定位设备上的具体应用程序。你可以用一个 pair(IP, Port) 来唯一地标识互联网上的通信端点。
我们举一个快递驿站的例子。我们把社区比作主机,把房子比作进程,每个社区都会有一个快递驿站负责社区间快递的邮递。而驿站只是一个快递寄存和分发的地方,也就是说驿站依托邮递公司把快递传输到不同社区中,随后快递寄存在社区的驿站中并分发不同的快递。
这里,快递驿站其实就都需要传输层协议,快递公司就相当于网络层协议。通过社区的地址(IP 地址),邮递公司可以把快递(数据)送到目的社区(主机)那里。随后,驿站会录入包裹信息并给不同的人发送信息进行包裹的分发。在传输层协议中,这一步被称为复用/解复用。
当一台主机上的多个进程需要通过网络发送数据时,传输层通过端口号标识不同进程的数据来源。传输层将来自不同端口的数据整合为段(segment)或数据报(datagram),并交给网络层处理,这一过程称为多路复用。
同样的,当传输层从网络层接收到数据时,它会通过解析协议头部中的端口号信息来确定数据的目标进程,然后通过 Socket 接口 将数据分发到对应的进程。这一过程称为多路解复用。
在应用层,系统为我们提供了操作窥视传输层和网络层的 API socket。Socket不同于端口号,它是在应用层对端口号的抽象与扩展。Socket结合了 IP 地址和端口号和协议,形成了网络通信的端点。Socket和端口号通过绑定函数来关联。
不同协议的 socket 也是不同的。在 UDP 协议中,socket 由 IP 和 端口号二元组所标识。而在 TCP 中,socket 是一个四元组,包括源 IP 地址、源端口号、目标 IP 地址 和 目标端口号。在一定程度上,socket 标识解释了不同协议的特点。
我们下面详细地给出 socket 接口的函数原型:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/*
Parameters:
1. Domain: address format;
- AF_INET: IPv4
- AF_INET6: IPv6
- AF_UNIX/AF_LOCAL: Unix domain sockets
- AF_PACKET: Low-level packet interface
- AF_NETLINK: Kernel/user-space communication
- AF_X25: ITU-T X.25 protocol
- AF_AX25: Amateur radio AX.25 protocol
- AF_BLUETOOTH: Bluetooth communication
- AF_CAN: Controller Area Network protocols
- AF_TIPC: Transparent Inter-process Communication
- AF_ALG: Cryptographic algorithms (Linux-only)
- AF_ECONET: Acorn Econet protocol
- AF_RDS: Reliable Datagram Sockets (cluster communication)
2. Type: what kind of data;
- SOCK_STREAM: Reliable, two-way, connection-oriented
- SOCK_DGRAM: Connectionless, datagram socket
- SOCK_RAW: Provides direct access to lower-layer protocols (IP headers)
- SOCK_SEQPACKET: Similar to SOCK_STREAM but preserves message boundaries
- SOCK_RDM: Reliable datagram messages (not widely supported)
- SOCK_PACKET: Obsolete, used for raw packets at the device driver level
3. Protocol: how data is transported; 0 for type inference
- IPPROTO_TCP: Transmission Control Protocol
- IPPROTO_UDP: User Datagram Protocol
- IPPROTO_RAW: Raw IP packets
- IPPROTO_ICMP: Internet Control Message Protocol (Network layer)
- IPPROTO_SCTP: Stream Control Transmission Protocol
- IPPROTO_IGMP: Internet Group Management Protocol (Network layer)
- IPPROTO_EGP: Exterior Gateway Protocol
- IPPROTO_PIM: Protocol Independent Multicast
- IPPROTO_IPV4
- IPPROTO_IPV6
Return value:
- Return a socketfd on success.
- -1 on failure.
*/
socket API 的第一个字段是用于定义套接字使用的通信域,也就是协议族。协议族决定了 socket API 的后续地址数据结构的格式和套接字能够进行的通信范围。常见的用于网络通信的字段选项有 AF_INET
, AF_INET6
。
AF_INET
用于定义一个 IPv4 的地址族,即基于 32 位 IP 地址的网络通信。使用时,我们会将 IPv4 的相关地址信息存储到结构体 struct sockaddr_in
中。
AF_INET6
用于定义一个 IPv6 的地址族,它和 AF_INET
的作用是一样的,只不过支持 128 位 IP 地址,地址空间大大扩展,可支持更多的设备。甚至有说法说你可以给地球上的每粒沙分配一个 IPV6 的地址。
第二个字段用来定义套接字如何传输数据,套接字的行为。这里我们介绍三种不同的套接字:SOCK_STREAM
, SOCK_DGRAM
和 SOCK_SEQPACKET
。我们拿一个表格比较它们的特性:
可靠性/顺序性 | 面向连接 | 拥塞控制/流控制 | 传输方式 | 消息边界 | |
---|---|---|---|---|---|
SOCK_STREAM | 支持 | 是 | 支持 | 连续的字节流 | 无 |
SOCK_DGRAM | 不支持 | 否 | 不支持 | 独立的数据报 | 有 |
SOCK_SEQPACKET | 支持 | 是 | 支持 | 独立的”消息流“ | 有 |
第三个参数用于指定具体的协议。这些协议中有传输层的协议,也有网络层的协议。一般,你可以将此位设置为 0
让系统自己推断选择哪个协议。但你也可以显式地制定出来。这里,我们关注三个和传输层相关的协议,TCP, UDP 和 SCTP。
UDP是最简单的传输层协议,它的唯一作用就是提供传输层的多路复用和解多路复用的功能。没有可靠的传输服务,也不保证数据有序到达。在建立 UDP 的网络连接时,UDP 也不维护连接,所以 UDP 是一个无连接的传输层协议。
IP 协议提供尽力而为的服务,而 UDP 实际上也并不提供说明服务。所以实际上 UDP 算得上一个最小的传输层协议,在 IP 的基础上提供尽力而为的服务。所以,你会看到 UDP 的 RFC 768l 文档实际上也非常短小,只有两页。
UDP 实际上好像什么也没有提供,复用和解复用是它的职责。除了传输层的职责,UDP 还会简单地校验传过来的数据,尽量避免 flip bits 的干扰。下面是 UDP header:
0 16 32
+----------------------+----------------------+
| Src Port (16 bits) | Dest Port (16 bits) |
+-----------------------+---------------------+
| Length (16 bits) | Checksum (16 bits) |
+----------------------+----------------------+
| |
| Contents... |
| |
+----------------------+----------------------+
在 UDP 头中,我们看到了四个字段:源端口、目的端口、长度、校验和。这四个字段各 16 字节。
源端口和目的端口不难理解,这是传输层协议用来实现多路复用/解复用功能的必要项。
UDP 是面向数据报传输的协议。UDP 的头中维护一个长度字段,这个字段用于指示整个数据报(即头部和数据)的总字节数。通过长度字段,你可以分辨一个 UDP 数据报的边界在哪里,这样,接收方可以准确地知道如何从接收到的字节流中提取完整的 UDP 数据报。
由于 UDP 天然保留消息边界,应用层无需担心出现粘包或拆包的问题。这与 TCP 不同,TCP 是面向流传输的协议,不保留消息边界,因此可能导致粘包或拆包问题,需要应用层协议进行额外处理。我们留在学习 TCP 的时候进行介绍。
UDP 校验和为传输层提供了一种简单的纠错机制,用于检查从源到目的的数据传输过程中是否有噪音污染。如果在传输过程中出现干扰,在 UDP 下,接收端就会直接丢弃该数据段,一定程度上避免了把错误的数据传给应用进程。
下面,我们来学习这种校验和是如何发挥作用的。UDP 在计算校验和时,会在 UDP 报文段的头部前加入一个伪头部(Pseudo header)。
0 16 32
+----------------------+----------------------+
| Source IP address |
+----------------------+----------------------+
| Destination IP address |
+-----------+----------+----------------------+
| Zero | Protocol | UDP Length |
+----------------------+----------------------+
UDP 校验和的计算过程十分简单,这种校验方式叫做反码求和。其过程如下:
简单举个例子,我们要将数据从 IP source:192.168.2.1,Port src:8080 传递到 IP dest:192.168.2.2,Port source:9090。这里的8位协议字段(protocol)是 17 ,表示UDP协议。再假设我们要传 4字节的数据字段(0x1234 55aa)。
小端方式表示的UDP segment如下:
Pseudo Header:
0-7bits | 8-15bits | 16-23bits | 24-31bits | |
---|---|---|---|---|
Source IP addr. | 0000 0001(1) | 0000 0010(2) | 1010 1000(168) | 1100 0000(192) |
Destination IP addr. | 0000 0010(2) | 0000 0010(2) | 1010 1000(168) | 1100 0000(192) |
Zero & Protocol & length | 0000 0000 | 0001 0001 | 0000 0100 | 0000 0000 |
UDP Header:
0-7bits | 8-15bits | 16-23bits | 24-31bits | |
---|---|---|---|---|
src port & dest port | 1001 0000 | 0001 1111 | 1000 0010 | 0010 0011 |
length & checksum | 0000 0100 | 0000 0000 | 0000 0000 | 0000 0000 |
Segment Body:
Body data | 0-7bits | 8-15bits | 16-23bits | 24-31bits |
---|---|---|---|---|
0x1234 55aa | 1010 1010 | 0101 0101 | 1000 0011 | 0001 0010 |
0000 0010 0000 0001 Pseudo Header Starts HERE
+1100 0000 1010 1000
————————————————————
1100 0010 1010 1001(SUM)
+0000 0010 0000 0010
————————————————————
1100 0100 1010 1011(SUM)
+1100 0000 1010 1000
————————————————————
1 1000 0101 0101 0011(SUM)
+0001 0001 0000 0000 +1
————————————————————
1001 0110 0101 0100(SUM)
+0000 0000 0000 0100
————————————————————
1001 0110 0101 1000(SUM)
+0001 1111 1001 0000 UDP Header Starts HERE
————————————————————
1011 0101 1110 1000(SUM)
+0010 0011 1000 0010
————————————————————
1101 1001 0110 1010(SUM)
+0000 0100 0000 0000
————————————————————
1101 1101 0110 1010(SUM)
+0101 0101 1010 1010 UDP Body Starts HERE
————————————————————
1 0011 0011 0001 0100(SUM)
+0001 0010 1000 0011 +1
————————————————————
0100 0101 1001 1000
^
————————————————————
1011 1010 0110 0111(CHECKSUM)
在接收端,传输层会将UDP segment中每16位相加并加上进位,最终与CHECKSUM相或,若结果不是0xFFFF(即全1)说明报文段出错,应舍弃。
我们发现,SOCK_DGRAM
和 UDP 极为匹配,所以我们用下面的方法来创建一个 UDP 的 socket :
// IPv4
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
相比 UCP ,TCP 给人的第一印象就是它是靠得住的传输层协议。因为它提供可靠、有序的数据传输,如此,应用程序就可以确保自己能够收到的数据是完整有序的。因此,HTTP超文本传输协议、FTP文件传输协议、SMTP电子邮件、DASH流媒体协议等都是在 TCP 传输层协议上实现的。
除了可靠性,TCP 还有许多特性。比如 TCP 维护点对点的连接(四元组),提供全双工的数据传输。面向流的传输服务(没有消息边界)。此外,TCP 还提供拥塞控制、流控制。我们下面一点点来介绍。你可以在 RFC 9293 获得更多关于 TCP 的知识。
UDP 实际上好像什么也没有提供,复用和解复用是它的职责。除了传输层的职责,UDP 还会简单地校验传过来的数据,尽量避免 flip bits 的干扰。下面是 UDP header 和 pseudo header (用于计算checksum):
0 16 32
+-------------------------------+--------------------------------+
| Source Port (16) | Destination Port (16) |
+-------------------------------+--------------------------------+
| Sequence Number (32) |
+-------------------------------+--------------------------------+
| Acknowledgment Number (32) |
+-------------------------------+--------------------------------+
|Header |Unused |C|E|U|A|P|R|S|F| |
|Length | |W|C|R|C|S|S|Y|I| Receiver Window Size (16) |
| (4) | (4) |R|E|G|K|H|T|N|N| |
+-------------------------------+--------------------------------+
| Internet Checksum (16) | Urgent Data Pointer (16) |
+-------------------------------+--------------------------------+
| Options (0-40 bytes, as needed) |
+-------------------------------+--------------------------------+
| Data (payload) |
+-------------------------------+--------------------------------+
+-------------------------------+--------------------------------+
| Source Address |
+----------------------------------------------------------------+
| Destination Address |
+----------------------------------------------------------------+
| zero | PTCL | TCP Length |
+----------------------------------------------------------------+
IPv4 TCP Pseudo Header
TCP 的头长度不定,在 20-60 字节之间。因为 TCP 除了固定的 20 字节的头之外,还提供 Options 字段来支持一些时间戳、窗口缩放、选择性确认等高级功能。这一部分在 Header Length 中提供。Header length 字段有 4 bits,可以表示的最大值是 15 。单位是 4 字节,所以 TCP 头最大为:
TCP 是面向流传输的协议,相比之下,UDP 我们说是面向数据报(datagram) 传输的协议。在之前,我们提到消息边界,那么没有消息边界的流传输会给我们带来说明问题呢?
假如我们分别发送两次消息 Hello1
和 Hello2
,在接收端的 TCP 传输层缓冲区中,可能将这两次发送的数据合并为 Hello1Hello2
后再送给应用。而因为 UDP 头中维护一个 Length 字段,所以使用 UDP 我们反而不用担心这种粘包的问题。
这么做有什么好处么?流传输不会把数据分割为独立的数据包,这种设计可以让发送端自由决定如何组织发送数据,而接收端也可以以大小任意的块来读取数据,带来了更大的灵活性。而且因为没有消息边界, TCP 的流传输可以自然的适应各类场景。这就需要认为地定义消息边界。
想要避免粘包或半包(一条 send()
消息被拆分成多次 recv()
)的问题,我们就需要通过在应用层定义协议来明确消息边界。常见的实现方法有:
无论是 TCP 还是 UDP,每一次最大传输的数据大小都受到 MSS 最大段大小的限制,而 MSS 又受到 MTU 最大传输单元的限制。这不难理解,因为网络中的数据是经过层层封装后以类似“包裹”一样来传输的。而链路层的帧最大受 MTU 大小的限制。由此,MSS 就等于 MTU 减去 TCP/IP 头部长度。
通常而言,MTU 大小是 1500 字节,因而 MSS 典型值就是1460字节 (TCP/IP) 或是 1472 (UDP/IP)。
现在,我们来学习一下 TCP 的可靠性和按序到达的数据传输是如何实现的。
假如我的服务器运行在192.168.0.1:8080,那么下面的几个TCP连接复用同一个port么?只不过不是同一个socket
192.168.0.1:8080<->192.168.1.1:5050
192.168.0.1:8080<->192.168.2.1:6060
192.168.0.1:8080<->192.168.3.1:7070
+---------+ ----------------\ active OPEN
| CLOSED | \ -----------
+---------+ <------------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
rcv RST (note 1) +---------+ CLOSE \ \
-------------------->| LISTEN | ---------- | |
/ +---------+ delete TCB | |
/ rcv SYN | | SEND | |
/ ----------- | | ------- | V
+--------+ snd SYN,ACK / \ snd SYN +--------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd SYN,ACK | |
| |------------------ -------------------| |
+--------+ rcv ACK of SYN \ / rcv SYN,ACK +--------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<---------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +----------+
|FINWAIT-2| | CLOSING | | LAST-ACK |
+---------+ +---------+ +----------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +----------+
-------------------->|TIME-WAIT|------------------->| CLOSED |
+---------+ +----------+
TCP fast retransmit
QUIC has no HOL, because it's stream parallelism
因为 UDP 不需要握手和对数据包进行排序,所以使用UDP时,应用上层能够更快的拿到数据,而不用等待。