引言
在 Linux 网络编程中,传输层提供两种核心协议:TCP(传输控制协议) 和 UDP(用户数据报协议)。它们各有特点,适用于不同的应用场景。
| 特性 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认重传) | 不可靠(尽最大努力) |
| 数据边界 | 流式服务(无边界) | 数据报服务(有边界) |
| 传输效率 | 较低 | 较高 |
| 适用场景 | 文件传输、网页访问 | 实时音视频、DNS查询 |
今天,我们将深入学习 TCP 和 UDP 的编程模型,理解它们的核心差异,并通过完整的代码示例掌握两种协议的使用方法。
第一部分:TCP 编程回顾
一、TCP 服务端完整代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 6000
#define BUFFER_SIZE 128
int main() {
int listen_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
char buffer[BUFFER_SIZE];
// 1. 创建套接字
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket error");
exit(1);
}
// 2. 绑定 IP 和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind error");
exit(1);
}
// 3. 创建监听队列
if (listen(listen_fd, 5) == -1) {
perror("listen error");
exit(1);
}
printf("TCP 服务器启动成功,端口:%d\n", PORT);
while (1) {
// 4. 接受客户端连接
client_len = sizeof(client_addr);
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept error");
continue;
}
printf("客户端连接:%s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 5. 循环接收数据
while (1) {
memset(buffer, 0, BUFFER_SIZE);
int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
if (n == 0) {
printf("客户端已断开\n");
break;
}
if (n == -1) {
perror("recv error");
break;
}
printf("收到数据:%s\n", buffer);
send(client_fd, "OK", 2, 0);
}
close(client_fd);
}
close(listen_fd);
return 0;
}
二、TCP 客户端完整代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 6000
#define BUFFER_SIZE 128
int main() {
int sock_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 1. 创建套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket error");
exit(1);
}
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 3. 连接服务器
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("connect error");
exit(1);
}
printf("连接服务器成功\n");
// 4. 循环收发数据
while (1) {
printf("请输入消息(输入end退出):");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strlen(buffer) - 1] = '\0';
if (strcmp(buffer, "end") == 0) {
break;
}
send(sock_fd, buffer, strlen(buffer), 0);
memset(buffer, 0, BUFFER_SIZE);
recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
printf("服务器响应:%s\n", buffer);
}
close(sock_fd);
return 0;
}
三、netstat 命令使用
查看所有 TCP 连接
netstat -natp
查看特定端口
netstat -natp | grep 6000
查看 UDP 服务
netstat -naupt
netstat 输出字段说明:
| 字段 | 含义 |
|---|---|
| Proto | 协议类型(TCP/UDP) |
| Recv-Q | 接收缓冲区待处理数据量 |
| Send-Q | 发送缓冲区待处理数据量 |
| Local Address | 本地 IP:端口 |
| Foreign Address | 对端 IP:端口 |
| State | 连接状态(TCP) |
| PID/Program name | 进程ID/程序名 |
第二部分:TCP 协议栈深入理解
一、监听套接字与连接套接字

| 套接字类型 | 功能 | 生命周期 |
|---|---|---|
| 监听套接字 | 接收客户端连接请求 | 整个服务周期 |
| 连接套接字 | 与特定客户端通信 | 单次会话周期 |
二、TCP 缓冲区机制
TCP 是流式服务,数据在发送方和接收方都有缓冲区:
重要特性:
-
send()成功只表示数据已写入发送缓冲区,不代表对方已接收 -
recv()从接收缓冲区读取数据,缓冲区为空时阻塞 -
TCP 允许分次读取(如
recv1字节可逐个接收)
cpp
// 实验:修改接收长度为1字节,观察现象
// 客户端发送 "hello" 需要5次 recv 才能读完
int n = recv(client_fd, buffer, 1, 0); // 每次只读1字节
三、TCP vs UDP 数据接收对比
| 场景 | TCP | UDP |
|---|---|---|
| 发送端 | 多次 send 可能合并 | 每次 sendto 独立报文 |
| 接收端 | 可分批读取 | 必须单次完整读取 |
| 数据边界 | 无边界(流) | 有边界(数据报) |
第三部分:UDP 编程
一、UDP 服务端完整代码
UDP 无需建立连接,使用 recvfrom() 和 sendto() 收发数据。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 6000
#define BUFFER_SIZE 128
int main() {
int sock_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
char buffer[BUFFER_SIZE];
// 1. 创建套接字(注意:SOCK_DGRAM)
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd == -1) {
perror("socket error");
exit(1);
}
// 2. 绑定 IP 和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind error");
exit(1);
}
printf("UDP 服务器启动成功,端口:%d\n", PORT);
while (1) {
client_len = sizeof(client_addr);
memset(buffer, 0, BUFFER_SIZE);
// 3. 接收数据(同时获取客户端地址)
int n = recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&client_addr, &client_len);
if (n == -1) {
perror("recvfrom error");
continue;
}
printf("收到来自 %s:%d 的数据:%s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 4. 回复数据(需要指定客户端地址)
sendto(sock_fd, "OK", 2, 0,
(struct sockaddr*)&client_addr, client_len);
}
close(sock_fd);
return 0;
}
二、UDP 客户端完整代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 6000
#define BUFFER_SIZE 128
int main() {
int sock_fd;
struct sockaddr_in server_addr;
socklen_t server_len;
char buffer[BUFFER_SIZE];
// 1. 创建套接字
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd == -1) {
perror("socket error");
exit(1);
}
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_len = sizeof(server_addr);
while (1) {
printf("请输入消息(输入end退出):");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strlen(buffer) - 1] = '\0';
if (strcmp(buffer, "end") == 0) {
break;
}
// 3. 发送数据(需指定目标地址)
sendto(sock_fd, buffer, strlen(buffer), 0,
(struct sockaddr*)&server_addr, server_len);
memset(buffer, 0, BUFFER_SIZE);
// 4. 接收回复
recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0, NULL, NULL);
printf("服务器响应:%s\n", buffer);
}
close(sock_fd);
return 0;
}
三、UDP 核心函数详解
recvfrom 函数
cpp
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
套接字描述符 |
buf |
接收数据缓冲区 |
len |
缓冲区大小 |
flags |
标志位(通常为0) |
src_addr |
输出参数,存储发送方地址 |
addrlen |
输入输出参数,地址结构大小 |
sendto 函数
cpp
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
套接字描述符 |
buf |
发送数据缓冲区 |
len |
数据长度 |
flags |
标志位(通常为0) |
dest_addr |
目标地址 |
addrlen |
地址结构大小 |
第四部分:TCP vs UDP 核心差异
一、协议特性对比
| 特性 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 确认重传、顺序保证 | 尽最大努力,可能丢包 |
| 数据边界 | 流式(无边界) | 数据报(有边界) |
| 拥塞控制 | 有 | 无 |
| 传输效率 | 较低 | 较高 |
| 编程复杂度 | 较高 | 较低 |
二、编程模型对比
| 操作 | TCP | UDP |
|---|---|---|
| 创建套接字 | socket(AF_INET, SOCK_STREAM, 0) |
socket(AF_INET, SOCK_DGRAM, 0) |
| 绑定地址 | bind() |
bind() |
| 建立连接(服务端) | listen() + accept() |
不需要 |
| 建立连接(客户端) | connect() |
不需要 |
| 发送数据 | send() / write() |
sendto() |
| 接收数据 | recv() / read() |
recvfrom() |
| 获取对端地址 | accept() 返回 |
recvfrom() 返回 |
三、数据接收特性对比
TCP 流式服务:
cpp
// 发送端:多次 send
send(fd, "hello", 5, 0);
send(fd, "world", 5, 0);
// 接收端:可能一次收到 "helloworld",也可能分次收到
// 数据没有边界
UDP 数据报服务:
cpp
// 发送端:每次 sendto 独立
sendto(fd, "hello", 5, 0, ...);
sendto(fd, "world", 5, 0, ...);
// 接收端:每次 recvfrom 对应一次 sendto
// 数据有边界,必须单次完整读取
// 如果缓冲区太小,剩余数据会被丢弃!
第五部分:端口复用与并发
一、端口复用规则
| 场景 | 是否可复用 | 说明 |
|---|---|---|
| TCP + TCP 同一端口 | ❌ 不可 | 端口已被占用 |
| UDP + UDP 同一端口 | ❌ 不可 | 端口已被占用 |
| TCP + UDP 同一端口 | ✅ 可 | 不同协议,互不冲突 |
验证:TCP 6000 和 UDP 6000 可同时存在
netstat -naupt | grep 6000
二、UDP 的并发特性
UDP 是无连接的,单线程即可处理多个客户端:

总结
一、TCP vs UDP 速查表
| 对比项 | TCP | UDP |
|---|---|---|
| 套接字类型 | SOCK_STREAM |
SOCK_DGRAM |
| 服务端流程 | socket→bind→listen→accept→recv/send→close | socket→bind→recvfrom→sendto→close |
| 客户端流程 | socket→connect→send/recv→close | socket→sendto→recvfrom→close |
| 数据边界 | 无(流) | 有(数据报) |
| 并发实现 | 需要多进程/多线程 | 单线程即可 |
二、代码运行测试
编译 UDP 程序
gcc udp_server.c -o udp_server
gcc udp_client.c -o udp_client
先启动服务器
./udp_server
另一终端启动客户端(可多个)
./udp_client
三、面试高频考点
-
TCP 三次握手:SYN → SYN+ACK → ACK
-
TCP 四次挥手:FIN → ACK → FIN → ACK
-
TCP vs UDP 区别:连接性、可靠性、数据边界
-
端口复用:不同协议可绑定同一端口
-
UDP 数据报边界:必须单次完整读取
本文详细介绍了 TCP 和 UDP 网络编程,包括:
-
TCP 服务端/客户端完整实现:理解面向连接的通信模型
-
UDP 服务端/客户端完整实现:理解数据报服务的特点
-
核心差异分析:连接性、可靠性、数据边界
-
端口复用与并发:UDP 天然支持多客户端
课后任务:
-
整理 TCP 和 UDP 的对比笔记
-
动手运行两种协议的代码
-
观察 UDP 数据报截断现象(减小 recvfrom 缓冲区)