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端收到数据