「Linux 网络编程」Socket 构建 TCP 本地通信

0 前言

在 Linux 网络编程中,TCP 本地通信是最基础的技术之一。本文将会详细介绍如何使用 Linux socket API 构建完整的 TCP 本地通信。


1 TCP 本地通信基础

1.1 TCP 协议的特点

  • 面向连接:通信前需要建立连接
  • 可靠传输:保证数据顺序和完整性
  • 流式传输:数据作为字节流传输
  • 全双工:双方可以同时发送和接收数据

TCP 收发数据是无消息边界的:TCP 把数据看作一连串无结构的字节流,发送方调用 send() 多次发送的数据,在接收方可能被合并成一次 recv() 接收,也可能一次发送的数据被拆分成多次接收。

「Linux 网络编程」计算机网络核心知识 6.3 TCP协议


1.2 本地回环地址

使用 本地回环地址127.0.0.1)进行本地通信:

  • 无需网络:不依赖物理网络设备
  • 高效安全:数据在系统内部传输,速度快且安全
  • 开发便利:轻松实现单机测试和开发

「Linux 网络编程」计算机网络核心知识 5.2.5 本地回环地址


1.3 什么是 Socket 套接字

Socket 套接字是网络通信的端点,是不同主机上的应用程序之间进行双向数据传输的通道接口。

可以把 Socket 套接字想象成一个虚拟的“电话插座”。可以用一个打电话的过程来类比网络通信:

  1. 电话系统 (网络):整个互联网或局域网就像是一个庞大的电话网络系统。
  2. IP 地址 (电话号码):每台联网的计算机都有一个唯一的IP地址,这就像家里的 电话号码。别人通过这个号码才能找到你。
  3. 端口号 (分机号):一台计算机上可能同时运行着多个网络程序(比如浏览器、微信、游戏)。端口号就像公司总机下的 分机号,例如:
    • 80 端口是给网页服务(HTTP)的
    • 21 端口是给文件传输(FTP)的
  4. 套接字 (完整的电话连接):一个套接字由 IP 地址 + 端口号 来唯一标识。就像是 “电话号码 + 分机号” 的组合。
    • 服务端:像一个呼叫中心,它先创建一个套接字(安装一个总机),绑定自己的IP和端口号(公布总机号码),然后等待来电。
    • 客户端:像一位客户,它创建一个套接字(拿起话筒),拨打服务器的IP和端口号(拨打总机号码+分机号)。
  5. 连接 (通话建立):当客户端和服务器端的套接字成功连接后,就像电话接通了。双方可以通过这个“电话线路”(套接字连接)进行双向的数据收发(对话)。

套接字在 TCP 通信中的工作步骤

  1. 服务器端监听

    • 服务器程序创建一个套接字。
    • 将套接字与自己的 IP地址 和某个 端口号 绑定。
    • 开始监听这个端口,等待客户端的连接请求。
  2. 客户端连接

    • 客户端程序创建一个套接字。
    • 它知道服务器的 IP地址端口号
    • 客户端向服务器发起连接请求。
  3. 建立连接

    • 服务器接受客户端的连接请求。建立一个稳定的、双向的通信通道。操作系统会为此连接 创建一个新的套接字 来专门处理这个客户端。
  4. 数据传输

    • 连接建立后,双方就可以通过各自的套接字使用 send()(发送)和 recv()(接收)等函数来读写数据。(就好像电话两头说的话)
  5. 关闭连接

    • 通信结束后,任何一方都可以关闭套接字,断开连接。(就像挂断电话一样)

套接字原理

在通信过程中,套接字一定是成对出现的。一个文件描述符指向一个套接字(套接字内部由内核借助两个缓冲区实现读写)


1.4 TCP 本地通信流程



2 网络字节序、IP 转换及 sockaddr 结构

2.1 网络字节序

2.1.1 什么是字节序

字节序(Byte Order)指的是多字节数据在内存中的存储顺序。在计算机系统中,主要有两种字节序:

1
2
3
4
5
6
7
8
9
// 以32位整数 0x12345678 为例:

// 大端字节序(Big-Endian)
内存地址: 低地址 ----> 高地址
存储内容: 0x12 | 0x34 | 0x56 | 0x78

// 小端字节序(Little-Endian)
内存地址: 低地址 ----> 高地址
存储内容: 0x78 | 0x56 | 0x34 | 0x12
  • 大端序:就像写数字一样,从左到右,高位在前(如:1234 读作"一千两百三十四")
  • 小端序:就像有些国家的日期写法,日/月/年,低位在前

2.1.2 为什么需要网络字节序

  • 不同架构的 CPU 使用不同的字节序
  • Intel x86 系列使用小端字节序
  • 某些处理器(如 PowerPC)使用大端字节序
  • 网络设备可能使用不同的字节序

TCP/IP 协议栈规定使用 大端字节序 作为网络字节序,所有在网络中传输的多字节数据都必须使用网络字节序。

2.1.3 字节序转换函数

Linux 提供了一组完整的字节序转换函数:

1
2
3
4
5
6
7
8
9
#include <arpa/inet.h>

// 主机字节序 → 网络字节序
uint32_t htonl(uint32_t hostlong); // 32位长整型
uint16_t htons(uint16_t hostshort); // 16位短整型

// 网络字节序 → 主机字节序
uint32_t ntohl(uint32_t netlong); // 32位长整型
uint16_t ntohs(uint16_t netshort); // 16位短整型
  • h = host(主机)
  • n = network(网络)
  • l = long(32位)
  • s = short(16位)

2.1.4 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <arpa/inet.h>

int main() {
uint32_t host_long = 0x12345678;
uint16_t host_short = 0x1234;

printf("原始数据 - 长整型: 0x%08x, 短整型: 0x%04x\n",
host_long, host_short);

// 转换为网络字节序
uint32_t net_long = htonl(host_long);
uint16_t net_short = htons(host_short);

printf("网络字节序 - 长整型: 0x%08x, 短整型: 0x%04x\n",
net_long, net_short);

// 转换回主机字节序
uint32_t back_long = ntohl(net_long);
uint16_t back_short = ntohs(net_short);

printf("转换回来 - 长整型: 0x%08x, 短整型: 0x%04x\n",
back_long, back_short);

return 0;
}

输出结果

1
2
3
原始数据 - 长整型: 0x12345678, 短整型: 0x1234
网络字节序 - 长整型: 0x78563412, 短整型: 0x3412
转换回来 - 长整型: 0x12345678, 短整型: 0x1234

2.2 IP 地址转换

2.2.1 为什么需要 IP 地址转换

在网络编程中,IP 地址有三种常见表示形式:

  1. 字符串形式:人类可读的格式,如 “192.168.1.1”
  2. 32 位整数形式:计算机内部使用的二进制格式
  3. in_addr 结构体:socket API 使用的格式

2.2.2 转换函数:inet_pton 和 inet_ntop

1
2
3
4
5
6
7
#include <arpa/inet.h>

// 字符串IP → 二进制IP(Presentation to Numeric)
int inet_pton(int af, const char *src, void *dst);

// 二进制IP → 字符串IP(Numeric to Presentation)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数详解

参数 说明 常用值
af 地址族 AF_INET(IPv4), AF_INET6(IPv6)
src 源地址 字符串或二进制IP指针
dst 目标地址 二进制或字符串IP指针
size 缓冲区大小 INET_ADDRSTRLEN(IPv4), INET6_ADDRSTRLEN(IPv6)

返回值

  • inet_pton:成功返回 1,无效输入返回 0,错误返回 -1
  • inet_ntop:成功返回目标字符串指针,错误返回 NULL

2.2.3 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>

int main() {
// 示例1:IPv4转换
printf("=== IPv4 地址转换 ===\n");

const char *ipv4_str = "192.168.1.100";
struct in_addr ipv4_bin;
char ipv4_str_back[INET_ADDRSTRLEN];

// 字符串 → 二进制
if (inet_pton(AF_INET, ipv4_str, &ipv4_bin) == 1) {
printf("字符串 '%s' → 二进制: 0x%08x\n",
ipv4_str, ipv4_bin.s_addr);
} else {
perror("inet_pton failed");
}

// 二进制 → 字符串
if (inet_ntop(AF_INET, &ipv4_bin, ipv4_str_back, INET_ADDRSTRLEN)) {
printf("二进制 0x%08x → 字符串: '%s'\n",
ipv4_bin.s_addr, ipv4_str_back);
} else {
perror("inet_ntop failed");
}

// 示例2:IPv6转换
printf("\n=== IPv6 地址转换 ===\n");

const char *ipv6_str = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
struct in6_addr ipv6_bin;
char ipv6_str_back[INET6_ADDRSTRLEN];

if (inet_pton(AF_INET6, ipv6_str, &ipv6_bin) == 1) {
printf("IPv6 字符串转换成功\n");
}

if (inet_ntop(AF_INET6, &ipv6_bin, ipv6_str_back, INET6_ADDRSTRLEN)) {
printf("IPv6 二进制转字符串: %s\n", ipv6_str_back);
}

return 0;
}
1
2
3
4
5
6
7
=== IPv4 地址转换 ===
字符串 '192.168.1.100' → 二进制: 0x6401a8c0
二进制 0x6401a8c0 → 字符串: '192.168.1.100'

=== IPv6 地址转换 ===
IPv6 字符串转换成功
IPv6 二进制转字符串: 2001:db8:85a3::8a2e:370:7334

以 IPv4 地址转换 192.168.1.100 为例,首先将 4 个部分分别转换为 二进制,例如 192 -> 11000000168 -> 101010001 -> 00000001100 -> 01100100,组合起来得到网络字节序(大端序) 0xc0a80164。 在小端机器上,当我们用%X打印整个32位数时,字节顺序是反的,所以显示为 0x6401a8c0


2.3 sockaddr 结构

2.3.1 为什么需要 sockaddr 结构

  • 提供统一的地址结构接口
  • 支持多种协议族(IPv4、IPv6、Unix域套接字等)
  • 保持 API 的向后兼容性

2.3.2 通用地址结构:sockaddr

1
2
3
4
5
6
7
#include <sys/socket.h>

// 通用地址结构(用于函数参数类型统一)
struct sockaddr {
sa_family_t sa_family; // 地址族(2字节)
char sa_data[14]; // 地址数据(14字节)
};
  • 大小固定为 16 字节
  • 前 2 字节标识地址族类型
  • 后 14 字节存储具体的地址信息

2.3.3 IPv4 专用结构:sockaddr_in

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <netinet/in.h>

// IPv4地址结构(16字节,与sockaddr大小相同)
struct sockaddr_in {
sa_family_t sin_family; // 地址族:AF_INET(2字节)
in_port_t sin_port; // 端口号(2字节,网络字节序)
struct in_addr sin_addr; // IPv4地址(4字节)
unsigned char sin_zero[8]; // 填充字段(8字节)
};

// IPv4地址(32位整数)
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址(网络字节序)
};

2.3.4 IPv6专用结构:sockaddr_in6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <netinet/in.h>

// IPv6地址结构(28字节)
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族:AF_INET6
in_port_t sin6_port; // 端口号(网络字节序)
uint32_t sin6_flowinfo; // IPv6流信息
struct in6_addr sin6_addr; // IPv6地址
uint32_t sin6_scope_id; // 接口范围ID
};

// IPv6地址(128位)
struct in6_addr {
unsigned char s6_addr[16]; // 128位IPv6地址
};


3 TCP 服务端构建

3.1 服务端构建流程


3.2 服务端构建相关函数

3.2.1 socket(): 创建通信端点

1
2
3
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明

  • domain协议族AF_INET(IPv4) 或 AF_INET6(IPv6)
  • type:套接字类型,SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)
  • protocol:通常设为 0,由系统自动选择

代码示例

1
2
3
4
5
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}

3.2.2 bind(): 绑定地址和端口

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

地址结构体设置

1
2
3
4
5
6
7
8
9
struct sockaddr_in server_addr;

// 清空结构体
memset(&server_addr, 0, sizeof(server_addr));

// 设置地址族、端口和IP
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(8080); // 端口号,需要转换为网络字节序
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 表示监听所有网络接口

绑定示例

1
2
3
4
5
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

3.2.3 listen(): 设置监听上限

1
int listen(int sockfd, int backlog);

参数说明

  • backlog:连接队列的最大长度,表示可以排队等待接受的连接数。

示例

1
2
3
4
5
6
if (listen(server_fd, 5) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port 8080...\n");

3.2.4 accept(): 接受客户端连接

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

接受连接示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len); // 阻塞等待客户端的连接请求
// 收到连接请求后,会返回一个新的套接字 client_fd 专门用于处理客户端发送的数据

if (client_fd < 0) {
perror("accept failed");
continue; // 继续等待其他连接
}

// 获取客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Client connected from %s:%d\n",
client_ip, ntohs(client_addr.sin_port));

3.2.5 recv() 和 send(): 数据收发函数

1
2
3
4
5
// 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

// 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);


4 TCP 客户端构建

4.1 客户端构建流程


4.2 客户端构建相关函数

4.2.1 connect(): 连接服务器

默认情况下,客户端不需要指定自己的端口和IP,系统会为其分配一个可用的临时端口和合适的本地IP地址。

在客户端代码中,我们通常不调用 bind(),而是直接调用 connect()这时内核会为套接字自动分配一个本地IP和临时端口

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

设置服务器地址

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));

server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 服务器端口

// 使用本地回环地址
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
perror("invalid address");
close(sockfd);
exit(EXIT_FAILURE);
}

连接示例

1
2
3
4
5
6
7
8
9
// 客户端向127.0.0.1:8080发起连接请求
// 操作系统识别这是回环地址,协议栈查找本地是否有程序在8080端口监听
// 如果服务器正在0.0.0.0:8080监听(包含回环接口),连接建立,数据直接在内存中传递
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Connected to server successfully!\n");


5 完整 TCP 本地通信示例

5.1 TCP 服务端完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024
#define BACKLOG 5

void handle_client(int client_fd, struct sockaddr_in *client_addr) {
char buffer[BUFFER_SIZE];
char client_ip[INET_ADDRSTRLEN];

inet_ntop(AF_INET, &client_addr->sin_addr, client_ip, sizeof(client_ip));
printf("Handling client %s:%d\n", client_ip, ntohs(client_addr->sin_port));

while (1) {
// 接收数据
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);

if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("Client %s:%d disconnected\n",
client_ip, ntohs(client_addr->sin_port));
} else {
perror("recv failed");
}
break;
}

printf("Received from %s: %s", client_ip, buffer);

// 回显数据
if (send(client_fd, buffer, bytes_received, 0) < 0) {
perror("send failed");
break;
}
}

close(client_fd);
}

int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);

// 创建socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 设置套接字选项,避免地址占用错误
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);

// 绑定地址
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 设置监听上限
if (listen(server_fd, BACKLOG) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}

printf("TCP Echo Server started on port %d\n", PORT);
printf("Server is listening for connections...\n");

// 主循环:接受和处理客户端连接,同时获取到客户端地址信息
while (1) {
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept failed");
continue;
}

// 处理客户端连接(这里可以改为多进程或多线程处理)
handle_client(client_fd, &client_addr);
}

close(server_fd);
return 0;
}

5.2 TCP 客户端完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];

// 创建客户端 socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);

// 将字符串IP转换为二进制格式
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid address");
close(sockfd);
exit(EXIT_FAILURE);
}

// 连接服务器
printf("Connecting to server %s:%d...\n", SERVER_IP, PORT);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}

printf("Connected to server successfully!\n");
printf("Type 'quit' to exit, or enter messages to send to server.\n\n");

// 通信循环
while (1) {
printf("Enter message: ");

// 读取用户输入
if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {
break;
}

// 检查退出条件
if (strncmp(buffer, "quit", 4) == 0) {
printf("Closing connection...\n");
break;
}

// 发送数据到服务器
ssize_t bytes_sent = send(sockfd, buffer, strlen(buffer), 0);
if (bytes_sent < 0) {
perror("send failed");
break;
}

printf("Sent %zd bytes to server\n", bytes_sent);

// 接收服务器回显
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);

if (bytes_received < 0) {
perror("recv failed");
break;
} else if (bytes_received == 0) {
printf("Server closed the connection\n");
break;
}

printf("Server echo: %s", buffer);
}

close(sockfd);
printf("Connection closed. Goodbye!\n");
return 0;
}

5.3 程序测试

编译程序

1
2
3
4
5
# 编译服务器
gcc -o tcp_server tcp_server.c

# 编译客户端
gcc -o tcp_client tcp_client.c

运行程序

1
2
3
4
5
# 终端1:启动服务器
./server

# 终端2:启动客户端
./client

输出结果示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 客户端输出示例
Connecting to server 127.0.0.1:8080...
Connected to server successfully!
Type 'quit' to exit, or enter messages to send to server.

Enter message: This is Marisa.
Sent 16 bytes to server
Server echo: This is Marisa.
Enter message: Hello Ubuntu
Sent 13 bytes to server
Server echo: Hello Ubuntu
Enter message: quit
Closing connection...
Connection closed. Goodbye!


# 服务器输出示例
TCP Echo Server started on port 8080
Server is listening for connections...
Handling client 127.0.0.1:34344
Received from 127.0.0.1: This is Marisa.
Received from 127.0.0.1: Hello Ubuntu
Client 127.0.0.1:34344 disconnected


6 TCP 的数据无边界性

TCP 的 数据无边界性 意味着发送端多次发送的数据可能在接收端一次接收,或者一次发送的数据可能被分多次接收。


6.1 数据无边界性服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// server_boundless.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024
#define BACKLOG 5

void handle_client(int client_fd, struct sockaddr_in *client_addr) {
char buffer[BUFFER_SIZE];
char client_ip[INET_ADDRSTRLEN];
int total_received = 0;
int recv_count = 0;

inet_ntop(AF_INET, &client_addr->sin_addr, client_ip, sizeof(client_ip));
printf("Handling client %s:%d\n", client_ip, ntohs(client_addr->sin_port));

while (1) {
// 接收数据
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);

if (bytes_received <= 0) {
if (bytes_received == 0) {
printf("Client %s:%d disconnected\n",
client_ip, ntohs(client_addr->sin_port));
} else {
perror("recv failed");
}
break;
}

recv_count ++ ;
total_received += bytes_received;

printf("[Receive %d] Received %zd bytes, total: %d bytes\n",
recv_count, bytes_received, total_received);
printf("Data content: ");
for (int i = 0; i < bytes_received; i++) {
if (buffer[i] >= 32 && buffer[i] <= 126) { // 可打印字符
printf("%c", buffer[i]);
} else {
printf("[0x%02X]", (unsigned char)buffer[i]);
}
}
printf("\n");

// 回显数据
if (send(client_fd, buffer, bytes_received, 0) < 0) {
perror("send failed");
break;
}

printf("Echoed %zd bytes back to client\n\n", bytes_received);
}

printf("Summary: Received %d times, total %d bytes from client %s\n",
recv_count, total_received, client_ip);
close(client_fd);
}

int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);

// 创建socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 设置套接字选项,避免地址占用错误
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);

// 绑定地址
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 开始监听
if (listen(server_fd, BACKLOG) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}

printf("TCP Boundless Test Server started on port %d\n", PORT);
printf("Server is listening for connections...\n\n");

// 主循环:接受和处理客户端连接
while (1) {
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept failed");
continue;
}

// 处理客户端连接
handle_client(client_fd, &client_addr);
}

close(server_fd);

return 0;
}

6.2 数据无边界性客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// client_boundless.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024

void test_small_messages(int sockfd) {
printf("\n=== 测试1: 连续发送小消息 ===\n");
char *messages[] = {
"Message 1\n",
"Message 2\n",
"Message 3\n",
"Message 4\n",
"Message 5\n"
};
int num_messages = 5;

printf("连续发送 %d 条小消息...\n", num_messages);

for (int i = 0; i < num_messages; i++) {
send(sockfd, messages[i], strlen(messages[i]), 0);
printf("Sent: %s", messages[i]);
usleep(10000); // 微小延迟,增加粘包可能性
}

printf("等待服务器回显...\n");

// 接收所有回显
char buffer[BUFFER_SIZE];
int total_received = 0;
while (total_received < 50) { // 总共发送了约50字节
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received <= 0) break;

total_received += bytes_received;
printf("Received %zd bytes: %s", bytes_received, buffer);
}
}

void test_large_message(int sockfd) {
printf("\n=== 测试2: 发送大消息 ===\n");

// 创建一个大消息(大于缓冲区)
char large_message[BUFFER_SIZE * 3];
memset(large_message, 'A', sizeof(large_message));

// 添加一些标识符
strcpy(large_message, "START:");
strcpy(large_message + sizeof(large_message) - 10, ":END");
large_message[sizeof(large_message) - 1] = '\0';

printf("发送 %zu 字节的大消息...\n", sizeof(large_message));
ssize_t bytes_sent = send(sockfd, large_message, sizeof(large_message), 0);
printf("实际发送: %zd 字节\n", bytes_sent);

// 接收回显(可能分多次)
printf("接收回显(可能分多次):\n");
char buffer[BUFFER_SIZE];
int recv_count = 0;
int total_received = 0;

while (total_received < bytes_sent) {
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received <= 0) break;

recv_count ++ ;
total_received += bytes_received;
printf("[Receive %d] %zd bytes, total: %d\n",
recv_count, bytes_received, total_received);

// 显示部分内容
if (bytes_received > 20) {
printf(" Data: %.20s...\n", buffer);
} else {
printf(" Data: %s\n", buffer);
}
}

printf("总结: 分 %d 次接收,总共 %d 字节\n", recv_count, total_received);
}

int main() {
int sockfd;
struct sockaddr_in server_addr;

// 创建socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);

if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("invalid address");
close(sockfd);
exit(EXIT_FAILURE);
}

// 连接服务器
printf("Connecting to server %s:%d...\n", SERVER_IP, PORT);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}

printf("Connected to server successfully!\n");
printf("=== TCP 数据无边界性验证 ===\n\n");

// 运行测试
test_small_messages(sockfd);
test_large_message(sockfd);

printf("\n=== 测试完成 ===\n");
close(sockfd);

return 0;
}

6.3 数据无边界性测试

1
2
3
4
5
6
7
# 编译
gcc server_boundless.c -o server_boundless
gcc client_boundless.c -o client_boundless

# 启动程序
./server_boundless # 启动服务端
./client_boundless # 另一个终端启动客户端

TCP 无边界性可能出现的情况

  1. 粘包现象:多个小消息可能在一次接收中到达

    客户端发送 5 条独立消息,服务端可能 1-2 次就接收完

  2. 拆包现象:大消息可能被分割成多次接收

    客户端发送 3000 字节,服务端可能分 3-4 次接收

  3. 接收次数不确定性:相同的发送模式可能产生不同的接收模式


输出结果如下

  • 服务端输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
TCP Boundless Test Server started on port 8080
Server is listening for connections...

Handling client 127.0.0.1:58664
[Receive 1] Received 10 bytes, total: 10 bytes
Data content: Message 1[0x0A]
Echoed 10 bytes back to client

[Receive 2] Received 10 bytes, total: 20 bytes
Data content: Message 2[0x0A]
Echoed 10 bytes back to client

[Receive 3] Received 10 bytes, total: 30 bytes
Data content: Message 3[0x0A]
Echoed 10 bytes back to client

[Receive 4] Received 10 bytes, total: 40 bytes
Data content: Message 4[0x0A]
Echoed 10 bytes back to client

[Receive 5] Received 10 bytes, total: 50 bytes
Data content: Message 5[0x0A]
Echoed 10 bytes back to client

[Receive 6] Received 1023 bytes, total: 1073 bytes
Data content: START:[0x00]AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Echoed 1023 bytes back to client

[Receive 7] Received 1023 bytes, total: 2096 bytes
Data content: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Echoed 1023 bytes back to client

[Receive 8] Received 1023 bytes, total: 3119 bytes
Data content: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:END[0x00]AA
Echoed 1023 bytes back to client

[Receive 9] Received 3 bytes, total: 3122 bytes
Data content: AA[0x00]
Echoed 3 bytes back to client

Client 127.0.0.1:58664 disconnected
Summary: Received 9 times, total 3122 bytes from client 127.0.0.1

  • 客户端输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Connecting to server 127.0.0.1:8080...
Connected to server successfully!
=== TCP 数据无边界性验证 ===


=== 测试1: 连续发送小消息 ===
连续发送 5 条小消息...
Sent: Message 1
Sent: Message 2
Sent: Message 3
Sent: Message 4
Sent: Message 5
等待服务器回显...
Received 50 bytes: Message 1
Message 2
Message 3
Message 4
Message 5

=== 测试2: 发送大消息 ===
发送 3072 字节的大消息...
实际发送: 3072 字节
接收回显(可能分多次):
[Receive 1] 1023 bytes, total: 1023
Data: START:...
[Receive 2] 1023 bytes, total: 2046
Data: AAAAAAAAAAAAAAAAAAAA...
[Receive 3] 1023 bytes, total: 3069
Data: AAAAAAAAAAAAAAAAAAAA...
[Receive 4] 3 bytes, total: 3072
Data: AA
总结: 分 4 次接收,总共 3072 字节

=== 测试完成 ===

在本地回环(lo)接口上,由于网络延迟几乎为零,内核优化会倾向于立即发送数据。所以在上述实际运行结果中并没有遇到粘包的情况。


「Linux 网络编程」Socket 构建 TCP 本地通信
https://marisamagic.github.io/2025/09/30/20250930/
作者
MarisaMagic
发布于
2025年9月30日
许可协议