一、UDP vs TCP 特征对比
| 特征 | UDP | TCP |
|---|---|---|
| 连接方式 | 无连接 | 有连接(需三次握手) |
| 可靠性 | 不可靠 | 可靠(应答、超时重传、拥塞控制) |
| 传输延迟 | 低 | 较高 |
| 网络开销 | 小 | 大(需维护链路状态) |
| 工作模式 | 半双工 | 全双工 |
| 套接字类型 | SOCK_DGRAM |
SOCK_STREAM 流式套接字 |
| 读/写阻塞 | 有读阻塞,无写阻塞 | 有读阻塞,有写阻塞(64K) |
| 数据顺序 | 不保证顺序到达 | 保证顺序(有序到达) |
| 发送与接收次数 | 必须一一对应 | 不需要对应,数据连续 |
| 协议头大小 | 8 字节 | 20 字节 |
二、通用 Socket 函数
以下函数 UDP 和 TCP 都会用到。
2.1 创建套接字
cs
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
| 参数 | 取值 | 说明 |
|---|---|---|
domain |
AF_INET / PF_INET |
互联网程序(IPv4) |
domain |
AF_UNIX / PF_UNIX |
单机程序 |
type |
SOCK_STREAM |
流式套接字 → TCP |
type |
SOCK_DGRAM |
用户数据报套接字 → UDP |
type |
SOCK_RAW |
原始套接字 → IP |
protocol |
0 |
自动适应应用层协议 |
返回值: 成功返回套接字 fd,失败返回 -1
2.2 绑定地址
cs
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
- 服务器端:将套接字与指定接口地址关联,用于从该接口接收数据
- 客户端:可省略,由系统默认接口发送
地址结构体:
bash
// 网络地址结构(传参时强转为 struct sockaddr*)
struct sockaddr_in
{
u_short sin_family; // 地址族,AF_INET
u_short sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP 地址
char sin_zero[8]; // 填充位
};
三、UDP 编程
3.1 UDP 特性说明
-
发送次数和接收次数必须一一对应
-
发送和接收的大小需保持一致(若接收 buf 小于发送大小,超出部分丢失)
-
每次发送数据,链路都可能不同
-
有读阻塞(没有数据时会阻塞等待)
-
无写阻塞(发送太快时,接收方来不及处理会丢包)
-
半双工
3.2 UDP 编程步骤
cs
服务器端:socket() → bind() → recvfrom() → sendto() → close()
客户端: socket() → sendto() → recvfrom() → close()
3.3 UDP 收发函数
// 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
本地套接字 fd |
buf |
要发送的数据 |
len |
数据长度 |
flags |
0 表示阻塞发送 |
dest_addr |
必填,目标主机地址结构体 |
addrlen |
目标地址长度 |
返回值: 成功返回发送的字节数,失败返回 -1
bash
// 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
本地套接字 fd |
buf |
存储接收数据的内存 |
len |
要接收的数据长度(一般为 buf 大小) |
flags |
0 表示阻塞接收 |
src_addr |
可选,对方地址信息(NULL 表示不关心) |
addrlen |
对方地址结构体大小(src_addr 为 NULL 时也为 NULL) |
返回值: 成功返回接收到的字节数,失败返回 -1
3.4 UDP 示例代码框架
cs
typedef struct sockaddr * (SA);
int main(int argc, char **argv)
{
// 1. internet , udp, 默认协议
int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == udpfd)
{
perror("socket");
return 1;
}
// 2. 给套接字 绑定ip ,端口号
// man 7 ip 查询 ipv4 的地址结构体
struct sockaddr_in ser, cli; // 服务器的地址结构体, 客户端的地址结构体
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET; // ipv4
ser.sin_port = htons(50000); // host to net short 小端转大端
ser.sin_addr.s_addr = inet_addr("192.168.31.149");
int ret = bind(udpfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{z
perror("bind");
return 1;
}
socklen_t len = sizeof(cli);
while (1)
{
char buf[1024]={0};
recvfrom(udpfd,buf,sizeof(buf),0,(SA)&cli,&len);
time_t tm;
time(&tm);
sprintf(buf,"%s %s",buf,ctime(&tm));
sendto(udpfd,buf,strlen(buf),0,(SA)&cli,len);
}
return 0;
}
cs
// ===== UDP 客户端 =====
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
//1创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return 1;
}
struct sockaddr_in ser,cli;
// 清空结构体
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
//2主动连接服务器
int ret = connect(sockfd,(SA)&ser,sizeof(ser));
if (-1 == ret)
{
perror("connect");
return 1;
}
while (1)
{
char buf[512] = "hello,woshihuihui";
//3发送数据 返回值>0,实际发送的字节数 ==0 网络状态不好,没有发出数据 -1错误
send(sockfd, buf, strlen(buf), 0);
bzero(buf, sizeof(buf));
//4接收数据 >0 实际接收的字节数,==0表示断开,-1表示错误
int rd_ret = recv(sockfd, buf, sizeof(buf), 0);
if (rd_ret<=0)
{
break;
}
printf("from ser:%s\n",buf);
sleep(1);
}
//5断开连接
close(sockfd);
return 0;
}
四、TCP 编程
4.1 TCP 特性说明
1. 有连接:通信前需建立链路(三次握手),通信中保持,断开时四次挥手
2. 可靠传输:应答机制 + 超时重传机制 + 拥塞控制
3. 全双工:同一时刻可以同时收发
4. 流式套接字:数据连续,发送/接收次数不需要一一对应
5. 有读阻塞,有写阻塞(缓冲区 64K)
6. 保证数据顺序到达
4.2 TCP 编程步骤
服务器端:socket() → bind() → listen() → accept() → recv()/send() → close()
客户端: socket() → connect() → send()/recv() → close()
4.3 TCP 专用函数
listen() - 监听
int listen(int sockfd, int backlog);
| 参数 | 说明 |
|---|---|
sockfd |
套接字 fd |
backlog |
允许连接的最大客户端数量 |
返回值: 成功 0,失败 -1
accept() - 接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
监听套接字 fd(listfd) |
addr |
NULL 表示不获取客户端信息;否则传入地址变量地址 |
addrlen |
addr 为 NULL 时也为 NULL;否则 len = sizeof(struct sockaddr) |
返回值: 成功返回新的通信套接字 fd(connfd),失败返回 -1
⚠️ 注意:
listfd负责监听,connfd负责实际通信,两者分开!
connect() - 发起连接(客户端专用)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
本地 socket() 创建的套接字 fd |
addr |
远程目标主机地址信息 |
addrlen |
参数2的长度 |
返回值: 成功 0,失败 -1
recv() / send() - 收发数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *msg, size_t len, int flags);
| 参数 | 说明 |
|---|---|
sockfd |
服务器用 connfd(accept返回值),客户端用 sockfd(socket返回值) |
buf/msg |
数据存储/发送的内存 |
len |
数据长度 |
flags |
0 表示阻塞 |
4.4 TCP 示例代码框架
cs
// ===== TCP 服务器端 =====
int main() {
// 1. 创建监听套接字
int listfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(listfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 3. 监听
listen(listfd, 5);
// 4. 接受连接
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connfd = accept(listfd, (struct sockaddr*)&client_addr, &len);
// 5. 收发数据(循环)
char buf[1024] = {0};
while (1) {
int n = recv(connfd, buf, sizeof(buf), 0);
if (n <= 0) break; // 客户端断开
printf("recv: %s\n", buf);
send(connfd, buf, n, 0);
}
// 6. 关闭
close(connfd);
close(listfd);
return 0;
}
cs
// ===== TCP 客户端 =====
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);
// 发起连接(三次握手)
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
char buf[] = "hello";
send(sockfd, buf, strlen(buf), 0);
char recv_buf[1024] = {0};
recv(sockfd, recv_buf, sizeof(recv_buf), 0);
printf("recv: %s\n", recv_buf);
close(sockfd);
return 0;
}
五、三次握手与四次挥手
5.1 三次握手(建立连接)
cs
Client Server
| ---- SYN, seq=1000 ----> | 第1次:客户端请求连接
| <-- SYN+ACK, seq=8000 --- | 第2次:服务器确认并请求
| ---- ACK 8001 ----------> | 第3次:客户端确认
| [连接建立] |
5.2 四次挥手(断开连接)
cs
Client Server
| ---- FIN, ACK ---------> | 第1次:客户端请求断开
| <---- ACK --------------- | 第2次:服务器确认
| <---- FIN, ACK ---------- | 第3次:服务器也请求断开
| ---- ACK ----------------> | 第4次:客户端确认
| [连接关闭] |
六、通信套接字与监听套接字
| 套接字类型 | 说明 |
|---|---|
listfd(监听套接字) |
绑定 IP + Port,专门监听客户端连接请求 |
connfd(通信套接字) |
每当有客户端连接成功,服务端生成一个新的 connfd 用于实际通信 |
服务器:
listfd(3) → 192.168.31.177:50000(监听)
connfd(4) → 与客户端 a 通信
connfd(5) → 与客户端 b 通信
七、TCP 黏包问题
7.1 什么是黏包?
发送方多次发送 数据,接收方因为流式传输特性,无法正确区分每次发送的数据边界,导致无法正常解析。
7.2 解决方案
| 方案 | 说明 |
|---|---|
| 设置结束标志 | 在数据末尾添加特殊标记,如 \r\n |
| 发送固定大小 | 使用 struct 固定报文大小,收发都按 sizeof(struct) 操作 |
| 自定义协议(变长) | 报文头包含数据长度字段,先读头再按长度读数据 |
cs
// 方案2:固定大小结构体示例
typedef struct {
char buf[1024]; // 正文数据
int buf_len; // 实际数据长度
int type; // 类型:1=chat, 2=file
} MSG;
// 发送
MSG msg = {0};
strcpy(msg.buf, "hello");
msg.buf_len = strlen(msg.buf);
msg.type = 1;
send(connfd, &msg, sizeof(MSG), 0);
// 接收
MSG recv_msg;
recv(connfd, &recv_msg, sizeof(MSG), 0);
八、API 速查表
| 函数 | TCP/UDP | 作用 |
|---|---|---|
socket() |
通用 | 创建套接字 |
bind() |
通用 | 绑定地址(服务器必用) |
listen() |
TCP 服务器 | 开始监听连接 |
accept() |
TCP 服务器 | 接受客户端连接 |
connect() |
TCP 客户端 | 发起连接 |
send() |
TCP | 发送数据 |
recv() |
TCP | 接收数据 |
sendto() |
UDP | 发送数据(含目标地址) |
recvfrom() |
UDP | 接收数据(可获取来源地址) |
close() |
通用 | 关闭套接字 |
上一篇:网络通信(一)------ 基础概念与网络模型 下一篇:网络通信(三)------ 协议头结构、HTTP 与 Wireshark 抓包