socket-tcp-udp
Socket、TCP、UDP 详解
总览关系图
应用层 (Application) HTTP / FTP / DNS / 自定义协议 ...
| |
传输层 (Transport) TCP UDP
| |
套接字层 (Socket) Socket API (编程接口)
|
网络层 (Network) IP
|
链路层 (Link) 以太网 / WiFi / PPP ...
核心观点: Socket 是操作系统提供给应用层的编程抽象/API,TCP 和 UDP 是传输层的两种协议实现。Socket 是对这两类协议的统一操作接口。
1. 分层模型
1.1 OSI 七层模型
| 层 | 名称 | 典型协议/标准 | 角色 |
|----|------|-------------|------|
| 7 | 应用层 | HTTP, FTP, SMTP, DNS | 为用户应用提供网络服务 |
| 6 | 表示层 | SSL/TLS, JPEG, ASCII | 数据格式转换、加密 |
| 5 | 会话层 | NetBIOS, RPC | 管理会话、建立/维持/终止连接 |
| 4 | 传输层 | TCP, UDP | 端到端通信、可靠性、流量控制 |
| 3 | 网络层 | IP, ICMP, ARP | 路由、寻址、分组转发 |
| 2 | 数据链路层 | Ethernet, WiFi (802.11), PPP | 相邻节点间帧传输 |
| 1 | 物理层 | 双绞线、光纤、无线电 | 比特流传输 |
1.2 TCP/IP 四层模型(实际使用)
| 层 | 对应 OSI | 典型协议 |
|----|---------|---------|
| 应用层 | L5-L7 | HTTP, DNS, SSH, TLS (在 TCP 之上) |
| 传输层 | L4 | TCP, UDP, SCTP |
| 网络层 | L3 | IP, ICMP, IGMP |
| 网络接口层 | L1-L2 | Ethernet, WiFi, PPP |
2. TCP(Transmission Control Protocol)
2.1 核心特性
| 特性 | 说明 |
|------|------|
| 面向连接 | 通信前三次握手建立连接,结束后四次挥手断开 |
| 可靠传输 | ACK 确认、超时重传、序列号去重 |
| 有序 | 接收端按序列号重组,保证数据顺序 |
| 全双工 | 双方可同时发送数据 |
| 字节流 | 无消息边界,应用层自行定界 |
| 流量控制 | 滑动窗口,防接收方过载 |
| 拥塞控制 | 慢启动、拥塞避免、快重传、快恢复 |
2.2 三次握手
Client Server
| |
|---- SYN (seq=x) ------------------------>| SYN_SENT -> SYN_RCVD
|<--- SYN+ACK (seq=y, ack=x+1) -----------| SYN_RCVD -> ESTABLISHED
|---- ACK (seq=x+1, ack=y+1) ------------>| ESTABLISHED
| |
1. 客户端发送 SYN(同步序列号),进入 SYN_SENT
2. 服务端回复 SYN + ACK,进入 SYN_RCVD
3. 客户端回复 ACK,双方进入 ESTABLISHED
为什么是三次而不是两次? 防止历史旧连接的 SYN 干扰。两次握手无法区分是旧连接还是新请求,三次握手可以通过序列号检测失效的 SYN(RFC 793)。
2.3 四次挥手
Client Server
| |
|---- FIN (seq=u) ----------------------->| FIN_WAIT_1 -> CLOSE_WAIT
|<--- ACK (ack=u+1) ----------------------| FIN_WAIT_2
| |
|<--- FIN (seq=v) ------------------------| LAST_ACK
|---- ACK (ack=v+1) --------------------->| TIME_WAIT -> CLOSED
| |
| 2MSL 等待 (TIME_WAIT) |
1. 主动关闭方发 FIN,进入 FIN_WAIT_1
2. 被动关闭方回 ACK,进入 CLOSE_WAIT(主动方进入 FIN_WAIT_2)
3. 被动关闭方发 FIN,进入 LAST_ACK
4. 主动关闭方回 ACK,进入 TIME_WAIT(等 2MSL 后关闭)
为什么 TIME_WAIT 要等 2MSL? 保证最后一个 ACK 能被对方收到,否则对方重发 FIN 时已关闭的端口可能被新连接误用。
2.4 核心机制
滑动窗口(流量控制)
发送方 接收方
| |
|<--- 窗口大小 = 4096 ---| 接收方通告可用缓冲区
|---- 数据[seq=1-2048] ->|
|---- 数据[seq=2049-4096]|
|<--- ACK(4097, win=0) --| 缓冲区满,暂停发送
|<--- ACK(4097, win=2048)| 应用读取后,恢复发送
|---- 数据[seq=4097-6144]|
- 接收方通过 TCP 头
window字段告知剩余缓冲区大小 - 发送方窗口不能超过接收方通告值,避免接收方溢出
- 零窗口时发送方停止发送,定期发窗口探测包
拥塞控制(4 阶段)
拥塞窗口 (cwnd)
^
| 慢启动 拥塞避免 检测到丢包
| (指数增长) (线性增长) (快重传+快恢复)
| / ___/
| / ____/
| / ________/
|/_____/
+----------------------------------------> 时间
1. 慢启动 (Slow Start): cwnd 从 1 开始,每收到一个 ACK 翻倍(指数增长),到 ssthresh 为止
2. 拥塞避免 (Congestion Avoidance): 每 RTT 增加 1(线性增长),直到丢包
3. 快重传 (Fast Retransmit): 收到 3 个重复 ACK 立即重传,不等超时
4. 快恢复 (Fast Recovery): 将 ssthresh 设为当前 cwnd/2,cwnd 降为 ssthresh + 3,进入拥塞避免
2.5 TCP 头部格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口 | 目的端口 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号 (seq) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号 (ack) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据偏移 | 保留 |N|C|E|U|A|P|R|S|F| 窗口大小 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 | 紧急指针 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项 (可选) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
标志位:
- U: URG(紧急指针有效)
- A: ACK(确认号有效)
- P: PSH(推送,立即交付给应用层)
- R: RST(重置连接)
- S: SYN(同步序列号,建连用)
- F: FIN(结束连接)
2.6 应用场景
| 场景 | 原因 |
|------|------|
| HTTP/HTTPS | 需要完整、正确地传输网页内容 |
| FTP | 文件传输不能出错 |
| SSH | 远程终端不能丢字符 |
| SMTP/IMAP | 邮件内容完整 |
| 数据库连接 | 查询结果必须准确 |
| Kafka(可选) | 生产端可靠投递 |
2.7 局限性
- 队头阻塞 (Head-of-Line Blocking): 前面丢包,后面即使到了也要等重传
- 握手延迟: 建连最少 1 RTT
- 协议开销大: TCP 头 20-60 字节
- 无法用于广播/多播: TCP 仅点对点
3. UDP(User Datagram Protocol)
3.1 核心特性
| 特性 | 说明 |
|------|------|
| 无连接 | 不握手,直接发 |
| 不可靠 | 不保证送达、不重传 |
| 无序 | 可能乱序到达 |
| 面向报文 | 保留消息边界,一次 recvfrom 对应一个完整报文 |
| 无拥塞控制 | 不会因为网络拥塞减少发送 |
| 轻量 | 头仅 8 字节 |
3.2 UDP 头部格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口 | 目的端口 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 长度 | 校验和 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 长度 = UDP 头(8 字节)+ 数据
- 校验和可选(IPv4 下可为 0,IPv6 下强制)
3.3 应用场景
| 场景 | 原因 |
|------|------|
| DNS 查询 | 一条请求对应一条响应,丢了自己重试 |
| DHCP | 获取 IP 地址时还没有 TCP 连接 |
| RTP/RTSP(音视频流) | 可以容忍少量丢包,但延迟要低 |
| 在线游戏 | 位置信息、操作指令优先,丢几个包无所谓 |
| QUIC (HTTP/3) | 基于 UDP,应用层解决可靠性 |
| VoIP | 实时通话,不能等重传 |
| SNMP | 网络设备监控,轻量 |
3.4 UDP 变体与上层方案
| 名称 | 说明 |
|------|------|
| QUIC | Google 设计,UDP + TLS + 可靠传输,HTTP/3 底层。解决了 TCP 的队头阻塞 |
| KCP | 可靠 UDP 协议,比 TCP 快,适用游戏场景 |
| UDT | 基于 UDP 的可靠传输,用于大文件、高速网络 |
| RUDP | 各种可靠 UDP 实现的统称 |
| DTLS | 基于 UDP 的 TLS,用于 WebRTC |
4. Socket(套接字抽象层)
4.1 什么是 Socket
Socket 是操作系统提供的网络编程抽象接口,它不是协议。Socket 的概念最早来自 BSD Unix(1983 年 4.2BSD)。
Socket 抽象将传输层的差异(TCP vs UDP)封装为统一的文件描述符操作模型:open-read-write-close。
4.2 Socket 的类型
| 类型 | 常量 | 说明 |
|------|------|------|
| 流式套接字 | SOCK_STREAM | TCP 协议 |
| 数据报套接字 | SOCK_DGRAM | UDP 协议 |
| 原始套接字 | SOCK_RAW | 直接操作 IP 层,可构造任意协议头 |
| 顺序数据包 | SOCK_SEQPACKET | SCTP 协议 |
4.3 核心 API
TCP Socket 完整生命周期
TCP Client TCP Server
| |
| socket() | socket()
| | bind()
| | listen()
| | accept() (阻塞)
| connect() |
|===============================| (三次握手)
| send() | recv()
| recv() | send()
|===============================| (四次挥手)
| close() | close()
关键系统调用详解
| 调用 | TCP | UDP |
|------|-----|-----|
| socket(domain, type, protocol) | socket(AF_INET, SOCK_STREAM, 0) | socket(AF_INET, SOCK_DGRAM, 0) |
| bind(sockfd, addr, addrlen) | 绑定本地地址和端口 | 绑定本地地址和端口 |
| listen(sockfd, backlog) | 进入被动监听,backlog 为连接队列长度 | 不需要 |
| accept(sockfd, addr, addrlen) | 摘取已完成连接,返回新的 socket fd | 不需要 |
| connect(sockfd, addr, addrlen) | 发起三次握手,阻塞直到连接建立 | 记录对方地址,也可不调直接 sendto |
| send(sockfd, buf, len, flags) | 发送字节流 | 发送,但无连接状态 |
| recv(sockfd, buf, len, flags) | 接收字节流 | 接收,但只能从已 connect 的地址收 |
| sendto(sockfd, buf, len, flags, dest, destlen) | 一般不使用 | UDP 标准发送方式,指定目标 |
| recvfrom(sockfd, buf, len, flags, src, srclen) | 一般不使用 | UDP 标准接收方式,获取源地址 |
| close(sockfd) | 发起 FIN/直接 RST 断开 | 释放 socket |
| shutdown(sockfd, how) | 半关闭(关闭读/写/或都关) | 意义不大 |
4.4 Socket 地址结构
// IPv4 (struct sockaddr_in)
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 端口 (网络字节序)
struct in_addr sin_addr; // IP 地址
char sin_zero[8]; // 填充
};
// IPv6 (struct sockaddr_in6)
struct sockaddr_in6 {
sa_family_t sin6_family; // AF_INET6
in_port_t sin6_port; // 端口
uint32_t sin6_flowinfo; // 流标签
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 范围 ID
};
// 通用地址结构
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 协议相关数据
};
4.5 Socket 选项
可以通过 setsockopt 配置大量行为:
| 选项 | 层级 | 说明 |
|------|------|------|
| SO_REUSEADDR | SOL_SOCKET | 重用 TIME_WAIT 状态的端口 |
| SO_REUSEPORT | SOL_SOCKET | 多进程/线程绑定同一端口 |
| SO_KEEPALIVE | SOL_SOCKET | 启用 TCP 保活探测 |
| SO_LINGER | SOL_SOCKET | close 时的行为控制(立刻关/等数据发完) |
| SO_RCVBUF | SOL_SOCKET | 接收缓冲区大小 |
| SO_SNDBUF | SOL_SOCKET | 发送缓冲区大小 |
| SO_NONBLOCK | (fcntl) | 非阻塞模式 |
| TCP_NODELAY | IPPROTO_TCP | 禁用 Nagle 算法 |
| TCP_QUICKACK | IPPROTO_TCP | 立即发 ACK |
| TCP_CORK | IPPROTO_TCP | 攒数据一起发 |
4.6 Socket 的几种模式
| 模式 | 特点 | 典型框架 |
|------|------|---------|
| 阻塞 I/O | 线程调用 recv 后挂起等数据,简单但并发差 | 简单程序 |
| 非阻塞 I/O | recv 立即返回 EWOULDBLOCK,应用轮询 | poll 循环 |
| I/O 多路复用 | 单线程同时监控多个 fd,有数据时通知 | select, poll, epoll (Linux), kqueue (macOS), IOCP (Windows) |
| 信号驱动 I/O | fd 就绪时发 SIGIO 信号 | 较少使用 |
| 异步 I/O (AIO) | 系统完成操作后通知,不需要应用等待 | Linux AIO, Windows IOCP |
多路复用对比
| 机制 | 平台 | O(1) | 最大 fd | 边缘触发 | 内部实现 |
|------|------|------|---------|---------|---------|
| select | 全平台 | 否 | 1024 (FD_SETSIZE) | 否 | 轮询位图 |
| poll | 全平台 | 否 | 无硬限制 | 否 | 轮询链表 |
| epoll | Linux | 是 | 无硬限制 | 是 | 红黑树 + 回调 |
| kqueue | macOS/BSD | 是 | 无硬限制 | 是 | 事件队列 |
| IOCP | Windows | 是 | 无硬限制 | - | 完成端口 |
5. TCP vs UDP 详细对比
| 维度 | TCP | UDP |
|------|-----|-----|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠,ACK + 重传 | 不可靠,不确认 |
| 有序 | 保证有序 | 可能乱序 |
| 消息边界 | 字节流,无边界 | 保留报文边界 |
| 流量控制 | 滑动窗口 | 无 |
| 拥塞控制 | 慢启动、拥塞避免等 | 无 |
| 数据包大小 | 无限制(基于流) | 单报文 ≤ 65535 字节(IP 限制) |
| 头部大小 | 20-60 字节 | 8 字节 |
| 组播/广播 | 不支持 | 支持 |
| 延迟 | 高(握手、ACK、拥塞控制) | 低(即发即走) |
| 吞吐量 | 调节(拥塞控制) | 可跑满带宽 |
| 应用开销 | 低(系统处理可靠性) | 高(如需可靠需应用层实现) |
| 典型场景 | 网页、文件、邮件 | DNS、音视频、游戏 |
6. 状态机
6.1 TCP 状态机
+---------+
| CLOSED |
+---------+
| |
主动开 | | 被动开
v |
+----------+|
| SYN_SENT ||
+----------+|
| |
SYN+ACK | | SYN
v v
+----------++
| SYN_RCVD |
+-----------+
| ACK
v
+---------------+
| ESTABLISHED |
+---------------+
| |
主动关 | | 被动关
v v
+---------+ +----------+
|FIN_WAIT1| |CLOSE_WAIT|
+---------+ +----------+
| |
ACK | | FIN
v v
+---------+ +--------+
|FIN_WAIT2| |LAST_ACK|
+---------+ +--------+
| |
FIN | | ACK
v v
+----------+ +------+
| TIME_WAIT| |CLOSED|
+----------+ +------+
| 2MSL
v
+------+
|CLOSED|
+------+
6.2 UDP 无状态
UDP 没有连接状态机。一个 UDP socket 本质上只是 "IP:端口" 的绑定,随时可以收发,不需要维护状态。
7. 常见问题
7.1 TCP 粘包问题
问题: TCP 是字节流协议,多次 send 可能合并为一次 recv,或一次 send 被拆分为多次 recv。
解决方案(应用层定界):
| 方式 | 说明 | 示例 |
|------|------|------|
| 固定长度 | 每个消息等长 | 4 字节指令码 |
| 长度前缀 | 头部 2/4 字节记录消息体长度 | HTTP/2 Frame |
| 分隔符 | 用特殊标记分割 | Line-based (telnet) |
| 自描述协议 | Protobuf/Thrift 等自带长度编码 | gRPC |
7.2 UDP 最大报文长度
- UDP 报文总长 ≤ 65535 字节(UDP 头部 16 位长度字段)
- IP 层通常分片阈值 1500(以太网 MTU)
- 超过 MTU 的 UDP 报文会被 IP 分片,一个分片丢了整包作废
- 推荐 UDP 数据 ≤ 1472 字节(1500 - 20 IP 头 - 8 UDP 头)
7.3 TCP 快速打开 (TFO - TCP Fast Open)
RFC 7413,允许在 SYN 中携带数据,节省 1 RTT:
普通 TCP: SYN ---- SYN+ACK ---- ACK+数据 ----
TFO: SYN+数据 ---- SYN+ACK+数据 ----
服务端通过 cookie 验证客户端,首次连接不能使用。
7.4 端口与套接字关系
- 一个 TCP 连接由四元组唯一标识:
(src_ip, src_port, dst_ip, dst_port) - 一个 UDP socket 由二元组标识:
(ip, port) - 因此 UDP socket 可以收发多个对端的报文,TCP socket 仅对应一个连接
8. 代码示例
8.1 TCP Server (Java)
import java.io.*;
import java.net.*;
public class TcpServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
System.out.println("Server listening on 8080");
while (true) {
try (Socket client = server.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(
client.getOutputStream(), true)) {
String line = in.readLine();
System.out.println("Received: " + line);
out.println("Echo: " + line);
}
}
}
}
8.2 TCP Client (Java)
import java.io.*;
import java.net.*;
public class TcpClient {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 8080);
PrintWriter out = new PrintWriter(
socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
out.println("Hello Server");
System.out.println("Response: " + in.readLine());
}
}
}
8.3 UDP Socket (Java)
import java.net.*;
public class UdpExample {
public static void main(String[] args) throws Exception {
// Server
Thread server = new Thread(() -> {
try (DatagramSocket sock = new DatagramSocket(9090)) {
byte[] buf = new byte[1024];
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
sock.receive(pkt);
String msg = new String(pkt.getData(), 0, pkt.getLength());
System.out.println("Server received: " + msg);
byte[] resp = ("Echo: " + msg).getBytes();
sock.send(new DatagramPacket(resp, resp.length,
pkt.getAddress(), pkt.getPort()));
} catch (Exception e) { e.printStackTrace(); }
});
server.start();
Thread.sleep(100);
// Client
try (DatagramSocket sock = new DatagramSocket()) {
byte[] data = "Hello UDP".getBytes();
sock.send(new DatagramPacket(data, data.length,
InetAddress.getByName("localhost"), 9090));
byte[] buf = new byte[1024];
DatagramPacket pkt = new DatagramPacket(buf, buf.length);
sock.receive(pkt);
System.out.println("Client received: " +
new String(pkt.getData(), 0, pkt.getLength()));
}
}
}
8.4 TCP Server (C, POSIX API)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 10);
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd,
(struct sockaddr *)&client_addr, &len);
char buf[1024] = {0};
read(client_fd, buf, 1024);
write(client_fd, buf, strlen(buf));
close(client_fd);
}
close(server_fd);
}
8.5 UDP Server (C)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(9090),
.sin_addr.s_addr = INADDR_ANY
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
char buf[1024];
struct sockaddr_in client;
socklen_t len = sizeof(client);
recvfrom(fd, buf, 1024, 0, (struct sockaddr *)&client, &len);
sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&client, len);
close(fd);
}
8.6 TCP Server with epoll (Linux)
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_EVENTS 1024
#define PORT 8080
int set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = INADDR_ANY
};
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, SOMAXCONN);
int epoll_fd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = server_fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
struct epoll_event events[MAX_EVENTS];
while (1) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == server_fd) {
int client_fd = accept(server_fd, NULL, NULL);
set_nonblock(client_fd);
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
} else {
char buf[4096];
int r = read(events[i].data.fd, buf, sizeof(buf));
if (r <= 0) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
} else {
write(events[i].data.fd, buf, r);
}
}
}
}
}
9. 工具与调试
常用网络命令
| 命令 | 用途 |
|------|------|
| tcpdump -i eth0 port 80 | 抓包分析 |
| ss -tpan | 查看 TCP 连接状态 |
| netstat -s | 协议层统计 |
| lsof -i :8080 | 查看占用端口的进程 |
| nc -zv host port | 测试端口是否开放 |
| iperf3 -s / -c | 吞吐量测试 |
| telnet host port | 手动发送 TCP 数据 |
| ping | 测试网络连通性 (ICMP) |
| traceroute | 路由路径追踪 |
| dig +short example.com | DNS 查询 (UDP) |
Wireshark 过滤常用表达式
tcp.port == 80 # 过滤 TCP 80 端口
tcp.flags.syn == 1 # SYN 包
tcp.analysis.retransmission # 重传包
tcp.analysis.fast_retransmission # 快重传
tcp.stream eq 0 # 按 TCP 流过滤
udp.port == 53 # DNS (UDP)
ip.addr == 192.168.1.1 # 按 IP 过滤
10. 深入阅读
- RFC 793 — TCP 协议规范
- RFC 1122 — 主机通信需求
- RFC 5681 — TCP 拥塞控制
- RFC 7413 — TCP Fast Open
- RFC 9000 — QUIC(基于 UDP 的下一代传输协议)
- 《TCP/IP 详解 卷1:协议》 — W. Richard Stevens
- 《Unix 网络编程 卷1:套接字联网 API》 — W. Richard Stevens
- 《计算机网络:自顶向下方法》 — Kurose & Ross
总结
Socket = 系统提供的 API 抽象层
|
|--- SOCK_STREAM (type) ---> TCP ---> 可靠、有序、面向连接
|
|--- SOCK_DGRAM (type) ---> UDP ---> 不可靠、无连接、低延迟
|
|--- SOCK_RAW (type) ---> IP/自定义协议
TCP 和 UDP 没有好坏之分,选择取决于业务需求:
- 需要可靠传输 + 不在乎额外的延迟 → TCP
- 需要低延迟 + 能容忍丢包 → UDP
- 需要低延迟 + 需要可靠 → QUIC (UDP + 应用层可靠性)