当前位置: 首页 > news >正文

c/c++实现 TCP Socket网络通信

一、基本框架

客户端                   服务器
| 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: TCPif (listen_fd < 0) {perror("socket");return 1;}// --------------------绑定 IP 与端口 --------------------struct sockaddr_in addr;addr.sin_family = AF_INET;           // IPv4addr.sin_port = htons(12345);        // 服务端端口号(可以自定义)addr.sin_addr.s_addr = INADDR_ANY;   // 监听本机所有网卡,也可以指定具体 IPif (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端收到数据

http://www.dtcms.com/a/332092.html

相关文章:

  • Docker存储卷备份策略于VPS服务器环境的实施标准与恢复测试
  • Linux 进程与内存布局详解
  • RecyclerView 拖拽与滑动操作
  • HQA-Attack: Toward High Quality Black-Box Hard-Label Adversarial Attack on Text
  • 多列集合---Map
  • 【无标题】设计文档
  • Cache的基本原理和缓存一致性
  • 基于大语言模型的爬虫数据清洗与结构化
  • 可信搜索中的多重签名
  • 系统日常巡检脚本
  • 将mysql数据库表结构导出成DBML格式
  • Qt---Qt函数库
  • ActionChains 鼠标操作笔记
  • # Vue 列表渲染详解
  • AI智能体|扣子(Coze)搭建【批量识别发票并录入飞书】Agent
  • FTP 服务详解:原理、配置与实践
  • 8月14日星期四今日早报简报微语报早读
  • [激光原理与应用-273]:理论 - 波动光学 - 光是电磁波,本身并没有颜色,可见光的颜色不过是人的主观感受
  • 时钟 中断 day54
  • close函数概念和使用案例
  • rustdesk 开源遥控软件
  • 云服务器运行持续强化学习COOM框架的问题
  • 低配硬件运行智谱GLM-4.5V视觉语言模型推理服务的方法
  • C#WPF实战出真汁01--项目介绍
  • linux设备驱动之USB驱动-USB主机、设备与Gadget驱动
  • 【Java|第十九篇】面向对象九——String类和枚举类
  • AI更换商品背景,智能融合,无痕修图
  • Java中加载语义模型
  • Windows bypassUAC 提权技法详解(一)
  • 洗浴中心泡池水过滤系统原理深度解析与工程实践