Linux专题八:生产者消费者,读写者模型以及网络编程

一、网络基础概念

1. 核心定义

概念 本质说明 关键特性
网络 将独立自主的计算机通过传输介质、网络设备连接起来,实现数据交互的系统 核心组件:计算机(主机)、传输介质、网络设备、网络协议
互联网 由多个独立网络相互连接形成的全球性网络(因特网是互联网的典型代表) 基于 TCP/IP 协议簇,实现跨网络、跨地域的主机通信
IP 地址 网络中主机的唯一标识(类似 "学号"),用于定位主机 分类:IPv4(32 位,如192.168.2.27)、IPv6(64 位,如248e:35:1816:558:91f5:1772:2c4:8fb7
MAC 地址 网络设备(网卡)的物理地址(48 位,类似 "姓名"),用于链路层识别设备 全球唯一,固化在网卡硬件中,适用于局域网内设备通信
端口号 主机上应用程序的唯一标识(short类型,范围 0-65535),用于区分同一主机的不同进程 核心作用:IP+端口确定网络中唯一进程(套接字地址);常用端口:HTTP 80、HTTPS 443
传输介质 数据传输的物理载体 分类:双绞线、同轴电缆、光纤(有线);无线(电磁波,如 Wi-Fi)
网络设备 实现网络连接、数据转发的硬件设备 核心设备:交换机(局域网内数据转发)、路由器(跨网络数据转发)
网络协议 网络通信的规则集合(软件层面),确保数据有序、正确传输 核心协议:TCP(可靠传输)、UDP(快速传输)、IP(地址路由)、HTTP/HTTPS(应用层交互)

2. 网络分层模型

(1)OSI 七层模型(从下到上)
层级 核心功能 对应组件 / 协议
物理层 传输原始比特流,定义物理接口、传输介质特性 双绞线、光纤、网卡物理接口
数据链路层 封装帧,处理链路错误、MAC 地址寻址 交换机、MAC 地址、ARP 协议
网络层 实现路由转发,确定跨网络的传输路径 路由器、IP 地址、ICMP 协议(ping 基于此)
传输层 提供端到端传输服务,负责流量控制、差错恢复 TCP 协议、UDP 协议、端口号
会话层 建立、维护、终止进程间通信会话 会话管理、同步机制
表示层 数据格式转换、加密解密、压缩解压 字符编码转换、SSL/TLS(HTTPS 底层)
应用层 提供具体应用服务,面向用户需求 HTTP/HTTPS、FTP、DNS 等协议
(2)TCP/IP 四层模型(实际应用主流)
层级 对应 OSI 层级 核心功能
网际接口层 物理层 + 数据链路层 处理硬件接口和局域网内数据传输
网络层 网络层 IP 地址路由、跨网络转发
传输层 传输层 TCP/UDP 协议、端到端传输
应用层 会话层 + 表示层 + 应用层 HTTP/HTTPS、FTP 等应用协议
(3)分层的核心优势
  1. 独立性:各层独立实现功能,无需关注下层实现细节(如应用层无需关心物理层是有线还是无线);
  2. 灵活性:某层技术更新(如物理层从双绞线升级为光纤),不影响其他层级;
  3. 易实现维护:复杂问题分解为独立子问题,降低开发和调试难度;
  4. 标准化:明确各层功能边界,促进不同厂商设备、软件的兼容性。

3. 常用网络指令

指令 功能说明 示例
ifconfig(Linux)/ipconfig(Windows) 查看网络接口信息(IP 地址、MAC 地址、子网掩码等) ifconfig(Linux 输出网卡 IP、MAC 等详情)
ping + IP地址 测试主机是否可达(基于 ICMP 协议) ping 127.0.0.1(测试本机网络正常性)、ping 192.168.2.27(测试局域网主机)
netstat -natp 查看网络连接状态、端口占用、TCP/UDP 协议状态 netstat -natp(查看所有 TCP/UDP 连接,包括监听、建立连接状态)

二、同步模型(生产者 - 消费者、读者 - 写者)

1. 生产者 - 消费者模型

(1)核心概念
  • 生产者:生成数据并放入共享 "缓冲区" 的进程 / 线程(如生成日志数据的程序);
  • 消费者:从缓冲区取出数据并处理的进程 / 线程(如分析日志的程序);
  • 缓冲区:中间媒介(队列 / 数组结构),平衡生产者生产速度和消费者消费速度;
  • 核心约束:同一时刻,生产者和消费者不可同时访问缓冲区(互斥访问)。
(2)核心优点
  1. 解耦:生产者与消费者无需直接通信,仅通过缓冲区交互,降低模块依赖;
  2. 平衡速度:生产者过快时缓冲区暂存数据,消费者过快时缓冲区缓存不足则等待;
  3. 支持并发:多个生产者可同时生产,多个消费者可同时消费,提高系统吞吐量。
(3)实现示例(基于线程 + 信号量 + 队列)
cpp 复制代码
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUF_SIZE 5  // 缓冲区大小
int buffer[BUF_SIZE]; // 共享缓冲区(队列)
int in = 0; // 生产者写入位置
int out = 0; // 消费者读取位置

sem_t empty; // 信号量:缓冲区空位数(初始为BUF_SIZE)
sem_t full;  // 信号量:缓冲区数据量(初始为0)
pthread_mutex_t mutex; // 互斥锁:保护缓冲区访问

// 生产者线程函数
void* producer(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 10; i++) {
        int data = rand() % 100; // 生成随机数据
        sem_wait(&empty); // P操作:获取空缓冲区(无空位则阻塞)
        pthread_mutex_lock(&mutex); // 加锁:保护缓冲区写入

        // 写入缓冲区
        buffer[in] = data;
        printf("生产者%d:写入数据%d,位置%d\n", id, data, in);
        in = (in + 1) % BUF_SIZE;

        pthread_mutex_unlock(&mutex); // 解锁
        sem_post(&full); // V操作:增加数据量(唤醒消费者)
        sleep(1); // 模拟生产耗时
    }
    pthread_exit(NULL);
}

// 消费者线程函数
void* consumer(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 10; i++) {
        sem_wait(&full); // P操作:获取数据(无数据则阻塞)
        pthread_mutex_lock(&mutex); // 加锁:保护缓冲区读取

        // 读取缓冲区
        int data = buffer[out];
        printf("消费者%d:读取数据%d,位置%d\n", id, data, out);
        out = (out + 1) % BUF_SIZE;

        pthread_mutex_unlock(&mutex); // 解锁
        sem_post(&empty); // V操作:增加空位数(唤醒生产者)
        sleep(2); // 模拟消费耗时
    }
    pthread_exit(NULL);
}

int main() {
    // 初始化信号量和互斥锁
    sem_init(&empty, 0, BUF_SIZE);
    sem_init(&full, 0, 0);
    pthread_mutex_init(&mutex, NULL);

    pthread_t p1, p2, c1, c2;
    int p_id1 = 1, p_id2 = 2, c_id1 = 1, c_id2 = 2;

    // 创建2个生产者、2个消费者
    pthread_create(&p1, NULL, producer, &p_id1);
    pthread_create(&p2, NULL, producer, &p_id2);
    pthread_create(&c1, NULL, consumer, &c_id1);
    pthread_create(&c2, NULL, consumer, &c_id2);

    // 等待所有线程执行完毕
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    pthread_join(c1, NULL);
    pthread_join(c2, NULL);

    // 销毁资源
    sem_destroy(&empty);
    sem_destroy(&full);
    pthread_mutex_destroy(&mutex);
    return 0;
}

编译运行:

bash 复制代码
gcc producer_consumer.c -o pc -lpthread
./pc

2. 读者 - 写者模型

(1)核心概念
  • 读者:仅读取共享资源,不修改数据(如查询数据库的进程);
  • 写者:修改共享资源(如更新数据库的进程);
  • 共享资源:被多进程 / 线程共同访问的数据(如配置文件、数据库表);
  • 核心约束 (读写锁特性):
    1. 读 - 读可并发:多个读者可同时读取,不阻塞;
    2. 读 - 写互斥:读者读取时写者阻塞,写者写入时读者阻塞;
    3. 写 - 写互斥:多个写者不可同时写入,避免数据覆盖。
(2)应用场景

读操作远多于写操作的场景(如新闻网站文章浏览、配置文件读取),比互斥锁效率更高。

(3)实现示例(基于读写锁)
cpp 复制代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_data = 100; // 共享资源
pthread_rwlock_t rwlock; // 读写锁

// 读者线程函数
void* reader(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 5; i++) {
        pthread_rwlock_rdlock(&rwlock); // 加读锁
        printf("读者%d:读取数据=%d\n", id, g_data);
        sleep(1); // 模拟读耗时
        pthread_rwlock_unlock(&rwlock); // 解读锁
        sleep(1); // 读者之间间隔
    }
    pthread_exit(NULL);
}

// 写者线程函数
void* writer(void* arg) {
    int id = *(int*)arg;
    for (int i = 0; i < 3; i++) {
        pthread_rwlock_wrlock(&rwlock); // 加写锁
        g_data += 50;
        printf("【写者%d】:修改数据=%d\n", id, g_data);
        sleep(2); // 模拟写耗时
        pthread_rwlock_unlock(&rwlock); // 解写锁
        sleep(2); // 写者之间间隔
    }
    pthread_exit(NULL);
}

int main() {
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t r1, r2, r3, w1;
    int r_id1 = 1, r_id2 = 2, r_id3 = 3, w_id1 = 1;

    // 创建3个读者、1个写者
    pthread_create(&r1, NULL, reader, &r_id1);
    pthread_create(&r2, NULL, reader, &r_id2);
    pthread_create(&r3, NULL, reader, &r_id3);
    pthread_create(&w1, NULL, writer, &w_id1);

    // 等待所有线程结束
    pthread_join(r1, NULL);
    pthread_join(r2, NULL);
    pthread_join(r3, NULL);
    pthread_join(w1, NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

编译运行:

cpp 复制代码
gcc reader_writer.c -o rw -lpthread
./rw

三、核心网络协议(TCP/UDP)(都是重点)

1. TCP 协议(传输控制协议)

(1)核心特性
  • 面向连接:通信前需建立连接(三次握手),通信后需释放连接(四次挥手);
  • 可靠传输:通过应答确认、超时重传、乱序重排、去重机制保证数据不丢失、不重复;
  • 流式服务:数据无边界,以字节流形式传输(类似水流持续传输);
  • 流量控制:通过滑动窗口机制避免发送方发送过快导致接收方缓冲区溢出;
  • 一对一通信:一个 TCP 连接仅支持两个端点通信。
(2)TCP 三次握手(建立连接)
1. 核心原理
  • 目的:确认双方发送和接收能力正常,同步序列号(确保数据有序传输);
  • 参与方:客户端(主动发起连接)、服务器(被动监听连接);
  • 关键标志位:SYN(同步请求,值为 1 表示发起同步)、ACK(确认应答,值为 1 表示确认);
  • 序列号:seq(发送方的序列号)、ack(应答序列号,= 对方seq+1,表示确认收到对方数据)。
2. 握手流程(结合示例)
步骤 发起方 报文内容 状态变化
1 客户端 SYN=1seq=x(如 x=100) 客户端:CLOSEDSYN-SENT(等待服务器响应)
2 服务器 SYN=1(发起服务器端同步)、ACK=1(确认客户端同步)、seq=y(如 y=200)、ack=x+1=101 服务器:LISTENSYN-RECV(等待客户端最终确认)
3 客户端 ACK=1(确认服务器同步)、seq=x+1=101ack=y+1=201 客户端:SYN-SENTESTABLISHED(连接建立,可传输数据);服务器:SYN-RECVESTABLISHED
3. 形象类比
  • 客户端:"我要和你建立连接(SYN=1),我的序列号是 100(seq=100)";
  • 服务器:"收到你的请求(ACK=1,ack=101),我也要和你同步(SYN=1),我的序列号是 200(seq=200)";
  • 客户端:"收到你的同步(ACK=1,ack=201),现在可以传数据了(seq=101)"。
(3)TCP 四次挥手(释放连接)
1. 核心原理
  • 目的:确保双方数据都已传输完毕,优雅释放连接;
  • 参与方:客户端(主动关闭)、服务器(被动关闭);
  • 关键标志位:FIN(终止连接请求,值为 1 表示请求关闭)。
2. 挥手流程(结合示例)
步骤 发起方 报文内容 状态变化
1 客户端 FIN=1(请求关闭)、seq=u(如 u=300) 客户端:ESTABLISHEDFIN-WAIT-1(等待服务器确认)
2 服务器 ACK=1(确认关闭请求)、seq=v(如 v=400)、ack=u+1=301 服务器:ESTABLISHEDCLOSE-WAIT(准备关闭自身连接,仍可发送剩余数据);客户端:FIN-WAIT-1FIN-WAIT-2(等待服务器关闭请求)
3 服务器 FIN=1(服务器请求关闭)、seq=w(如 w=450)、ack=u+1=301 服务器:CLOSE-WAITLAST-ACK(等待客户端最终确认)
4 客户端 ACK=1(确认服务器关闭)、seq=u+1=301ack=w+1=451 客户端:FIN-WAIT-2TIME-WAIT(等待 2MSL 时间,避免迟来报文)→ CLOSED;服务器:LAST-ACKCLOSED(连接释放)
4. 关键状态说明
  • TIME-WAIT状态:客户端独有的状态,等待 2MSL(报文最大生存时间),确保迟来的报文被丢弃,避免端口复用导致数据错乱;
  • CLOSE-WAIT状态:服务器独有的状态,此时服务器仍可向客户端发送未传输完的数据。
5. 形象类比
  • 客户端:"我没有数据要发了(FIN=1),我的序列号是 300(seq=300)";
  • 服务器:"收到你的关闭请求(ACK=1,ack=301),我还有点数据没发完(seq=400)";
  • 服务器:"我数据发完了(FIN=1,seq=450),可以关闭了";
  • 客户端:"收到(ACK=1,ack=451),我等一会儿再彻底关闭(TIME-WAIT)"。
(4)TCP 网络编程核心步骤
1. 核心头文件
cpp 复制代码
#include <sys/socket.h>   // 套接字操作
#include <netinet/in.h>  // 网络地址结构体
#include <arpa/inet.h>   // IP地址转换函数
2. 服务器端示例代码
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    // 1. 创建监听套接字(IPv4,TCP协议)
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 绑定IP+端口(服务器端IP和端口固定)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;         // IPv4地址族
    server_addr.sin_port = htons(6000);       // 端口号(主机字节序转网络字节序)
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本地回环IP

    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind绑定失败");
        close(listen_fd);
        exit(1);
    }

    // 3. 设置监听队列(最大等待连接数5)
    if (listen(listen_fd, 5) == -1) {
        perror("listen监听失败");
        close(listen_fd);
        exit(1);
    }
    printf("服务器启动,监听端口6000...\n");

    while (1) {
        // 4. 阻塞等待客户端连接,创建通信套接字
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
        if (conn_fd == -1) {
            perror("accept接收连接失败");
            continue;
        }
        printf("客户端连接成功,conn_fd=%d\n", conn_fd);

        // 5. 接收客户端数据
        char buf[128] = {0};
        ssize_t recv_len = recv(conn_fd, buf, sizeof(buf)-1, 0);
        if (recv_len <= 0) {
            printf("客户端断开连接\n");
            close(conn_fd);
            continue;
        }
        printf("收到客户端数据:%s\n", buf);

        // 6. 向客户端发送响应
        send(conn_fd, "收到你的消息!", strlen("收到你的消息!"), 0);

        // 7. 关闭通信套接字(短连接,一次通信后关闭)
        close(conn_fd);
    }

    // 8. 关闭监听套接字(实际不会执行,需信号处理)
    close(listen_fd);
    return 0;
}
3. 客户端示例代码
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    // 1. 创建通信套接字
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 连接服务器(指定服务器IP+端口)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(6000);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect连接服务器失败");
        close(sock_fd);
        exit(1);
    }
    printf("连接服务器成功!\n");

    // 3. 向服务器发送数据
    char buf[128] = {0};
    printf("请输入要发送的消息:");
    fgets(buf, sizeof(buf), stdin);
    send(sock_fd, buf, strlen(buf)-1, 0); // 去除fgets读取的换行符

    // 4. 接收服务器响应
    memset(buf, 0, sizeof(buf));
    ssize_t recv_len = recv(sock_fd, buf, sizeof(buf)-1, 0);
    if (recv_len > 0) {
        printf("服务器响应:%s\n", buf);
    }

    // 5. 关闭套接字
    close(sock_fd);
    return 0;
}
4. 编译运行
cpp 复制代码
# 编译服务器端
gcc tcp_server.c -o tcp_server
# 编译客户端
gcc tcp_client.c -o tcp_client

# 终端1运行服务器
./tcp_server
# 终端2运行客户端
./tcp_client
(5)tcp协议下的有限状态机
缓冲区问题:

这个图展示的是TCP 的 "字节流服务" 特性 ,核心就是TCP 的发送 / 接收缓冲区对数据的 "拼接 / 拆分" 处理。我结合图给你详细讲解 TCP 缓冲区的工作逻辑:

一、先明确核心概念:TCP 是 "字节流",无数据边界

TCP 不像 UDP 那样以 "数据报" 为单位传输,而是把数据当作连续的字节流 处理 ------ 发送端多次send()的数据会被拼接成一个 "流",接收端recv()时也会从 "流" 里读取数据,不保证和发送端的send()次数对应

二、结合图拆解 TCP 缓冲区的工作流程

这个图分为发送端接收端,涉及 "应用层→传输层(TCP 缓冲区)→网络层" 的处理:

1. 发送端的处理(左侧)
  • 应用层 :发送端调用了 3 次send(),分别发送helloabcdeftest12345
  • TCP 发送缓冲区 :这 3 次send()的数据不会直接发出去,而是先被拼接成连续的字节流helloabcdeftest12345),存入 "TCP 发送缓冲区";
  • 传输层(TCP 报文段) :TCP 会根据网络状况(如 MTU 最大传输单元),将缓冲区里的字节流拆分成多个 TCP 报文段 (图中拆成了 2 段:helloabcdeftest12345),再发送到网络。
2. 接收端的处理(右侧)
  • 传输层(TCP 报文段):接收端收到发送端拆分的 2 个 TCP 报文段;
  • TCP 接收缓冲区 :TCP 会把这 2 个报文段的内容重新拼接成完整的字节流helloabcdeftest12345),存入 "TCP 接收缓冲区";
  • 应用层 :接收端调用 1 次recv(),就能从缓冲区里读取到完整的字节流(helloabcdeftest12345)。
三、TCP 缓冲区的关键特性(结合图总结)
  1. 发送缓冲区:拼接数据 发送端多次send()的数据会被 TCP 自动 "拼接" 成连续字节流,不会保留send()的边界;

  2. 接收缓冲区:重组数据 接收端收到的多个 TCP 报文段,会被 TCP 自动 "重组" 成原始字节流,recv()读取的是连续的字节(可能一次读走多次send()的内容);

  3. 与应用层的 "解耦" 应用层只需要调用send()/recv()操作字节流,不需要关心 TCP 如何拆分 / 重组报文段(这部分由 TCP 协议栈自动处理);

  4. 缓冲区的 "暂存" 作用

    • 发送缓冲区:如果网络拥塞,发送端的数据会暂存在缓冲区,等网络空闲时再发送;
    • 接收缓冲区:如果应用层recv()不及时,接收的数据会暂存在缓冲区,避免数据丢失。
四、举个实际例子(对应图的场景)

发送端代码:

cpp 复制代码
send(sock, "hello", 5, 0);   // 第1次send
send(sock, "abcdef", 6, 0);  // 第2次send
send(sock, "test12345", 9, 0); // 第3次send
接收端代码:
cpp 复制代码
char buf[1024] = {0};
recv(sock, buf, sizeof(buf)-1, 0); // 1次recv就读到了"helloabcdeftest12345"
五、需要注意的问题
  • 接收端recv()的长度 :如果接收端recv()的缓冲区比发送的字节流小,会分多次读取(比如缓冲区只有 10 字节,第一次读helloabcde,第二次读ftest12345);
  • "粘包" 问题 :因为 TCP 是字节流,多次send()的数据会被拼接,接收端无法区分 "原始send()的边界"------ 如果需要区分不同的消息,得自己在应用层加 "分隔符"(比如用\n分割)或 "消息长度"(比如先发送 4 字节的长度,再发送消息内容)。

总结来说:TCP 的发送 / 接收缓冲区是实现 "字节流服务" 的核心,它自动完成了数据的 "拼接、拆分、暂存",让应用层可以像操作 "流" 一样读写数据,不用关心底层的报文段拆分。

2. UDP 协议(用户数据报协议)

(1)核心特性
  • 面向无连接:无需建立连接,直接发送数据,节省连接开销;
  • 不可靠传输:无应答确认、超时重传机制,可能出现数据丢失、乱序;
  • 数据报服务:数据有边界,每次发送一个完整报文,接收方需完整接收;
  • 支持一对多通信:一个 UDP 端口可接收多个客户端的数据;
  • 速度快:无连接和重传开销,适用于实时性要求高的场景。
(2)UDP 与 TCP 的核心区别
对比维度 TCP 协议 UDP 协议
连接方式 面向连接(三次握手 + 四次挥手) 无连接
可靠性 可靠(应答、重传、去重、排序) 不可靠(无应答、可能丢失 / 乱序)
数据传输方式 流式服务(无边界,字节流) 数据报服务(有边界,完整报文)
通信模式 一对一 一对一、一对多、多对多
适用场景 文件传输、HTTP/HTTPS 通信、邮件发送等对可靠性要求高的场景 实时音视频、游戏数据、DNS 查询等对实时性要求高的场景
开销 高(连接建立、流量控制、重传机制) 低(无额外开销)
(3)、UDP 编程完整示例(服务器 + 客户端)
1. UDP 服务器端代码(接收客户端数据并回复)

服务器端逻辑:绑定端口 → 循环接收客户端数据 → 解析发送方地址 → 回复数据。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000       // 服务器监听端口
#define BUF_SIZE 1024   // 数据缓冲区大小

int main() {
    // 1. 创建UDP套接字(AF_INET=IPv4,SOCK_DGRAM=UDP数据报)
    int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_fd == -1) {
        perror("socket 创建失败");
        exit(EXIT_FAILURE);
    }
    printf("UDP套接字创建成功\n");

    // 2. 绑定IP和端口(服务器必须绑定,否则客户端无法定位)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr)); // 初始化地址结构体
    server_addr.sin_family = AF_INET;             // IPv4地址族
    server_addr.sin_port = htons(PORT);           // 端口(主机序转网络序)
    // INADDR_ANY = 0.0.0.0,监听所有网卡的6000端口
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(udp_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind 绑定失败");
        close(udp_fd);
        exit(EXIT_FAILURE);
    }
    printf("UDP服务器启动,绑定端口 %d,等待客户端数据...\n", PORT);

    // 3. 循环接收客户端数据
    char buf[BUF_SIZE] = {0};
    struct sockaddr_in client_addr; // 存储客户端地址
    socklen_t client_len = sizeof(client_addr); // 地址长度

    while (1) {
        // 清空缓冲区
        memset(buf, 0, BUF_SIZE);

        // 接收客户端数据(阻塞等待)
        ssize_t recv_len = recvfrom(udp_fd, buf, BUF_SIZE-1, 0,
                                   (struct sockaddr*)&client_addr, &client_len);
        if (recv_len == -1) {
            perror("recvfrom 接收失败");
            continue;
        }

        // 解析客户端信息
        char client_ip[INET_ADDRSTRLEN] = {0};
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
        int client_port = ntohs(client_addr.sin_port); // 网络序转主机序

        // 打印客户端数据
        printf("收到客户端 [%s:%d] 数据:%s\n", client_ip, client_port, buf);

        // 4. 回复客户端(使用同一个套接字,指定客户端地址)
        char reply_buf[BUF_SIZE] = {0};
        snprintf(reply_buf, BUF_SIZE, "服务器已收到:%s", buf);
        sendto(udp_fd, reply_buf, strlen(reply_buf), 0,
               (struct sockaddr*)&client_addr, client_len);
        printf("已回复客户端 [%s:%d]\n", client_ip, client_port);
    }

    // 5. 关闭套接字(实际不会执行,需手动终止程序)
    close(udp_fd);
    return 0;
}
2. UDP 客户端代码(发送数据到服务器并接收回复)

客户端逻辑:创建套接字 → 指定服务器地址 → 发送数据 → 接收服务器回复。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"  // 服务器IP(本地回环)
#define SERVER_PORT 6000       // 服务器端口
#define BUF_SIZE 1024          // 数据缓冲区大小

int main() {
    // 1. 创建UDP套接字
    int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_fd == -1) {
        perror("socket 创建失败");
        exit(EXIT_FAILURE);
    }
    printf("UDP客户端启动\n");

    // 2. 配置服务器地址(指定要发送的目标)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    // 字符串IP转网络序整数
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("inet_pton IP转换失败");
        close(udp_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 输入要发送的数据
    char send_buf[BUF_SIZE] = {0};
    printf("请输入要发送给服务器的内容(输入quit退出):");
    fgets(send_buf, BUF_SIZE-1, stdin);
    // 去除fgets读取的换行符
    send_buf[strcspn(send_buf, "\n")] = '\0';

    // 退出条件
    if (strcmp(send_buf, "quit") == 0) {
        close(udp_fd);
        return 0;
    }

    // 4. 发送数据到服务器
    ssize_t send_len = sendto(udp_fd, send_buf, strlen(send_buf), 0,
                              (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (send_len == -1) {
        perror("sendto 发送失败");
        close(udp_fd);
        exit(EXIT_FAILURE);
    }
    printf("已发送 %zd 字节到服务器 [%s:%d]\n", send_len, SERVER_IP, SERVER_PORT);

    // 5. 接收服务器回复
    char recv_buf[BUF_SIZE] = {0};
    socklen_t server_len = sizeof(server_addr);
    ssize_t recv_len = recvfrom(udp_fd, recv_buf, BUF_SIZE-1, 0,
                               (struct sockaddr*)&server_addr, &server_len);
    if (recv_len == -1) {
        perror("recvfrom 接收回复失败");
        close(udp_fd);
        exit(EXIT_FAILURE);
    }
    printf("收到服务器回复:%s\n", recv_buf);

    // 6. 关闭套接字
    close(udp_fd);
    return 0;
}
三、编译与运行步骤

1. 编译代码

cpp 复制代码
# 编译服务器端
gcc udp_server.c -o udp_server
# 编译客户端
gcc udp_client.c -o udp_client

四、应用层协议(HTTP/HTTPS)(了解)

1. 核心概念

  • 属于应用层协议,底层基于 TCP 协议(可靠传输);
  • HTTP:明文传输,端口 80,安全性低;
  • HTTPS:加密传输(SSL/TLS 协议),端口 443,安全性高;
  • 核心功能:实现客户端(浏览器 / 应用)与服务器(Web 服务器)的交互(如请求网页、提交表单)。

2. 核心方法(HTTP)

方法 功能说明
GET 从服务器获取资源(如浏览网页),参数拼接在 URL 中,长度有限制
POST 向服务器提交资源(如登录、上传文件),参数放在请求体中,长度无限制

3. 实现示例(C 语言 HTTP 客户端)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    // 1. 创建套接字
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 连接HTTP服务器(以百度为例)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(80); // HTTP默认端口80
    server_addr.sin_addr.s_addr = inet_addr("180.101.50.242"); // 百度IP(示例)

    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect连接失败");
        close(sock_fd);
        exit(1);
    }

    // 3. 发送HTTP GET请求
    char request[] = "GET / HTTP/1.1\r\n"
                     "Host: www.baidu.com\r\n"
                     "Connection: close\r\n\r\n";
    send(sock_fd, request, strlen(request), 0);

    // 4. 接收服务器响应(HTML文档)
    char buf[4096] = {0};
    ssize_t recv_len;
    while ((recv_len = recv(sock_fd, buf, sizeof(buf)-1, 0)) > 0) {
        printf("%s", buf);
        memset(buf, 0, sizeof(buf));
    }

    // 5. 关闭套接字
    close(sock_fd);
    return 0;
}

编译运行:

bash 复制代码
gcc http_client.c -o http_client
./http_client
  • 运行结果:接收百度服务器返回的 HTML 文档(浏览器解析后即为百度首页)。

五、关键补充说明

1. 套接字地址(IP + 端口)

  • 本质:IP地址+端口号的组合,唯一标识网络中的一个进程;
  • 存储方式:通过struct sockaddr_in结构体存储(IPv4),包含地址族、端口号、IP 地址;
  • 核心转换函数:
    • htons():主机字节序转网络字节序(端口号转换);
    • inet_addr():字符串格式 IP(如"127.0.0.1")转无符号整数格式。

2. 长连接与短连接

  • 短连接:一次通信后立即关闭连接(如 TCP 服务器示例中的close(conn_fd));
  • 长连接:多次通信复用同一个连接(如 HTTP/1.1 默认长连接),减少连接建立开销;
  • 适用场景:短连接适用于通信频率低的场景(如查询天气),长连接适用于通信频繁的场景(如聊天软件)。

3. 线程安全补充

  • 线程安全:多线程程序无论调度顺序如何,都能得到正确结果(如使用互斥锁、读写锁保护共享资源);
  • 网络编程中的线程安全:多线程处理客户端连接时,需为每个客户端分配独立的通信套接字和缓冲区,避免共享资源竞争。
相关推荐
代码游侠2 小时前
复习——网络基础知识
网络·笔记·网络协议·算法·http
Web极客码2 小时前
如何在 Linux 中终止一个进程?
linux·运维·服务器
wregjru2 小时前
【C++】2.4 map和set的使用
网络·网络协议·rpc
大聪明-PLUS2 小时前
Linux 中的 GPIO 驱动程序
linux·嵌入式·arm·smarc
蒜丶2 小时前
基于 frp 0.65 tcp 模式,实现web服务&ssh服务内网穿透
网络
Kiyra2 小时前
LinkedHashMap 源码阅读
java·开发语言·网络·人工智能·安全·阿里云·云计算
Clarence Liu2 小时前
虚拟机与容器的差异与取舍
linux·后端·容器
A13247053123 小时前
防火墙配置入门:保护你的服务器
linux·运维·服务器·网络