Linux网络通信(二)----核心网络协议(传输与应用)
一、UDP 用户数据报
1. 核心特征
UDP 是一种无连接、不可靠的传输层协议,核心特点可以概括为
- 无连接:通信前不需要建立链路,每次发送数据都独立寻址,链路可能不同。
- 不可靠:不保证数据一定到达,也不保证顺序到达,存在丢包风险。
- 低延迟 & 开销小:无需维护连接状态,包头开销小,适合实时场景。
- 数据报特性:
- 发送次数与接收次数必须一一对应(发几次就要收几次)。
- 发送和接收的缓冲区大小要一致,否则多余数据会被丢弃(一包数据为最小单位)。
- 阻塞特性:有读阻塞(没数据时会等待),无写阻塞(发送过快可能导致接收方来不及处理而丢包)。
- 半双工:同一时刻只能单向通信。

2. UDP 编程步骤
UDP 编程基于数据报套接字(SOCK_DGRAM),客户端和服务器的流程相对简单,无需建立连接:

编程流程
服务器端(必须先接收才能传输):
socket()→bind()→ 循环recvfrom()/sendto()→close()客户端(必须先传输才能接收):
socket()→ 循环sendto()/recvfrom()→close()
3. UDP 核心函数
3.1 socket():创建 UDP 套接字
int socket(int domain, int type, int protocol);
功能
向内核申请创建一个用户数据报套接字(UDP),返回套接字文件描述符,是 UDP 通信的入口。
参数
domain:地址族,UDP 网络编程固定为PF_INET == AF_INET(互联网程序);单机通信用PF_UNIX == AF_UNIX。type:套接字类型,UDP 必须填SOCK_DGRAM(用户数据报套接字)。SOCK_STREAM 流式套接字 ===》TCPSOCK_DGRAM 用户数据报套接字===>UDPSOCK_RAW 原始套接字 ===》IPprotocol:协议,填0表示自动适配应用层协议(UDP 场景默认匹配 UDP 协议)。
返回值
- 成功:返回申请的套接字 ID(非负整数);
- 失败:返回
-1,并设置errno。
3.2 bind():绑定 UDP 套接字地址
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
功能
将套接字与指定的 IP 地址、端口号绑定,用于从该接口接收数据(UDP 服务器端必须调用,客户端可省略,由系统分配默认端口)。
(1) 如果该函数在服务器端调用,则表示将参数1相关的文件描述符文件与参数2 指定的接口地址关联, 用于从该接口接受数据。(2) 如果该函数在客户端调用,则表示要将数据从 参数1所在的描述符中取出并从参数2所在的接口设备上发送出去。注意:如果是客户端,则该函数可以省略,由默认接口发送数据。
参数
sockfd:socket()创建的套接字文件描述符。my_addr:物理接口的结构体指针,表示该接口的信息,通用地址结构为struct sockaddr,实际使用网络地址结构struct sockaddr_in(需强转),包含地址族、端口、IP 等信息。
struct sockaddr ////通用地址结构{u_short sa_family; ////地址族char sa_data[14]; ////地址信息};转换成网络地址结构如下:struct _sockaddr_in ///网络地址结构{u_short sin_family; ////地址族u_short sin_port; ///地址端口struct in_addr sin_addr; ///地址IPchar sin_zero[8]; ////占位};struct in_addr{in_addr_t s_addr;}
addrlen:my_addr结构体的长度。
返回值
- 成功:返回
0; - 失败:返回
-1,并设置errno。
3.3 sendto():UDP 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
功能
UDP 协议专用发送函数,向指定目标主机发送数据,无需建立连接,每次发送都需指定目标地址。
参数
sockfd:本地的套接字 ID。buf:本地的数据缓冲区,存储要发送的数据。len:要发送的数据长度。flags:发送方式,填0表示阻塞发送。dest_addr:必选,目标主机的地址信息结构体(IP + 端口)。addrlen:目标地址结构体的长度。
返回值
- 成功:返回实际发送的数据长度;
- 失败:返回
-1,并设置errno。
3.4 recvfrom():UDP 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
功能
UDP 协议专用接收函数,从套接字获取对方发送的数据,同时可获取发送方的地址信息。
参数
sockfd:本地的套接字 ID。buf:存储数据的内存区(一般为数组 / 动态内存)。len:要获取的数据长度(需与发送长度一致,否则剩余数据会丢失)。flags:获取方式,填0表示阻塞接收。src_addr:可选,发送方的地址信息结构体,填NULL表示不关心对方地址。addrlen:对方地址结构体大小,若src_addr为NULL,则该值也为NULL。
返回值
- 成功:返回实际接收到的数据长度;
- 失败:返回
-1,并设置errno。
3.5 close():关闭 UDP 套接字
int close(int fd);
功能
关闭套接字文件描述符,释放内核分配的网络通信资源,结束 UDP 通信。
参数
fd:socket()创建的套接字 ID。
返回值
- 成功:返回
0; - 失败:返回
-1,并设置errno。
示例1 : UDP 多线程实现双向实时聊天
服务端 :
// server 服务端
#include <sys/types.h>
#include <sys/socket.h> // 套接字核心头文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h> // IPv4地址结构体
#include <netinet/ip.h>
#include <arpa/inet.h> // 字节序转换、IP转换
#include <pthread.h> // 多线程头文件
// 简化地址结构体强制类型转换
typedef struct sockaddr* (SA);
// 全局地址结构体:服务端、客户端地址信息(ip + 端口)
struct sockaddr_in ser, cli;
void* th1(void* arg) // 线程1:专门 接收 客户端发来的消息
{
// 获取主线程创建的UDP套接字文件描述符
int udpfd = *(int*)arg;
socklen_t len = sizeof(cli);
// 持续接收客户端消息
while (1)
{
char buf[1024] = {0}; // 接收缓冲区
// 无连接,每次接收独立数据报
recvfrom(udpfd, buf, sizeof(buf), 0, NULL, NULL);
if (0 == strcmp(buf, "#quit\n"))
{
exit(0);
}
// 打印客户端消息
printf("from cli:%s", buf);
fflush(stdout); // 强制刷新输出缓冲区
}
return NULL;
}
// 线程参数结构体:传递 客户端地址 + 套接字
typedef struct
{
struct sockaddr_in cli; // 客户端IP+端口
int fd; // UDP套接字
} TH_ARG;
void* th2(void* arg) // 线程2:专门 发送 消息给客户端
{
TH_ARG* tmp = (TH_ARG*)arg;
socklen_t len = sizeof(cli);
// 持续从键盘输入并发送
while (1)
{
char buf[1024] = {0};
printf("to cli:");
// 从标准输入读取要发送的消息
fgets(buf, sizeof(buf), stdin);
// 必须指定目标客户端地址(IP+端口)
sendto(tmp->fd, buf, strlen(buf), 0, (SA)&tmp->cli, len);
if (0 == strcmp(buf, "#quit\n"))
{
exit(0);
}
}
return NULL; // 补全函数返回值
}
// 主函数:UDP服务端初始化 + 线程创建
int main(int argc, char** argv)
{
// 1. 创建UDP套接字(重点)
int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == udpfd)
{
perror("socket");
return 1;
}
// 初始化地址结构体(清空内存)
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
// 2. 配置服务端地址信息
ser.sin_family = AF_INET; // IPv4协议
ser.sin_port = htons(50000); // 端口号:主机字节序 → 网络字节序(大端)
ser.sin_addr.s_addr = INADDR_ANY; // 绑定本机所有网卡IP
// 3. 绑定IP和端口(服务端必须bind)
int ret = bind(udpfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("bind");
return 1;
}
// 客户端地址长度
socklen_t len = sizeof(cli);
TH_ARG arg; // 线程参数
char buf[1024] = {0};
// 关键步骤:先接收一次消息
// 目的:获取客户端的IP和端口号
// UDP无连接,必须知道对方地址才能发送
recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
// 把客户端地址和套接字打包传给发送线程
arg.cli = cli;
arg.fd = udpfd;
// 4. 创建两个线程,实现双向通信
// tid1:接收线程
// tid2:发送线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, th1, &udpfd); // 收消息
pthread_create(&tid2, NULL, th2, &arg); // 发消息
// 等待线程结束(阻塞主线程)
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
客户端 :
// client 客户端
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <pthread.h>
typedef struct sockaddr*(SA);
struct sockaddr_in ser; // 服务器的服务器的 IP + 端口
void* th1(void* arg) // 线程1:专门 接收 服务端发来的消息
{
int udpfd = *(int*)arg; // 获取主线程的 UDP 套接字
while (1)
{
char buf[1024] = {0};
// UDP 接收函数 无连接,接收数据报
recvfrom(udpfd, buf, sizeof(buf), 0, NULL, NULL);
if (0 == strcmp(buf, "#quit\n"))
{
exit(0);
}
printf("from ser:%s", buf);
fflush(stdout);
}
return NULL;
}
void* th2(void* arg) // 线程2:专门 发送 消息给服务端
{
int udpfd = *(int*)arg;
socklen_t len = sizeof(ser); // 服务器地址长度
while (1)
{
char buf[1024] = {0};
printf("to ser:");
fgets(buf, sizeof(buf), stdin);
// UDP 发送函数 必须指定目标服务器地址(IP+端口)
sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, len);
if (0 == strcmp(buf, "#quit\n"))
{
exit(0);
}
}
}
int main(int argc, char** argv)
{
int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == udpfd)
{
perror("socket");
return 1;
}
bzero(&ser, sizeof(ser));
ser.sin_family = AF_INET; // ipv4
ser.sin_port = htons(50000); //服务器端口(主机序 → 网络序) 小端转大端
ser.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP(本地回环)
// 关键步骤:先发一条消息给服务器
// 目的:让服务器获取客户端的 IP + 端口
// 因为 UDP 无连接,服务器必须先收到一次才能回复
char buf[1024] = "start";
sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, th1, &udpfd);
pthread_create(&tid2, NULL, th2, &udpfd);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
close(udpfd);
return 0;
}
示例2 : UDP 文件传输(图片传输)
客户端发送图片数据
// client 客户端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <fcntl.h>
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
// AF_INET:IPv4协议族 | SOCK_DGRAM:UDP数据报套接字 | 0:默认UDP协议
int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == udpfd)
{
perror("socket");
return 1;
}
struct sockaddr_in ser; // 服务端地址结构体(服务端IP+端口)
bzero(&ser, sizeof(ser)); // 初始化地址结构体(清零)
ser.sin_family = AF_INET; // ipv4
ser.sin_port = htons(50000); // host to net short 小端转大端
// 127.0.0.1 本地回环地址 ,用于本地网络测试的地址 ,表示本机ip地址
ser.sin_addr.s_addr = inet_addr("127.0.0.1"); // 目标IP:本地回环地址,仅本地测试用
// 打开待发送的图片文件:只读模式,路径为/home/linux/1.png
int fd = open("/home/linux/1.png", O_RDONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
char buf[1024] = {0}; // 数据缓冲区:每次最多读取/发送1024字节
while (1)
{
bzero(buf, sizeof(buf));
// 从文件读取数据到缓冲区,返回实际读取的字节数
int rd_ret = read(fd, buf, sizeof(buf));
if (rd_ret <= 0) // 读取完毕(rd_ret=0)或读取失败(rd_ret=-1)
{
break;
}
// 发送数据到服务端:udpfd-套接字 | buf-数据 | rd_ret-实际长度 | 0-默认标志
// (SA)&ser-服务端地址 | sizeof(ser)-地址长度
sendto(udpfd, buf, rd_ret, 0, (SA)&ser, sizeof(ser));
bzero(buf, sizeof(buf));
// 接收服务端的确认消息(阻塞等待):NULL表示不关心服务端地址
recvfrom(udpfd, buf, rd_ret, 0, NULL, NULL);
}
strcpy(buf, "^_^");
sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser)); // 发送结束标志
close(fd);
close(udpfd);
return 0;
}
服务端接收
// server 服务端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <fcntl.h>
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 小端转大端
// INADDR_ANY 表示本机地址 == 本机真实的ip ,服务端使用
ser.sin_addr.s_addr = INADDR_ANY;
int ret = bind(udpfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("bind");
return 1;
}
socklen_t len = sizeof(cli);
int fd = open("2.png", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (-1 == fd)
{
perror("open error");
return 1;
}
while (1)
{
char buf[1024] = {0};
int rd_ret = recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
if (0 == strcmp(buf, "^_^")) // 判断是不是结束标志
{
break;
}
write(fd, buf, rd_ret);
bzero(buf, sizeof(buf));
strcpy(buf, "go on");
sendto(udpfd, buf, strlen(buf), 0, (SA)&cli, len);
}
close(fd);
close(udpfd);
return 0;
}
UDP 两类代码(聊天 / 文件传输)核心重点
| 操作 | 服务端 | 客户端 |
|---|---|---|
bind() |
必须调用,绑定固定端口 | 无需调用,系统自动分配 |
| 地址配置 | 绑定自身 IP + 端口 | 配置服务端 IP + 端口 |
| 初始操作 | 先 recvfrom() 拿客户端地址 |
先 sendto() 发消息告知服务端 |
| 线程作用 | 收客户端消息 / 发消息给客户端 | 收服务端消息 / 发消息给服务端 |
bind() 服务端必须调用,客户端无需调用
- 作用:将服务端套接字与固定 IP + 端口号 绑定,让客户端能定位到服务端
- 客户端由系统自动分配临时端口,无需手动绑定
- 绑定前需配置地址结构体,完成主机字节序→网络字节序的转换(
htons())
UDP 是无连接协议,通信双方没有预先建立的固定链路,因此:
- 收发函数必须带地址:
sendto()/recvfrom()是 UDP 专用函数,每次发送都必须手动指定目标 IP + 端口,每次接收也能获取发送方地址,因为没有连接帮你记录对方信息;- 服务端必须先收一次:服务端要给客户端回消息,必须先通过
recvfrom()拿到客户端的 IP + 端口,否则根本不知道消息发给谁;- 客户端必须先发一次:客户端要让服务端拿到自己的地址,必须先主动发一条消息给服务端,同时客户端自己也必须提前配置好服务端的 IP + 端口,否则不知道消息发给谁。
| 对比维度 | UDP 多线程双向聊天代码 | UDP 图片文件传输代码 |
|---|---|---|
| 核心功能 | 服务端 / 客户端实时双向消息交互,人机对话 | 客户端单向发图片文件,服务端接收保存 |
| 线程模型 | 双线程(独立收、独立发,并行执行) | 单线程(发→收确认→再发,串行执行) |
| 数据通信方向 | 全双工(双方可同时收发) | 半双工(客户端发数据 + 服务端回确认) |
| 收发函数执行逻辑 | 两个线程各自死循环:一个只调recvfrom,一个只调sendto |
单循环内交替执行:sendto→recvfrom(客户端);recvfrom→sendto(服务端) |
| 结束机制 | 检测到#quit\n指令,主动退出程序 |
客户端文件读取完毕,发送^_^结束标志,服务端检测到后终止循环 |
| 数据类型 | 字符串(键盘输入的文本消息) | 二进制数据(图片文件的字节流) |
| 核心设计目的 | 实现实时交互,解决单线程收发互斥问题 | 实现文件数据传输,通过简单确认保证数据接收完整性 |
| 文件操作 | 无文件打开 / 读写操作 | 包含文件核心操作:客户端读图片、服务端创建 / 写图片 |
| 缓冲区使用 | 仅用于收发文本消息,无需严格匹配长度 | 缓冲区(1024 字节)用于分块读写文件、分块传输,按实际读写长度收发 |
| 确认机制 | 无显式确认,发消息后无需等待回复 | 显式简单确认:服务端收数据后回go on,客户端收到才继续发下一块 |
二、TCP 传输控制协议
1. 核心特征
TCP 是一种面向连接、可靠的传输层协议,核心特点可以概括为:
- 有连接:通信前必须通过三次握手建立固定链路,通信期间保持连接,直到四次挥手断开。
- 可靠传输:通过应答机制、超时重传、拥塞控制保证数据完整、按序到达。
- 流式套接字:
- 发送次数与接收次数不需要一一对应(数据是连续的字节流)。
- 数据之间无边界,可能出现粘包问题。
- 保证数据到达顺序。
- 阻塞特性:同时存在读阻塞和写阻塞。
- 全双工:同一时刻可以双向同时通信。

2. TCP 核心机制:三次握手与四次挥手

三次握手 (1 - 3) : 建立连接
目的:同步双方初始序列号,确认双方收发能力正常。
- 客户端 → 服务器:发送
SYN包,携带初始序列号seq=1000,进入SYN_SENT状态。- 服务器 → 客户端:回复
SYN+ACK包,携带自己的初始序列号seq=8000,并确认客户端序列号ack=1001,进入SYN_RCVD状态。- 客户端 → 服务器:回复
ACK包,确认服务器序列号ack=8001,双方进入ESTABLISHED状态,连接建立完成。
四次挥手 (7 - 10): 断开连接
目的:保证双方数据都传输完毕后再断开连接。
- 客户端 → 服务器:发送
FIN包,请求关闭连接,进入FIN_WAIT_1状态。- 服务器 → 客户端:回复
ACK包,确认关闭请求,进入CLOSE_WAIT状态。- 服务器 → 客户端:发送
FIN包,请求关闭连接,进入LAST_ACK状态。- 客户端 → 服务器:回复
ACK包,确认关闭请求,进入TIME_WAIT状态,等待 2MSL 后彻底关闭。
TCP 可靠传输与拥塞控制
- 应答机制(ACK):接收方收到数据后,会回复确认号,告知发送方数据已收到。
- 超时重传:发送方发送数据后启动定时器,若未收到 ACK,则重新发送数据。
- 拥塞控制:通过慢启动、拥塞避免、快重传、快恢复算法,避免网络拥塞导致的丢包和延迟。
3. TCP 编程流程

TCP 编程基于流式套接字(SOCK_STREAM),严格区分客户端和服务器端:
编程流程
- 服务器端:
socket()→bind()→listen()→accept()→ 循环read()/write()→close()- 客户端:
socket()→connect()→ 循环write()/read()→close()
4. 核心函数
1. socket():创建 TCP 套接字
int socket(int domain, int type, int protocol);
功能
向内核申请创建一个流式套接字(TCP),返回套接字文件描述符,是 TCP 通信的入口。
参数
domain:地址族,TCP 网络编程固定为PF_INET == AF_INET(互联网程序);单机通信用PF_UNIX == AF_UNIX。type:套接字类型,TCP 必须填SOCK_STREAM(流式套接字)。protocol:协议,填0表示自动适配应用层协议(TCP 场景默认匹配 TCP 协议)。
返回值
- 成功:返回申请的套接字 ID(非负整数);
- 失败:返回
-1,并设置errno。
2. bind():绑定 TCP 套接字地址
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
功能
将套接字与指定的 IP 地址、端口号绑定,用于从该接口接收客户端连接(TCP 服务器端必须调用,客户端可省略)。
参数
sockfd:socket()创建的套接字文件描述符。my_addr:物理接口的结构体指针,通用地址结构为struct sockaddr,实际使用网络地址结构struct sockaddr_in(需强转),包含地址族、端口、IP 等信息。addrlen:my_addr结构体的长度。
返回值
- 成功:返回
0; - 失败:返回
-1,并设置errno。
3. listen():TCP 监听套接字
int listen(int sockfd, int backlog);
功能
将套接字设置为监听状态,等待客户端的连接请求,仅 TCP 服务器端使用。
参数
sockfd:socket()创建的套接字 ID。backlog:允许等待的最大连接数(内核维护的连接队列长度)。
返回值
- 成功:返回
0; - 失败:返回
-1,并设置errno。
4. accept():TCP 接受客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
从监听队列中取出一个有效的客户端连接,为该连接创建新的通信套接字,后续数据交互基于新套接字完成,仅 TCP 服务器端使用。
参数
sockfd:listen()后的监听套接字 ID。addr:
(1) 如果该值为NULL ,表示不论客户端是谁都接入。(2) 如果要获取客户端信息,则事先定义变量,并传入变量地址,函数执行完毕将会将客户端信息存储到该变量中。
addrlen:客户端地址结构体大小,若addr为NULL,则该值也为NULL。
返回值
- 成功:返回用于通信的新套接字 ID;
- 失败:返回
-1,并设置errno。
5. connect():TCP 客户端发起连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能
客户端专用函数,向目标服务器发起连接请求,触发 TCP 三次握手,建立通信链路。
参数
sockfd:socket()创建的客户端套接字 ID。addr:远程目标主机的地址信息(服务器 IP + 端口)。addrlen:地址结构体(参数二)的长度。
返回值
- 成功:返回
0; - 失败:返回
-1,并设置errno。
6. recv():TCP 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能
从指定的 sockfd 套接字中以 flags 方式获取长度为 len 字节的数据到指定的 buff 内存中。
参数
sockfd:套接字 ID
如果服务器则是accept的返回的新fd如果客户端则是socket的返回的旧fd
buf:存储数据的本地内存(一般为数组 / 动态内存)。len:要获取的数据长度。flags:获取数据的方式,填0表示阻塞接收。
返回值
- 成功:返回实际接收的数据长度(≤
len); - 失败:返回
-1,并设置errno; - 对端关闭连接:返回
0。
7. send():TCP 发送数据
ssize_t send(int sockfd, const void *msg, size_t len, int flags);
功能
参数
sockfd:套接字 ID
如果服务器则是accept的返回的新fd如果客户端则是socket的返回的旧fd
msg:要发送的数据缓冲区。len:要发送的数据长度。flags:发送方式,填0表示阻塞发送。
返回值
- 成功:返回实际发送的数据长度;
- 失败:返回
-1,并设置errno。
8. close():关闭 TCP 套接字
int close(int fd);
功能
关闭套接字文件描述符,释放内核资源,触发 TCP 四次挥手,断开通信链路。
参数
fd:套接字 ID(服务器端需关闭监听 fd 和通信 fd,客户端关闭自身 fd)。
返回值
- 成功:返回
0; - 失败:返回
-1,并设置errno。
示例1 TCP 多线程实现双向实时聊天 :
服务器端 :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* 系统类型相关头文件 */
#include <sys/socket.h> /* 套接字核心头文件 */
#include <netinet/in.h> /* 网络地址结构头文件 */
#include <netinet/ip.h>
#include <time.h>
#include <pthread.h> /* 线程相关头文件 */
typedef struct sockaddr*(SA); // 简化结构体指针类型定义
// 线程1:负责接收客户端发送的数据
void* th1(void* arg)
{
// 解析传入的通信套接字描述符
int conn = *(int*)arg;
while (1) // 循环接收数据
{
char buf[512] = {0}; // 数据接收缓冲区
// 接收客户端数据
// 返回值:>0 实际接收字节数;==0 客户端断开连接;-1 接收错误
int rd_ret = recv(conn, buf, sizeof(buf), 0);
if (rd_ret <= 0) // 接收失败或客户端断开
{
exit(0); // 退出进程
}
if (0 == strcmp(buf, "#quit\n")) // 检测退出指令
{
exit(0); // 退出进程
}
printf("from cli:%s", buf); // 打印客户端发送的内容
fflush(stdout); // 强制刷新输出缓冲区
}
}
// 线程2:负责向客户端发送数据
void* th2(void* arg)
{
// 解析传入的通信套接字描述符
int conn = *(int*)arg;
while (1) // 循环发送数据
{
char buf[512] = {0}; // 数据发送缓冲区
printf("to cli:"); // 提示输入发送内容
fgets(buf, sizeof(buf), stdin); // 从标准输入读取要发送的内容
send(conn, buf, strlen(buf), 0); // 发送数据到客户端
if (0 == strcmp(buf, "#quit\n")) // 检测退出指令
{
exit(0); // 退出进程
}
}
}
int main(int argc, char** argv)
{
// 1. 创建监听套接字(打开网络设备)
// AF_INET:IPv4协议族;SOCK_STREAM:流式套接字(TCP);0:默认协议
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listfd) // 套接字创建失败
{
perror("socket"); // 打印错误信息
return 1; // 异常退出
}
// 定义服务器地址结构和客户端地址结构
struct sockaddr_in ser, cli;
// 清空地址结构体(避免脏数据)
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET; // 设置地址族为IPv4
ser.sin_port = htons(50000); // 设置端口号(50000),htons转换为网络字节序
ser.sin_addr.s_addr = INADDR_ANY; // 绑定本机所有可用IP地址
// 2. 绑定套接字到指定IP和端口
int ret = bind(listfd, (SA)&ser, sizeof(ser));
if (-1 == ret) // 绑定失败
{
perror("bind"); // 打印错误信息
return 1; // 异常退出
}
// 3. 将套接字设置为监听状态
// 参数2:半连接队列长度(最多允许3个未完成三次握手的连接)
listen(listfd, 3);
socklen_t len = sizeof(cli); // 客户端地址结构长度
// 4. 阻塞等待客户端连接(触发TCP三次握手)
// 返回值:通信套接字(用于和客户端收发数据)
int conn = accept(listfd, (SA)&cli, &len);
if (-1 == conn) // 连接建立失败
{
perror("accept"); // 打印错误信息
return 1; // 异常退出
}
// 创建两个线程,分别处理接收和发送数据
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, th1, &conn); // 创建接收线程
pthread_create(&tid2, NULL, th2, &conn); // 创建发送线程
// 等待线程结束(阻塞)
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 7. 关闭套接字(断开连接)
close(listfd); // 关闭监听套接字
close(conn); // 关闭通信套接字
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <arpa/inet.h>
#include <pthread.h>
typedef struct sockaddr*(SA);
void* th1(void* arg) // 线程1:负责接收服务器发送的数据
{
int conn = *(int*)arg; // 解析传入的通信套接字描述符
while (1)
{
char buf[512] = {0};
// 返回值 >0 实际收到的字节数 ==0 对方断开 -1 错误
int rd_ret = recv(conn, buf, sizeof(buf), 0);
if (rd_ret <= 0)
{
exit(0);
}
if (0 == strcmp(buf, "#quit\n"))
{
exit(0);
}
printf("from ser:%s", buf);
fflush(stdout);
}
}
void* th2(void* arg) // 线程2:负责向服务器发送数据
{
int conn = *(int*)arg;
while (1)
{
char buf[512] = {0};
printf("to ser:");
fgets(buf, sizeof(buf), stdin);
send(conn, buf, strlen(buf), 0); // 发送数据到服务器
if (0 == strcmp(buf, "#quit\n"))
{
exit(0);
}
}
}
int main(int argc, char** argv)
{
// 1 创建套接字
// AF_INET:IPv4协议族;SOCK_STREAM:流式套接字(TCP);0:默认协议
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return 1;
}
struct sockaddr_in ser;
//清空 结构体(避免脏数据)
bzero(&ser, sizeof(ser));
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;
}
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, th1, &sockfd);
pthread_create(&tid2, NULL, th2, &sockfd);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 5.断开连接
close(sockfd);
return 0;
}
示例2 : TCP 文件传输(图片传输)
客户端发送图片数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <arpa/inet.h>
#include <fcntl.h>
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;
//清空 结构体
bzero(&ser, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
// 2 ,主动向服务器发起TCP连接(触发三次握手)
// (SA)&ser:服务器地址结构体 | sizeof(ser):结构体长度
int ret = connect(sockfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("connect");
return 1;
}
// O_RDONLY:只读方式打开 | 路径:客户端本地的1.png文件
int fd = open("/home/linux/1.png",O_RDONLY);
if(-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[1024]={0};
// 从文件读取数据到缓冲区
// ret:>0 实际读取字节数 | ==0 文件读取到末尾 | -1 读取错误
ret = read(fd,buf,sizeof(buf));
if(ret<=0)
{
break;
}
// 3. 发送数据 返回值 >0 实际发送的字节数 ==0 网络状态不好,没有发出数据 -1错误
send(sockfd,buf,ret,0);
// 清空缓冲区,准备接收服务器响应
bzero(buf,sizeof(buf));
// 4. 接收数据 >0 实际接收的字节数 ==0 表示断开 -1 错误
int rd_ret = recv(sockfd,buf,sizeof(buf),0);
if(rd_ret<=0)
{
break;
}
}
// 5.断开连接
close(sockfd);
close(fd);
return 0;
}
服务端接收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <fcntl.h>
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
// 1 打开网络设备,获得文件描述(套接字)
// 监听套接字
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listfd)
{
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 = INADDR_ANY;
// 2 将监听套接字与指定IP+端口绑定,让客户端能找到服务器
// (SA)&ser:将sockaddr_in强转为sockaddr*(通用地址结构体)
int ret = bind(listfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("bind");
return 1;
}
// 3 让监听套接字进入被动监听状态,参数2:半连接队列长度(三次握手未完成的连接数)
listen(listfd, 3);
// 客户端地址结构体长度(accept需要传入地址长度的指针)
socklen_t len = sizeof(cli);
// 4 建立链接 (触发三次握手)
// 通信套接字,表示从服务端来看,conn 是客户端的套接字
// 返回值:>0 实际接收字节数 | ==0 客户端断开连接 | -1 接收错误
int conn = accept(listfd, (SA)&cli, &len);
if (-1 == conn)
{
perror("accept");
return 1;
}
int fd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
char buf[1024]={0};
// 5 接收数据
// 返回值 >0 实际收到的字节数 ==0 对方断开 -1 错误
int rd_ret = recv(conn,buf,sizeof(buf),0);
if(rd_ret<=0)
{
break;
}
// 将接收的数据写入文件(保存图片)
write(fd,buf,rd_ret);
// 6 发送数据
bzero(buf,sizeof(buf));
strcpy(buf,"go on");
send(conn,buf,strlen(buf),0); // 告诉对方继续发送
}
// 7 断开链接
close(listfd);
close(conn);
close(fd);
return 0;
}
TCP 两类代码(聊天 / 文件传输)核心重点
| 对比维度 | TCP 多线程双向聊天 | TCP 图片文件传输 |
|---|---|---|
| 核心功能 | 服务端 & 客户端实时双向文本交互,支持人机对话聊天 | 客户端单向发送图片文件,服务端接收并本地保存为新文件 |
| 线程模型 | 双线程(独立收 / 独立发,并行执行),解决收发互斥 | 单线程(串行执行:发→收确认 / 收→写文件,无线程) |
| 数据通信方向 | 全双工(双方可同时收发数据,无先后依赖) | 半双工(客户端发数据→服务端回确认,有严格先后顺序) |
| 核心数据类型 | 字符串(键盘输入的文本消息,按行交互) | 二进制字节流(图片文件数据,分块传输) |
| 收发函数执行逻辑 | 两个线程各自死循环:一个只调recv,一个只调send |
单循环内交替执行:客户端:read→send→recv服务端:recv→write→send |
| 结束机制 | 检测到#quit\n指令,主动调用exit(0)退出程序 |
无自定义结束指令,通过read/recv返回≤0 触发:客户端:文件读取完毕服务端:客户端断开连接 |
| 文件操作 | 无任何文件打开 / 读写 / 关闭操作,仅内存缓冲区交互 | 包含完整文件操作:客户端:只读打开本地图片服务端:创建 / 清空 / 只写保存新图片,传输完成后关闭文件 |
| 缓冲区设计 | 512 字节缓冲区,仅用于收发文本,无需严格匹配实际长度 | 1024 字节缓冲区,用于文件分块读写 / 传输,按实际读写字节数收发(保证二进制数据完整) |
| 确认机制 | 无显式确认,发消息后无需等待对方回复,实时交互 | 显式简单确认:服务端收数据后回发go on,客户端收到才继续发送下一块数据 |
| 套接字使用细节 | 仅用监听套接字 + 通信套接字完成网络交互,无额外资源 | 除网络套接字外,新增文件描述符,需同时管理网络套接字 + 文件描述符两类资源 |
核心共性(必记)
- 均基于 TCP 协议,创建套接字均为
socket(AF_INET, SOCK_STREAM, 0)(流式套接字);- 服务端遵循固定四步:
socket→bind→listen→accept,客户端固定三步:socket→配置服务端地址→connect;- 均通过
htons()做端口号主机字节序→网络字节序转换,服务端绑定INADDR_ANY适配本机所有 IP;- 收发数据均使用 TCP 专用函数
send()/recv(),依赖通信套接字完成实际数据交互;- 均通过
typedef struct sockaddr*(SA)简化地址结构体强转,用bzero()初始化地址结构体避免脏数据;- 核心系统调用均做错误判断(返回 - 1 时
perror报错并退出),程序结束后主动关闭套接字释放资源。
UDP 与 TCP总结
相同点
- 同属传输层协议,是网络通信的核心基础;
- 都基于 IP 网络层 实现,依赖 IP 地址完成跨网络寻址;
- 服务端都需要
bind()绑定固定端口,客户端都需配置服务端 IP + 端口; - 都需要处理字节序转换(
htons()等),统一网络大端序; - 都支持客户端 / 服务器(C/S)通信模型,可实现双向数据传输。
核心差异
| 对比维度 | UDP(用户数据报协议) | TCP(传输控制协议) |
|---|---|---|
| 连接特性 | 无连接:无需三次握手,直接发数据 | 面向连接:必须三次握手建立链路,四次挥手断开 |
| 可靠性 | 不可靠:不保证顺序、不保证到达、无重传 | 可靠:通过 ACK、超时重传、拥塞控制,保证数据按序完整到达 |
| 数据形式 | 数据报:有边界,收发次数 / 大小必须对应 | 字节流:无边界,可能粘包,收发次数无需对应 |
| 收发函数 | 专用 sendto()/recvfrom(),必须指定目标地址 |
通用 send()/recv(),基于连接无需地址 |
| 线程 / 通信模式 | 聊天用双线程全双工,传文件用单线程半双工 | 聊天用双线程全双工,传文件用单线程半双工 |
| 开销与延迟 | 包头开销小,延迟低,无连接维护成本 | 包头开销大,延迟高,需维护连接状态 |
| 拥塞控制 | 无,发送过快直接丢包 | 有,慢启动 / 拥塞避免等算法,避免网络拥塞 |
| 适用场景 | 实时性优先、可容忍少量丢包 | 可靠性优先、数据完整性要求高 |
- UDP = 快、轻、不可靠,适合实时场景
- TCP = 稳、全、可靠,适合数据完整性要求高的场景
三、TCP 粘包问题
1. 问题本质
发送方多次发送小数据,接收方无法区分数据边界,把多包数据当成一包读取,导致解析错误。
- 根本原因:TCP 是流式套接字,数据是连续的字节流,没有天然的数据包边界,内核会自动合并 / 拆分数据。
- 对比 UDP:UDP 是数据报套接字,有天然边界,收发次数 / 大小必须对应,不会出现粘包。
2. 3 种经典解决方案
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 设置结束标志 | 在每条消息末尾加固定分隔符(如\n、\0),接收方按分隔符拆分数据 |
文本消息、聊天场景 |
| 发送固定大小(struct) | 用结构体定义固定长度的消息,每次收发都按结构体大小读取 | 二进制数据、固定格式消息 |
| 自定义变长协议 | 消息头部先发送数据长度,接收方先读长度,再按长度读取完整数据 | 通用场景、变长消息(最常用) |
四、TCP 流式套接字核心特性(对比 UDP)

流式套接字核心特点(重点)
- 收发次数无需对应:发送 3 次,接收方可以 1 次读完,也可以分多次读,数据是连续的。
- 数据无边界、连续:TCP 只保证字节流的顺序,不保证 “发几次收几次”,这是粘包的根源。
- 顺序保证:TCP 保证数据按发送顺序到达,UDP 不保证顺序。
- 阻塞特性:TCP 同时有读阻塞和写阻塞(发送缓冲区满会阻塞),UDP 只有读阻塞、无写阻塞。
- 双工对比:TCP 是全双工(同时收发),UDP 是半双工(同一时刻只能单向)。
- 发送 / 接收缓冲区:TCP 内核维护缓冲区,数据先写入缓冲区再发送,接收方从缓冲区读取,这也是粘包的诱因。
- 自定义协议示例:
struct MSG结构体,包含数据长度、类型、正文,是解决粘包的经典方案。- 会话(session):TCP 是面向连接的,通信期间会维护会话状态,UDP 无会话。
五、TCP 监听套接字 vs 通信套接字

1. 两个套接字的核心区别(TCP 服务端专属)
| 套接字类型 | 作用 | 生命周期 | 对应图中标识 |
|---|---|---|---|
| 监听套接字(listfd) | 仅用于监听客户端连接请求,不负责数据收发 | 服务端启动后创建,直到服务端关闭 | listfd 3(绑定192.168.31.177:50000) |
| 通信套接字(conn) | 仅用于与客户端收发数据,每个客户端对应一个独立的 conn | 客户端连接成功后创建,连接断开后关闭 | conn 4(对应客户端 a)、conn 5(对应客户端 b) |
2. 核心流程
- 服务端先创建监听套接字,绑定 IP + 端口,进入监听状态。
- 客户端发起连接,
accept()成功后,服务端为该客户端创建专属通信套接字。 - 后续所有数据收发,都通过通信套接字完成,监听套接字继续监听新连接。
- 多客户端场景:一个监听套接字,对应多个通信套接字,每个客户端独立通信互不干扰。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)