C高级:关于UDP和TCP的学习记录
在网络编程中,TCP 和 UDP 是两种最核心的传输层协议。它们为应用程序提供了不同的数据传输方式——TCP 提供可靠的、面向连接的服务,而 UDP 提供无连接的、尽最大努力交付的服务。本文将从概念原理到实际代码,系统讲解 UDP 和 TCP 的含义、作用和使用方法,最后做一个全面的对比。以下是我在职坐标的学习记录:
一、UDP 的概念及其作用
1.1 什么是 UDP
UDP(User Datagram Protocol,用户数据报协议)是 OSI 参考模型中传输层的一种协议。它是一种无连接的协议,发送方在发送数据之前不需要与接收方建立连接,直接把数据报"扔"出去就完事了。
可以这样理解:UDP 就像寄明信片——你写好内容、贴上邮票、写上地址,直接丢进邮筒就行了。你不需要提前和收件人"建立联系",邮局也不会给你回执确认对方是否收到。至于对方能不能收到、收到的顺序对不对,UDP 都不保证。
1.2 UDP 的核心特点
- 无连接:发送数据前不需要建立连接,不需要三次握手。
- 不可靠:不保证数据一定到达、不保证数据按序到达、不保证数据不重复。如果网络丢包,UDP 不会重传。
- 面向报文:UDP 对应用层传下来的数据不做拆分也不做合并,而是直接封装成 UDP 数据报发送。接收方也是整块接收,保留了报文的边界。
- 没有拥塞控制:不管网络多拥堵,UDP 都会按照应用层给的速度发送数据。
- 头部开销小:UDP 头部只有 8 字节(源端口、目的端口、长度、校验和),相比 TCP 的 20~60 字节头部要轻量得多。
- 支持一对一、一对多、多对多:UDP 支持单播、组播和广播。
1.3 UDP 的作用与适用场景
正因为 UDP 轻量、快速的特点,它特别适合以下场景:
- 实时性要求高的应用:视频会议、在线直播、语音通话等。这些场景对延迟敏感,偶尔丢一帧画面比延迟几百毫秒更可接受。
- 简单请求-响应模型:DNS 查询、NTP 时间同步等。一次请求一次响应,不需要建立连接的开销。
- 游戏:多人在线游戏中玩家位置、操作的同步,丢一条旧的位置信息没关系,下一条马上就来了。
- 广播/组播:局域网设备发现、流媒体分发等。
1.4 UDP 通信流程
客户端 服务器
│ │
├─ socket() 创建套接字 ├─ socket() 创建套接字
│ ├─ bind() 绑定地址和端口
│ │
├─ sendto() 发送数据 ──────────→ ├─ recvfrom() 接收数据
│ │
├─ recvfrom() 接收响应 ←──────── ├─ sendto() 发送响应
│ │
├─ close() ├─ close()
二、如何使用 UDP
2.1 核心函数
UDP 编程主要用到以下几个函数:
| 函数 | 作用 |
|---|---|
socket() |
创建套接字 |
bind() |
绑定本地地址和端口(通常只有服务器需要) |
sendto() |
向指定地址发送数据 |
recvfrom() |
接收数据并获取发送方地址 |
close() |
关闭套接字 |
2.2 socket() —— 创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
| 参数 | 类型 | 说明 |
|---|---|---|
domain |
int |
协议族(地址族)。常用 AF_INET(IPv4)或 AF_INET6(IPv6)。 |
type |
int |
套接字类型。UDP 使用 SOCK_DGRAM(数据报套接字)。 |
protocol |
int |
具体协议。通常传 0,表示自动选择默认协议(对于 SOCK_DGRAM 就是 UDP)。也可以显式传 IPPROTO_UDP。 |
返回值: 成功返回套接字文件描述符(非负整数),失败返回 -1。
// 创建 UDP 套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket failed");
}
2.3 bind() —— 绑定地址和端口
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| 参数 | 类型 | 说明 |
|---|---|---|
sockfd |
int |
socket() 返回的套接字描述符。 |
addr |
const struct sockaddr * |
指向地址结构体的指针,包含要绑定的 IP 地址和端口号。通常使用 struct sockaddr_in(IPv4),然后强转为 struct sockaddr *。 |
addrlen |
socklen_t |
地址结构体的大小,通常传 sizeof(struct sockaddr_in) 或 sizeof(struct sockaddr)。 |
struct sockaddr_in 结构体:
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 协议族,固定为 AF_INET
in_port_t sin_port; // 端口号(网络字节序,用 htons() 转换)
struct in_addr sin_addr; // IP 地址(网络字节序)
// ...
};
常用 IP 地址设置方式:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080); // 端口 8080
// 方式 1:绑定所有网卡的 IP(服务器常用)
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 等价于 0.0.0.0
// 方式 2:绑定指定 IP
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
// 方式 3:绑定回环地址(本机测试)
servaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 等价于 127.0.0.1
2.4 sendto() —— 发送数据
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
| 参数 | 类型 | 说明 |
|---|---|---|
sockfd |
int |
套接字描述符。 |
buf |
const void * |
要发送的数据缓冲区指针。 |
len |
size_t |
要发送的数据长度(字节数)。 |
flags |
int |
发送标志,通常传 0。 |
dest_addr |
const struct sockaddr * |
目标地址(对方的 IP + 端口)。 |
addrlen |
socklen_t |
目标地址结构体的大小。 |
返回值: 成功返回实际发送的字节数,失败返回 -1。
2.5 recvfrom() —— 接收数据
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
| 参数 | 类型 | 说明 |
|---|---|---|
sockfd |
int |
套接字描述符。 |
buf |
void * |
接收数据的缓冲区指针。 |
len |
size_t |
缓冲区的最大容量(字节数)。 |
flags |
int |
接收标志,通常传 0。 |
src_addr |
struct sockaddr * |
输出参数,用于获取发送方的地址信息(IP + 端口)。如果不需要可以传 NULL。 |
addrlen |
socklen_t * |
输入输出参数。输入时为 src_addr 缓冲区大小,输出时为实际地址长度。不需要时传 NULL。 |
返回值: 成功返回实际接收到的字节数,对端关闭连接时返回 0,失败返回 -1。
2.6 完整示例:UDP 服务器和客户端
UDP 服务器(udp_server.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define BUF_SIZE 1024
int main() {
int sockfd;
char buf[BUF_SIZE];
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
// 1. 创建 UDP 套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
// 3. 绑定地址和端口
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("[服务器] UDP 服务器已启动,监听端口 %d...\n", PORT);
// 4. 循环接收和响应
while (1) {
cliaddr_len = sizeof(cliaddr);
// 接收数据
int n = recvfrom(sockfd, buf, BUF_SIZE - 1, 0,
(struct sockaddr *)&cliaddr, &cliaddr_len);
if (n == -1) {
perror("recvfrom failed");
continue;
}
buf[n] = '\0';
// 打印客户端信息
char cli_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
printf("[服务器] 收到来自 %s:%d 的消息: %s\n",
cli_ip, ntohs(cliaddr.sin_port), buf);
// 发送响应
const char *response = "服务器已收到你的消息!";
sendto(sockfd, response, strlen(response), 0,
(struct sockaddr *)&cliaddr, cliaddr_len);
}
close(sockfd);
return 0;
}
UDP 客户端(udp_client.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUF_SIZE 1024
int main() {
int sockfd;
char buf[BUF_SIZE];
struct sockaddr_in servaddr;
socklen_t servaddr_len = sizeof(servaddr);
// 1. 创建 UDP 套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
// 3. 发送数据
const char *msg = "Hello, UDP Server!";
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&servaddr, sizeof(servaddr));
printf("[客户端] 已发送: %s\n", msg);
// 4. 接收响应
int n = recvfrom(sockfd, buf, BUF_SIZE - 1, 0,
(struct sockaddr *)&servaddr, &servaddr_len);
if (n > 0) {
buf[n] = '\0';
printf("[客户端] 收到响应: %s\n", buf);
}
// 5. 关闭套接字
close(sockfd);
return 0;
}
编译运行:
gcc udp_server.c -o udp_server
gcc udp_client.c -o udp_client
# 终端 1:运行服务器
./udp_server
# 终端 2:运行客户端
./udp_client
三、TCP 的概念及其作用
3.1 什么是 TCP
TCP(Transmission Control Protocol,传输控制协议)是 OSI 参考模型中传输层的另一种协议。它是一种面向连接的、可靠的、基于字节流的传输协议。互联网上绝大多数需要可靠数据传输的应用(HTTP、HTTPS、FTP、SSH 等)都基于 TCP。
可以这样理解:TCP 就像打电话——你需要先拨号(三次握手建立连接),对方接听后才能通话(传输数据),通话结束后双方挂断(四次挥手断开连接)。整个过程中,双方都能确认对方说的话(确认机制),没听清还能让对方重复(重传机制)。
3.2 TCP 的核心特点
- 面向连接:通信前必须通过"三次握手"建立连接,通信结束后通过"四次挥手"断开连接。
- 可靠传输:通过序列号、确认应答(ACK)、超时重传、校验和等机制保证数据完整、按序、无差错地到达对方。
- 面向字节流:TCP 把数据看成无结构的字节流,不保留消息边界。发送方发送了 100 字节,接收方可能分多次接收(如 30 + 70),也可能一次读完。
- 流量控制:通过滑动窗口机制,接收方可以告诉发送方"我还能接收多少数据",防止发送方发太快把接收方淹没。
- 拥塞控制:当网络拥堵时,TCP 会自动降低发送速率,避免加剧网络拥塞。
- 全双工通信:连接建立后,双方可以同时发送和接收数据。
- 点对点:TCP 连接只能是两个端点之间的(一对一),不支持广播和组播。
3.3 TCP 的三次握手(建立连接)
客户端 服务器
│ │
├──── SYN (seq=x) ──────────→ │ 第 1 次握手:客户端发起连接请求
│ │
│ ←──── SYN+ACK (seq=y, │ 第 2 次握手:服务器同意并回复
│ ack=x+1) ──────────── │
│ │
├──── ACK (ack=y+1) ────────→ │ 第 3 次握手:客户端确认
│ │
│ ===== 连接建立 ===== │
为什么是三次? 两次握手可能导致"已失效的连接请求"被服务器误认为新的请求,从而建立无效连接并浪费资源。第三次握手让服务器确认客户端确实收到了自己的回复。
3.4 TCP 的四次挥手(断开连接)
客户端 服务器
│ │
├──── FIN (seq=u) ──────────→ │ 第 1 次挥手:客户端请求断开
│ │
│ ←──── ACK (ack=u+1) ────── │ 第 2 次挥手:服务器确认(但可能还有数据要发)
│ │
│ ←──── FIN (seq=w) ──────── │ 第 3 次挥手:服务器也请求断开
│ │
├──── ACK (ack=w+1) ────────→ │ 第 4 次挥手:客户端确认
│ │
│ ===== 连接断开 ===== │
3.5 TCP 的作用与适用场景
- Web 浏览:HTTP/HTTPS 协议基于 TCP,确保网页内容完整加载。
- 文件传输:FTP、SCP 等,文件数据不能有任何丢失。
- 远程登录:SSH、Telnet,命令和响应必须可靠。
- 电子邮件:SMTP、IMAP 等。
- 数据库访问:MySQL、PostgreSQL 等的客户端连接。
3.6 TCP 通信流程
客户端 服务器
│ │
├─ socket() ├─ socket()
│ ├─ bind()
│ ├─ listen()
├─ connect() ───三次握手──────────→ ├─ accept() ←── 阻塞等待连接
│ │
├─ send()/write() ──────────────→ ├─ recv()/read()
├─ recv()/read() ←───────────── ├─ send()/write()
│ │
├─ close() ───四次挥手────────────→ ├─ close()
四、如何使用 TCP
4.1 核心函数
TCP 编程比 UDP 多了连接管理相关的函数:
| 函数 | 角色 | 作用 |
|---|---|---|
socket() |
通用 | 创建套接字 |
bind() |
服务器 | 绑定本地地址和端口 |
listen() |
服务器 | 将套接字设为监听状态 |
accept() |
服务器 | 接受客户端连接请求 |
connect() |
客户端 | 向服务器发起连接请求 |
send() / write() |
通用 | 发送数据 |
recv() / read() |
通用 | 接收数据 |
close() |
通用 | 关闭连接 |
4.2 socket() —— 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET:IPv4
// SOCK_STREAM:流式套接字(TCP)
// 0:自动选择协议
与 UDP 的唯一区别是第二个参数:TCP 使用 SOCK_STREAM(流式套接字),UDP 使用 SOCK_DGRAM(数据报套接字)。
4.3 bind() —— 绑定地址(服务器)
TCP 的 bind() 用法与 UDP 完全相同:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
4.4 listen() —— 开始监听(服务器)
#include <sys/socket.h>
int listen(int sockfd, int backlog);
| 参数 | 类型 | 说明 |
|---|---|---|
sockfd |
int |
已经 bind() 过的套接字描述符。 |
backlog |
int |
监听队列的最大长度,即允许多少个客户端连接排队等待 accept()。当队列满时,新的连接请求会被拒绝。常见取值为 5、10、128 等。 |
返回值: 成功返回 0,失败返回 -1。
listen(sockfd, 10); // 最多允许 10 个连接排队
4.5 accept() —— 接受连接(服务器)
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
| 参数 | 类型 | 说明 |
|---|---|---|
sockfd |
int |
正在监听的套接字描述符(即 listen() 的那个)。 |
addr |
struct sockaddr * |
输出参数,用于获取连接的客户端地址信息(IP + 端口)。不需要时传 NULL。 |
addrlen |
socklen_t * |
输入输出参数。输入时为 addr 缓冲区大小,输出时为实际地址长度。不需要时传 NULL。 |
返回值: 成功返回一个新的套接字描述符(用于与该客户端通信),失败返回 -1。
⚠️ 重要概念:
accept()返回的新 fd 和原来的监听 fd 是两个不同的东西。监听 fd 继续监听新的连接请求,新的 fd 专门用来和这个客户端通信。服务器通常会为每个客户端连接创建新线程或新进程来处理。
4.6 connect() —— 发起连接(客户端)
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| 参数 | 类型 | 说明 |
|---|---|---|
sockfd |
int |
客户端的套接字描述符。 |
addr |
const struct sockaddr * |
服务器的地址(IP + 端口)。 |
addrlen |
socklen_t |
地址结构体大小。 |
返回值: 成功返回 0,失败返回 -1。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
4.7 send() / recv() —— 收发数据
连接建立后,TCP 可以像操作普通文件一样用 send()/recv() 或 write()/read() 来收发数据:
// 发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// sockfd:连接的套接字
// buf:数据缓冲区
// len:数据长度
// flags:标志,通常传 0
// 接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// sockfd:连接的套接字
// buf:接收缓冲区
// len:缓冲区大小
// flags:标志,通常传 0
返回值: 成功返回实际发送/接收的字节数。recv() 返回 0 表示对端已关闭连接,返回 -1 表示出错。
⚠️ TCP 粘包问题:由于 TCP 是面向字节流的,
send()发送的数据可能会被合并或拆分。例如发送两次各 50 字节,接收方可能一次recv()就收到 100 字节。解决粘包问题的常见方案包括:固定长度、特殊分隔符、消息头携带长度字段等。
4.8 close() —— 关闭连接
close(sockfd); // 发起四次挥手,断开连接
4.9 完整示例:TCP 服务器和客户端
TCP 服务器(tcp_server.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define BUF_SIZE 1024
#define BACKLOG 10
int main() {
int listenfd, connfd;
char buf[BUF_SIZE];
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
// 1. 创建 TCP 套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 允许端口复用(服务器重启时不用等 TIME_WAIT)
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 绑定地址和端口
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind failed");
close(listenfd);
exit(EXIT_FAILURE);
}
// 3. 开始监听
if (listen(listenfd, BACKLOG) == -1) {
perror("listen failed");
close(listenfd);
exit(EXIT_FAILURE);
}
printf("[服务器] TCP 服务器已启动,监听端口 %d...\n", PORT);
// 4. 接受客户端连接
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (connfd == -1) {
perror("accept failed");
continue;
}
char cli_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip));
printf("[服务器] 客户端 %s:%d 已连接\n",
cli_ip, ntohs(cliaddr.sin_port));
// 5. 与该客户端通信
while (1) {
int n = recv(connfd, buf, BUF_SIZE - 1, 0);
if (n <= 0) {
// n == 0:客户端主动断开
// n == -1:出错
printf("[服务器] 客户端 %s:%d 已断开\n",
cli_ip, ntohs(cliaddr.sin_port));
break;
}
buf[n] = '\0';
printf("[服务器] 收到: %s\n", buf);
// 回显(Echo):将收到的内容原样返回
send(connfd, buf, n, 0);
}
close(connfd); // 关闭与该客户端的连接
}
close(listenfd); // 关闭监听套接字
return 0;
}
TCP 客户端(tcp_client.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUF_SIZE 1024
int main() {
int sockfd;
char buf[BUF_SIZE], msg[BUF_SIZE];
struct sockaddr_in servaddr;
// 1. 创建 TCP 套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 连接服务器
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("[客户端] 已连接到服务器 %s:%d\n", SERVER_IP, SERVER_PORT);
printf("输入消息(输入 quit 退出):\n");
// 3. 发送和接收
while (1) {
printf("> ");
fgets(msg, BUF_SIZE, stdin);
// 去掉末尾换行符
msg[strcspn(msg, "\n")] = '\0';
if (strcmp(msg, "quit") == 0) break;
// 发送
send(sockfd, msg, strlen(msg), 0);
// 接收回显
int n = recv(sockfd, buf, BUF_SIZE - 1, 0);
if (n <= 0) {
printf("[客户端] 服务器已断开\n");
break;
}
buf[n] = '\0';
printf("[服务器回显] %s\n", buf);
}
// 4. 关闭连接
close(sockfd);
return 0;
}
编译运行:
gcc tcp_server.c -o tcp_server
gcc tcp_client.c -o tcp_client
# 终端 1:运行服务器
./tcp_server
# 终端 2:运行客户端
./tcp_client
五、TCP 和 UDP 的区别
5.1 核心对比
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠传输(确认、重传、排序) | 不可靠(不保证到达、不保证顺序) |
| 数据格式 | 面向字节流(无边界) | 面向报文(保留边界) |
| 传输速度 | 较慢(建立连接 + 确认机制开销) | 较快(无连接开销) |
| 头部大小 | 20~60 字节 | 8 字节 |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有 | 无 |
| 通信模式 | 点对点(一对一) | 支持一对一、一对多、多对多 |
| 系统调用 | socket → bind → listen → accept → connect → send/recv |
socket → bind → sendto/recvfrom |
| 套接字类型 | SOCK_STREAM |
SOCK_DGRAM |
5.2 编程方式对比
| 对比维度 | TCP 编程 | UDP 编程 |
|---|---|---|
socket() 的 type |
SOCK_STREAM |
SOCK_DGRAM |
是否需要 listen() |
✅ 服务器需要 | ❌ 不需要 |
是否需要 accept() |
✅ 服务器需要 | ❌ 不需要 |
是否需要 connect() |
✅ 客户端需要 | ❌ 不需要 |
| 发送函数 | send() / write() |
sendto() |
| 接收函数 | recv() / read() |
recvfrom() |
| 发送时是否指定地址 | ❌ 连接已确定 | ✅ 每次都要指定目标地址 |
| 粘包问题 | ⚠️ 存在(字节流) | ❌ 不存在(报文边界清晰) |
5.3 选择建议
| 应用场景 | 推荐协议 | 原因 |
|---|---|---|
| Web 网页浏览(HTTP/HTTPS) | TCP | 网页内容不能丢失 |
| 文件传输(FTP) | TCP | 文件完整性要求高 |
| 远程登录(SSH) | TCP | 命令不能丢 |
| 视频通话/直播 | UDP | 实时性优先,偶尔卡顿比延迟更可接受 |
| 在线游戏 | UDP | 操作同步实时性高,丢旧数据无所谓 |
| DNS 查询 | UDP | 请求简单,一次一问 |
| 语音通话(VoIP) | UDP | 延迟敏感 |
| 邮件收发(SMTP/IMAP) | TCP | 邮件内容不能丢 |
5.4 一张图看懂
┌──────────────────────────────────┐
│ 应用层 │
│ HTTP FTP SSH DNS 视频 游戏 │
└──────┬─────────────┬─────────────┘
│ │
┌──────▼─────┐ ┌────▼─────┐
│ TCP │ │ UDP │
│ 可靠传输 │ │ 尽最大努力│
│ 面向连接 │ │ 无连接 │
│ 字节流 │ │ 报文 │
│ 流量控制 │ │ 无控制 │
│ 拥塞控制 │ │ 无控制 │
└──────┬─────┘ └────┬─────┘
│ │
┌──────▼─────────────▼──────────────┐
│ IP 层 │
└───────────────┬───────────────────┘
│
┌───────────────▼───────────────────┐
│ 链路层/物理层 │
└───────────────────────────────────┘
六、常见问题 FAQ
Q1:TCP 为什么比 UDP 慢?
A:TCP 在发送数据前需要三次握手建立连接,每次发送数据还需要等待 ACK 确认,超时还要重传。这些机制保证了可靠性,但也增加了延迟。UDP 直接发,不等确认,所以更快。
Q2:UDP 能实现可靠传输吗?
A:UDP 本身不可靠,但可以在应用层实现可靠性(如自定义确认、重传机制)。QUIC 协议(HTTP/3)就是基于 UDP 实现了类似 TCP 的可靠传输,同时减少了握手延迟。
Q3:TCP 的 recv() 返回 0 是什么意思?
A:返回 0 表示对端已经调用了 close() 关闭了连接(发送了 FIN),不是出错。这是正常的连接关闭信号。
Q4:一个 TCP 服务器能同时处理多少客户端?
A:取决于系统资源(文件描述符数量、内存等)。单线程的 TCP 服务器一次只能和一个客户端通信(accept → 通信 → accept 下一个)。要并发处理多个客户端,需要使用多线程、多进程或 I/O 多路复用(select/poll/epoll)。
Q5:TCP 粘包怎么解决?
A:常见方案有三种:① 固定长度(每条消息固定 N 字节);② 分隔符(用特殊字符如 \n 分隔消息);③ 消息头(在消息头部用一个字段标明消息体的长度,接收方先读头部再读对应长度的数据)。
七、总结
| 协议 | 一句话概括 | 典型应用 |
|---|---|---|
| TCP | 可靠、面向连接、字节流 | Web、文件传输、远程登录、数据库 |
| UDP | 快速、无连接、报文 | 视频通话、在线游戏、DNS、语音 |
TCP 和 UDP 没有优劣之分,只有适用场景的不同。需要可靠性就选 TCP,需要速度和实时性就选 UDP(必要时在应用层补充可靠性)。理解它们的原理和特点,是进行网络编程的基础。
浙公网安备 33010602011771号