C语言通过TCP套接字发送结构体触发段错误问题排查
问题根因
这个段错误和结构体对齐、内存填充没有任何关系,核心问题是直接传输带指针的结构体时,既没有传输指针指向的实际内容,跨进程的指针值本身也是无效的。
- 你的
toto_t结构体里str是char*类型,本质是一个内存地址,不是字符串本身。服务端调用strdup("Helloworld")时,是在自身进程的独立虚拟地址空间里申请了一块内存存储字符串,把这块内存的地址存在了str指针成员里。 - 你调用
send(new_socket, &toto, 14, 0)时,发出去的内容只有两部分:4字节的int值a,加上8字节(64位系统下指针占8字节)的地址值,加起来仅12字节,你写的14字节发送长度甚至还越界读取了结构体后面的非法内存,从头到尾没有把"Helloworld"这个字符串的实际内容发送出去。 - 客户端接收数据后,
toto.a是直接存在结构体里的整数值,所以能正常读出10;但toto.str里存的是服务端进程的虚拟内存地址,这个地址在客户端进程的地址空间里要么没有做内存映射,要么存储的是完全无关的数据,printf尝试访问这个地址读取字符串,自然触发段错误。 - 你添加的
#pragma pack、__attribute__((packed))修饰只能消除结构体内的对齐填充,对指针指向的堆内存中存储的字符串内容没有任何作用,无法解决这个问题。
正确实现方案
网络传输结构体时,绝对不能直接传输持有指针(指向堆/栈其他内存位置)的结构体原始内存,需要做序列化:把结构体里的所有实际内容按约定的字节序、格式拼成连续的字节流发送,接收端收到字节流后再反序列化解析回本地结构体。
针对当前场景,最简单的传输逻辑是:
- 传输时先发送字符串长度(即原结构体的
a字段),再发送字符串的实际内容,不需要传输指针 - 多字节整数要转成网络字节序,避免不同设备大小端差异导致解析错误
- 收发逻辑要做循环处理,因为
send/read不保证一次调用就能把指定长度的数据全部发送/接收完成
修正后的服务端代码:
#include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> #define PORT 8080 // 循环发送指定长度数据的工具函数 ssize_t send_all(int sockfd, const void *buf, size_t len) { size_t total_sent = 0; const char *p = (const char *)buf; while (total_sent < len) { ssize_t sent = send(sockfd, p + total_sent, len - total_sent, 0); if (sent <= 0) return sent; total_sent += sent; } return total_sent; } int main(int argc, char const* argv[]) { int server_fd, new_socket; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); const char *str = "Helloworld"; int str_len = strlen(str); if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(server_fd, 3) < 0) { perror("listen"); exit(EXIT_FAILURE); } if ((new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) { perror("accept"); exit(EXIT_FAILURE); } // 先发送网络字节序的字符串长度 int net_len = htonl(str_len); send_all(new_socket, &net_len, sizeof(net_len)); // 再发送字符串实际内容 send_all(new_socket, str, str_len); close(new_socket); shutdown(server_fd, SHUT_RDWR); return 0; }
修正后的客户端代码:
#include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> #define PORT 8080 // 循环接收指定长度数据的工具函数 ssize_t recv_all(int sockfd, void *buf, size_t len) { size_t total_recv = 0; char *p = (char *)buf; while (total_recv < len) { ssize_t r = recv(sockfd, p + total_recv, len - total_recv, 0); if (r <= 0) return r; total_recv += r; } return total_recv; } int main(int argc, char const* argv[]) { int sock, client_fd; struct sockaddr_in serv_addr; int str_len, net_len; char *str; if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("\n Socket creation error \n"); return -1; } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) { printf("\nInvalid address/ Address not supported \n"); return -1; } if ((client_fd = connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) < 0) { printf("\nConnection Failed \n"); return -1; } // 先接收长度,转成本地字节序 recv_all(sock, &net_len, sizeof(net_len)); str_len = ntohl(net_len); // 申请内存存储字符串,多1字节放字符串结束符'\0' str = (char*)malloc(str_len + 1); // 接收字符串内容 recv_all(sock, str, str_len); str[str_len] = '\0'; printf("%d\n%s\n", str_len, str); free(str); close(client_fd); return 0; }
额外注意事项:
- 不同进程的虚拟地址空间是完全独立的,A进程里的地址值放到B进程里没有任何意义,不要尝试跨网络/跨进程传输指针。
- 如果结构体里有更复杂的嵌套指针、可变长度数组,都需要按实际内容逐字段序列化,不能直接拷贝结构体原始内存发送。
- 跨机器传输时除了字节序问题,还要注意两端基本类型的长度差异(比如long类型在32位和64位系统长度不同),简单场景可以自己按约定格式组包,复杂场景可以使用成熟的序列化方案。
内容的提问来源于stack exchange,提问作者GloutonBargeot
相关产品推荐
相关产品推荐

