一、基本框架

客户端                   服务器
  | PUSH ctrl             |
  | data_len+data      |
  |---------------->         |
                             接收数据
                             打印/处理
  |<---------------         |
  |                PULL ctr |
客户端收到 PULL
处理完成,输入下一条或 EXIT
固定长度 ctrl 避免粘包/拆包问题

TCP 是 字节流,不保证字节序。字节序(endianness)只影响多字节整数,而不影响字符数组。

1️⃣ 字节序的概念

  • CPU 内部存储整数时有两种方式:

    • 小端(Little Endian):低字节放低地址,高字节放高地址(x86/AMD 默认)

    • 大端(Big Endian):高字节放低地址,低字节放高地址(网络标准)

  • 字节序影响的是整数类型(如 int, uint32_t),因为一个整数在内存中有多个字节,需要规定顺序。


2️⃣ 字符串在内存中的存储

  • 字符串在 C 中本质上是 一串 char(1 字节),例如:

    char msg[5] = {'P','U','S','H','\0'};
  • 每个字符占 1 字节,所以 字节顺序和内存布局没有歧义

  • 不管是小端还是大端,发送网络后逐字节接收,顺序一致,因此能正确解析。


3️⃣ 为什么整数长度必须网络字节序

  • 假设发送 32 位整数 0x12345678:

    • 小端存储:78 56 34 12

    • 大端存储:12 34 56 78

  • 如果接收方 CPU 与发送方不同端序,直接解析就会出错(得到错误长度)。

  • 因此需要 htonl() → 网络字节序统一为大端。

对于整数类型字段(如长度、序号)必须统一为 网络字节序(big-endian):

uint32_t data_len = strlen(data_buf); 
uint32_t net_len = htonl(data_len); // 转为网络字节序 send_fixed(sock_fd, &net_len, sizeof(net_len));
  • 服务器接收时:

uint32_t net_len; 
recv_fixed(sock_fd, &net_len, sizeof(net_len)); 
uint32_t data_len = ntohl(net_len); // 转回主机字节序
  • 函数说明

    • htonl → host to network long(32位整型)

    • ntohl → network to host long

  • 固定长度控制消息(如 PUSH / PULL / EXIT)可以直接按字节数组发送,不需要网络字节序,因为都是字符数据。

  • 整数类型字段(长度、序号等)必须使用网络字节序,以保证跨机器正确解析。

二、定义协议(common_net.h)

  • 重要点

    • 固定长度控制消息 (CTRL_MSG_MAX) 避免 TCP 粘包/拆包问题。

    • 约定 PUSH → 发送数据长度 → 数据内容 → PULL 响应 → 客户端处理。

  • 设计目的

    • 双方通信流程标准化,客户端/服务端可以正确解析数据。

common_net.h

#ifndef COMMON_NET_H
#define COMMON_NET_H

#include <stdint.h>

#define CTRL_MSG_MAX 16        // 固定长度控制消息
#define CTRL_MSG_PUSH "PUSH"   // 客户端发送数据请求
#define CTRL_MSG_PULL "PULL"   // 服务端响应通知客户端可以处理结果
#define CTRL_MSG_EXIT "EXIT"   // 客户端或服务端退出信号

#endif // COMMON_NET_H

三、server.c

  • 作用:创建 TCP socket 并绑定端口,进入监听状态。

  • 重要点

    • AF_INET + SOCK_STREAM → TCP/IP 流式通信。

    • listen 的 backlog 控制等待队列长度。

    • INADDR_ANY 可监听本机所有网卡,方便跨 PC 访问。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "common_net.h"  // 包含 CTRL_MSG_PUSH / PULL / EXIT / CTRL_MSG_MAX 等宏定义

// --------------------全局变量--------------------
static int listen_fd = -1;          // 监听 socket
static int conn_fd = -1;            // 已连接客户端 socket
static volatile sig_atomic_t g_stop = 0;  // 信号安全停止标志

// --------------------信号处理--------------------
// 捕获 Ctrl+C 或系统终止信号,只设置 g_stop 标志
static void on_sigint(int sig) {
    g_stop = 1;  // 主循环根据该标志退出
}

// --------------------固定长度发送/接收函数--------------------
// TCP 流式 socket 不保证一次 send/recv 完整传输,需要循环直到完成
static bool recv_fixed(int fd, void *buf, size_t len) {
    size_t got = 0;
    while (got < len) {
        ssize_t r = recv(fd, (char*)buf + got, len - got, 0);
        if (r <= 0) return false;  // 0=对端关闭,<0=错误
        got += (size_t)r;          // 累加已接收字节数
    }
    return true;
}

static bool send_fixed(int fd, const void *buf, size_t len) {
    size_t sent = 0;
    while (sent < len) {
        ssize_t r = send(fd, (const char*)buf + sent, len - sent, 0);
        if (r <= 0) return false;  // 0 很少见,<0 出错
        sent += (size_t)r;          // 累加已发送字节数
    }
    return true;
}

// --------------------主函数--------------------
int main(void) {
    // 安装信号处理器,保证 Ctrl+C 或系统退出时可以优雅停止
    signal(SIGINT, on_sigint);
    signal(SIGTERM, on_sigint);

    // --------------------创建 TCP 监听 socket --------------------
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);  // AF_INET: IPv4, SOCK_STREAM: TCP
    if (listen_fd < 0) {
        perror("socket");
        return 1;
    }

    // --------------------绑定 IP 与端口 --------------------
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;           // IPv4
    addr.sin_port = htons(12345);        // 服务端端口号(可以自定义)
    addr.sin_addr.s_addr = INADDR_ANY;   // 监听本机所有网卡,也可以指定具体 IP

    if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(listen_fd);
        return 1;
    }

    // --------------------进入监听状态 --------------------
    if (listen(listen_fd, 5) < 0) {     // backlog=5,允许等待连接队列长度
        perror("listen");
        close(listen_fd);
        return 1;
    }
    printf("[SERVER] Listening on port %d\n", ntohs(addr.sin_port));

    // --------------------等待客户端连接 --------------------
    conn_fd = accept(listen_fd, NULL, NULL);  // 单连接示例,可改成循环支持多客户端
    if (conn_fd < 0) {
        perror("accept");
        close(listen_fd);
        return 1;
    }
    printf("[SERVER] Client connected.\n");

    // --------------------主循环:控制消息 & 数据交互 --------------------
    char ctrl[CTRL_MSG_MAX];   // 固定长度控制消息缓冲区
    uint32_t seq = 0;          // 序号,用于响应消息演示

    while (!g_stop) {
        // 1. 接收固定长度控制消息
        if (!recv_fixed(conn_fd, ctrl, sizeof(ctrl))) {
            printf("[SERVER] client disconnected or recv error.\n");
            break;
        }

        // 2. 根据控制消息类型处理
        if (strncmp(ctrl, CTRL_MSG_PUSH, sizeof(ctrl)) == 0) {
            // 客户端发送数据请求:接收后直接打印(实际可处理二进制数据)
            // 这里示例:假设客户端紧跟 ctrl 发送 uint32_t 数据长度 + 数据内容
            uint32_t data_len = 0;
            if (!recv_fixed(conn_fd, &data_len, sizeof(data_len))) break;  // 读取数据长度
            data_len = ntohl(data_len);  // 网络字节序转主机字节序

            if (data_len > 0 && data_len <= 1024) {  // 最大限制,防止过大
                char buffer[1024] = {0};
                if (!recv_fixed(conn_fd, buffer, data_len)) break;
                printf("[SERVER] <- PUSH: seq=%u len=%u data=\"%.*s\"\n",
                       ++seq, data_len, data_len, buffer);

                // 发送 PULL 响应通知客户端可以处理结果
                char msg[CTRL_MSG_MAX] = {0};
                strncpy(msg, CTRL_MSG_PULL, sizeof(msg) - 1);
                if (!send_fixed(conn_fd, msg, sizeof(msg))) break;
            } else {
                printf("[SERVER] Invalid data length: %u\n", data_len);
            }

        } else if (strncmp(ctrl, CTRL_MSG_EXIT, sizeof(ctrl)) == 0) {
            // 客户端请求退出
            printf("[SERVER] <- EXIT\n");
            break;

        } else {
            // 未知控制消息,打印调试
            printf("[SERVER] <- UNKNOWN CTRL: \"%.*s\"\n", (int)sizeof(ctrl), ctrl);
        }
    }

    // --------------------清理资源 --------------------
    if (conn_fd >= 0) close(conn_fd);
    if (listen_fd >= 0) close(listen_fd);

    printf("[SERVER] Exit.\n");
    return 0;
}

四、client.c

  • 作用:客户端与服务器交互流程实现。

  • 关键点

    • 数据长度用 uint32_t + 网络字节序 (htonl) 保证跨平台兼容。

    • 固定长度控制消息 + 长度 + 数据 → 完整协议链。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "common_net.h"

// --------------------固定长度发送/接收函数--------------------
static bool recv_fixed(int fd, void *buf, size_t len) {
    size_t got = 0;
    while (got < len) {
        ssize_t r = recv(fd, (char*)buf + got, len - got, 0);
        if (r <= 0) return false;
        got += (size_t)r;
    }
    return true;
}

static bool send_fixed(int fd, const void *buf, size_t len) {
    size_t sent = 0;
    while (sent < len) {
        ssize_t r = send(fd, (const char*)buf + sent, len - sent, 0);
        if (r <= 0) return false;
        sent += (size_t)r;
    }
    return true;
}

int main(int argc, char *argv[]) {
    if (argc < 3) {
        printf("Usage: %s <server_ip> <server_port>\n", argv[0]);
        return 1;
    }

    const char *server_ip = argv[1];
    int server_port = atoi(argv[2]);

    // --------------------创建 TCP socket --------------------
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket");
        return 1;
    }

    // --------------------连接服务器 --------------------
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);

    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sock_fd);
        return 1;
    }
    printf("[CLIENT] Connected to %s:%d\n", server_ip, server_port);

    char ctrl[CTRL_MSG_MAX] = {0};
    char data_buf[1024] = {0};
    uint32_t seq = 0;

    // --------------------示例循环:发送 PUSH --------------------
    while (1) {
        printf("Input message to PUSH (or 'exit' to quit): ");
        if (!fgets(data_buf, sizeof(data_buf), stdin)) break;

        size_t len = strnlen(data_buf, sizeof(data_buf));
        if (data_buf[len - 1] == '\n') {  // 去掉换行
            data_buf[len - 1] = '\0';
            len--;
        }

        if (strcmp(data_buf, "exit") == 0) {
            // 发送 EXIT 控制消息
            memset(ctrl, 0, sizeof(ctrl));
            strncpy(ctrl, CTRL_MSG_EXIT, sizeof(ctrl) - 1);
            send_fixed(sock_fd, ctrl, sizeof(ctrl));
            printf("[CLIENT] Sent EXIT, quitting.\n");
            break;
        }

        // 1. 发送 PUSH 控制消息
        memset(ctrl, 0, sizeof(ctrl));
        strncpy(ctrl, CTRL_MSG_PUSH, sizeof(ctrl) - 1);
        if (!send_fixed(sock_fd, ctrl, sizeof(ctrl))) break;

        // 2. 紧跟发送数据长度 + 数据内容
        uint32_t data_len = htonl((uint32_t)len);
        if (!send_fixed(sock_fd, &data_len, sizeof(data_len))) break;
        if (!send_fixed(sock_fd, data_buf, len)) break;
        seq++;

        // 3. 等待服务端 PULL 响应
        memset(ctrl, 0, sizeof(ctrl));
        if (!recv_fixed(sock_fd, ctrl, sizeof(ctrl))) break;
        if (strncmp(ctrl, CTRL_MSG_PULL, sizeof(ctrl)) == 0) {
            printf("[CLIENT] <- PULL received from server for seq %u\n", seq);
        } else {
            printf("[CLIENT] <- Unknown response: \"%.*s\"\n", (int)sizeof(ctrl), ctrl);
        }
    }

    close(sock_fd);
    return 0;
}

五、执行方式


0、可选:修改server.c中的主机地址和端口,编译;
1、在服务端linux执行SERVER程序
2、在客户端linux执行CLIENT:   
./client 服务端ip 监听端口
./client 172.17.*.* 12345

3、在client端向sever端发送字符串或数字

4、sever端收到数据

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐