c/c++实现 TCP Socket网络通信
重要点固定长度控制消息 () 避免 TCP 粘包/拆包问题。约定 PUSH → 发送数据长度 → 数据内容 → PULL 响应 → 客户端处理。设计目的双方通信流程标准化,客户端/服务端可以正确解析数据。#define CTRL_MSG_MAX 16 // 固定长度控制消息#define CTRL_MSG_PUSH "PUSH" // 客户端发送数据请求#define CTRL_MSG_PULL
一、基本框架
客户端 服务器
| 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端收到数据
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)