1. 概览
1
socket 链接双方
c 伪码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 服务端
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 端口 8080
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡 0.0.0.0
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 5);
while (1) {
// 等待客户端连接,返回新的 socket fd
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
// 用 client_fd 和客户端进行通信(读写数据)
recv(client_fd, buffer, sizeof(buffer), 0);
}
// 客户端
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// 指定源 ip & 端口 才需要用bind
// 否则connect 自选择port
// bind(sockfd, (struct sockaddr *)&local, sizeof(local));
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
fd & port ?
- fd (文件描述符)
概念:操作系统内核给进程分配的一个小整数,用来标识一个打开的文件、socket、管道等。
- port (端口号)
概念:TCP/UDP 协议栈在内核中的一个标识,用来区分同一 IP 上的不同服务。
1
addr.sin_port = htons(8080);
这里的 8080 是一个 全局的网络端口,只要某个进程已经绑定(bind)了它,别的进程就不能再用。
内核用 (本地 IP, 本地 port, 远端 IP, 远端 port, 协议) 这个五元组来区分一个 TCP 连接;而用户态进程只是通过 fd 来访问这个连接。
并发问题
从两个角度来看:
- 服务端
1
2
root>ulimit -n
1000000
单进程最多维持百万并发。(socket 编程, 四元组确定唯一fd)
- 客户端
1
server_addr.sin_port = htons(8080);
单个客户端 IP + 目标服务器端口最多能发起约 65535 个 TCP 连接(端口号 16 位,0-65535)
2. socket & fd 问题 概览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
应用层代码
┌───────────────────────────────┐
│ int fd = socket(...); │ ← 得到文件描述符 fd
│ bind(fd, ...) │ ← 绑定本地 IP/Port (服务端)
│ listen(fd, backlog) │ ← 监听端口
│ client_fd = accept(fd, ...) │ ← 接收客户端连接
└───────────────┬───────────────┘
│
▼
用户态文件描述符表
┌───────────────────────────────┐
│ fd=3 → 指向 server socket │
│ fd=4 → 指向 client socket │
└───────────────┬───────────────┘
│
▼
内核 socket 对象
┌───────────────────────────────┐
│ 协议族:AF_INET / AF_INET6 │
│ 类型:SOCK_STREAM / SOCK_DGRAM │
│ 本地 IP/Port │
│ 对端 IP/Port │
│ 发送缓冲区 / 接收缓冲区 │
│ 状态:LISTEN / ESTABLISHED ... │
└───────────────┬───────────────┘
│
▼
TCP 四元组(唯一标识一条 TCP 连接)
┌───────────────────────────────┐
│ (SrcIP, SrcPort, DstIP, DstPort) │
│ 例:192.168.1.20:52344 → 192.168.1.10:8080 │
└───────────────┬───────────────┘
│
▼
内核缓冲区 / TCP/IP 栈
┌───────────────────────────────┐
│ send(fd, buf) → 放入发送缓冲区 │
│ 内核拆分成 TCP segment │
│ 加入 IP 数据报 → 以太网帧 → 网卡发送 │
│ recv(fd, buf) ← 内核从接收缓冲区读取 │
└───────────────────────────────┘
│
▼
网络传输 & 对端处理
┌───────────────────────────────┐
│ 对端内核接收 TCP 报文 → 放入接收缓冲区 │
│ 对端应用通过 fd 读取数据 │
└───────────────────────────────┘
3. port 问题
1
一个端口可以对应多个 socket(内核对象),每个 socket 绑定在不同的四元组上。应用层看到的是多个 fd,每个 fd 都指向不同的 socket。
在 Linux 里,只要四元组不冲突,就可以 bind 同一个端口。
- 来自 bug 单:
1 2 3
wafd bind 后 set portreuse或addrreuse 导致 inet_bind_bucket 相关位被置位 后续别的进程通过 connect 找port时 发现此状态位被置位的 都不用导致每端口可占 通过bind 找port 不会做此状态位的检测
- UNIX socket 不需要端口,因为通信只在本地内核里,内核通过 socket 对象和 fd 就能区分各个通信端点,不需要像网络通信那样依赖 IP+port。
4. time_wait
TIME_WAIT 是 TCP 协议的一个标准状态,又叫 2MSL 等待状态。
出现在 主动关闭连接的一方
作用:
确保最后的 ACK 包能被对方收到,如果对方没收到,可以重发 FIN
避免端口号被立即复用导致旧数据干扰新连接