一、UDP 用户数据报

1. 核心特征

UDP 是一种无连接、不可靠的传输层协议,核心特点可以概括为

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

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 流式套接字 ===TCP
    SOCK_DGRAM 用户数据报套接字===>UDP
    SOCK_RAW 原始套接字 ===IP
  • protocol:协议,填 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所在的接口设备上发送出去。
        注意:如果是客户端,则该函数可以省略,由默认接口发送数据。
参数
  • sockfdsocket() 创建的套接字文件描述符。
  • 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; ///地址IP
        char sin_zero[8]; ////占位
};
struct in_addr
{
        in_addr_t s_addr;
}
  • addrlenmy_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_addrNULL,则该值也为 NULL
返回值
  • 成功:返回实际接收到的数据长度;
  • 失败:返回 -1,并设置 errno

3.5 close():关闭 UDP 套接字

int close(int fd);

功能

关闭套接字文件描述符,释放内核分配的网络通信资源,结束 UDP 通信。

参数
  • fdsocket() 创建的套接字 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 是无连接协议,通信双方没有预先建立的固定链路,因此:

  1. 收发函数必须带地址sendto()/recvfrom() 是 UDP 专用函数,每次发送都必须手动指定目标 IP + 端口,每次接收也能获取发送方地址,因为没有连接帮你记录对方信息;
  2. 服务端必须先收一次:服务端要给客户端回消息,必须先通过recvfrom()拿到客户端的 IP + 端口,否则根本不知道消息发给谁;
  3. 客户端必须先发一次:客户端要让服务端拿到自己的地址,必须先主动发一条消息给服务端,同时客户端自己也必须提前配置好服务端的 IP + 端口,否则不知道消息发给谁。
对比维度 UDP 多线程双向聊天代码 UDP 图片文件传输代码
核心功能 服务端 / 客户端实时双向消息交互,人机对话 客户端单向发图片文件,服务端接收保存
线程模型 双线程(独立收、独立发,并行执行) 单线程(发→收确认→再发,串行执行)
数据通信方向 全双工(双方可同时收发) 半双工(客户端发数据 + 服务端回确认)
收发函数执行逻辑 两个线程各自死循环:一个只调recvfrom,一个只调sendto 单循环内交替执行:sendtorecvfrom(客户端);recvfromsendto(服务端)
结束机制 检测到#quit\n指令,主动退出程序 客户端文件读取完毕,发送^_^结束标志,服务端检测到后终止循环
数据类型 字符串(键盘输入的文本消息) 二进制数据(图片文件的字节流)
核心设计目的 实现实时交互,解决单线程收发互斥问题 实现文件数据传输,通过简单确认保证数据接收完整性
文件操作 无文件打开 / 读写操作 包含文件核心操作:客户端读图片、服务端创建 / 写图片
缓冲区使用 仅用于收发文本消息,无需严格匹配长度 缓冲区(1024 字节)用于分块读写文件、分块传输,按实际读写长度收发
确认机制 无显式确认,发消息后无需等待回复 显式简单确认:服务端收数据后回go on,客户端收到才继续发下一块

二、TCP 传输控制协议

1. 核心特征

TCP 是一种面向连接、可靠的传输层协议,核心特点可以概括为:

  • 有连接:通信前必须通过三次握手建立固定链路,通信期间保持连接,直到四次挥手断开。
  • 可靠传输:通过应答机制、超时重传、拥塞控制保证数据完整、按序到达。
  • 流式套接字
    1. 发送次数与接收次数不需要一一对应(数据是连续的字节流)。
    2. 数据之间无边界,可能出现粘包问题
    3. 保证数据到达顺序。
  • 阻塞特性:同时存在读阻塞写阻塞
  • 全双工:同一时刻可以双向同时通信。

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

三次握手 (1 - 3) : 建立连接

目的:同步双方初始序列号,确认双方收发能力正常。

  1. 客户端 → 服务器:发送 SYN 包,携带初始序列号 seq=1000,进入 SYN_SENT 状态。
  2. 服务器 → 客户端:回复 SYN+ACK 包,携带自己的初始序列号 seq=8000,并确认客户端序列号 ack=1001,进入 SYN_RCVD 状态。
  3. 客户端 → 服务器:回复 ACK 包,确认服务器序列号 ack=8001,双方进入 ESTABLISHED 状态,连接建立完成。
四次挥手 (7 - 10): 断开连接

目的:保证双方数据都传输完毕后再断开连接。

  1. 客户端 → 服务器:发送 FIN 包,请求关闭连接,进入 FIN_WAIT_1 状态。
  2. 服务器 → 客户端:回复 ACK 包,确认关闭请求,进入 CLOSE_WAIT 状态。
  3. 服务器 → 客户端:发送 FIN 包,请求关闭连接,进入 LAST_ACK 状态。
  4. 客户端 → 服务器:回复 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 服务器端必须调用,客户端可省略)。

参数
  • sockfdsocket() 创建的套接字文件描述符。
  • my_addr:物理接口的结构体指针,通用地址结构为 struct sockaddr,实际使用网络地址结构 struct sockaddr_in(需强转),包含地址族、端口、IP 等信息。
  • addrlenmy_addr 结构体的长度。
返回值
  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

3. listen():TCP 监听套接字

int listen(int sockfd, int backlog);

功能

将套接字设置为监听状态,等待客户端的连接请求,仅 TCP 服务器端使用。

参数
  • sockfdsocket() 创建的套接字 ID。
  • backlog:允许等待的最大连接数(内核维护的连接队列长度)。
返回值
  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

4. accept():TCP 接受客户端连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能

从监听队列中取出一个有效的客户端连接,为该连接创建新的通信套接字,后续数据交互基于新套接字完成,仅 TCP 服务器端使用。

参数
  • sockfdlisten() 后的监听套接字 ID。
  • addr
(1) 如果该值为NULL ,表示不论客户端是谁都接入。
(2) 如果要获取客户端信息,则事先定义变量,并传入变量地址,函数执行完毕将会将客户端
信息存储到该变量中。
  • addrlen:客户端地址结构体大小,若 addrNULL,则该值也为 NULL
返回值
  • 成功:返回用于通信的新套接字 ID;
  • 失败:返回 -1,并设置 errno

5. connect():TCP 客户端发起连接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能

客户端专用函数,向目标服务器发起连接请求,触发 TCP 三次握手,建立通信链路。

参数
  • sockfdsocket() 创建的客户端套接字 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);

功能
从 msg 所在的内存中获取长度为 len 的数据以 flags 方式写入到 sockfd 对应的套接字中。
参数
  • 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 单循环内交替执行:客户端:readsendrecv服务端:recvwritesend
结束机制 检测到#quit\n指令,主动调用exit(0)退出程序 无自定义结束指令,通过read/recv返回≤0 触发:客户端:文件读取完毕服务端:客户端断开连接
文件操作 无任何文件打开 / 读写 / 关闭操作,仅内存缓冲区交互 包含完整文件操作:客户端:只读打开本地图片服务端:创建 / 清空 / 只写保存新图片,传输完成后关闭文件
缓冲区设计 512 字节缓冲区,仅用于收发文本,无需严格匹配实际长度 1024 字节缓冲区,用于文件分块读写 / 传输,按实际读写字节数收发(保证二进制数据完整)
确认机制 无显式确认,发消息后无需等待对方回复,实时交互 显式简单确认:服务端收数据后回发go on,客户端收到才继续发送下一块数据
套接字使用细节 仅用监听套接字 + 通信套接字完成网络交互,无额外资源 除网络套接字外,新增文件描述符,需同时管理网络套接字 + 文件描述符两类资源

 核心共性(必记)

  1. 均基于 TCP 协议,创建套接字均为socket(AF_INET, SOCK_STREAM, 0)(流式套接字);
  2. 服务端遵循固定四步socketbindlistenaccept,客户端固定三步socket配置服务端地址connect
  3. 均通过htons()做端口号主机字节序→网络字节序转换,服务端绑定INADDR_ANY适配本机所有 IP;
  4. 收发数据均使用 TCP 专用函数send()/recv(),依赖通信套接字完成实际数据交互;
  5. 均通过typedef struct sockaddr*(SA)简化地址结构体强转,用bzero()初始化地址结构体避免脏数据;
  6. 核心系统调用均做错误判断(返回 - 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)

 流式套接字核心特点(重点)

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

五、TCP 监听套接字 vs 通信套接字

客户端刚开始连接服务器的,先找listfd(监听套接字,IP+port) .一旦成功建立连接,服务端会产生一个新的套接字conn(通信套接字)。对于服务端而言,如果需要给第一个客户端通信的话,给 下图的 4 发数

1. 两个套接字的核心区别(TCP 服务端专属)

套接字类型 作用 生命周期 对应图中标识
监听套接字(listfd) 仅用于监听客户端连接请求,不负责数据收发 服务端启动后创建,直到服务端关闭 listfd 3(绑定192.168.31.177:50000
通信套接字(conn) 仅用于与客户端收发数据,每个客户端对应一个独立的 conn 客户端连接成功后创建,连接断开后关闭 conn 4(对应客户端 a)、conn 5(对应客户端 b)

2. 核心流程

  1. 服务端先创建监听套接字,绑定 IP + 端口,进入监听状态。
  2. 客户端发起连接,accept() 成功后,服务端为该客户端创建专属通信套接字
  3. 后续所有数据收发,都通过通信套接字完成,监听套接字继续监听新连接。
  4. 多客户端场景:一个监听套接字,对应多个通信套接字,每个客户端独立通信互不干扰。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐