第11章 TCP回射服务器:你发什么,我回什么
说实话,回射服务器(Echo Server)是TCP网络编程里最经典的入门案例。它做的事情很简单:客户端发来什么数据,服务器原封不动地返回去。听起来像是个玩具?我在项目里还真用过这个模式——调试网络延迟、测试带宽、验证防火墙规则,回射服务器都是个趁手的工具。
这一章,我们就手写一个完整的TCP回射服务器。我会把代码拆开讲,每一行都说明白为什么这么写。嗯,咱们开始吧。
11.1 回射服务器的核心逻辑
回射服务器的流程,说白了就是三步:
- 接收数据:从已连接的客户端socket读取数据
- 原样发送:把读到的数据写回给客户端
- 循环处理:重复1和2,直到客户端关闭连接
你想想看,这个模型其实涵盖了TCP服务器最核心的读写操作。学会了它,后面做文件传输、聊天室、HTTP服务器,都是在这个基础上加逻辑而已。
核心要点:回射服务器是“短连接”还是“长连接”?答案是长连接。服务器会持续为同一个客户端服务,直到客户端主动断开。我在项目中遇到过有人把它做成短连接——每次收发完就close——结果客户端频繁重连,性能惨不忍睹。
11.2 代码实现:单进程版本
我们先写一个最简单的单进程版本。它一次只能服务一个客户端,但逻辑最清晰,适合理解原理。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
ssize_t n;
// 1. 创建socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
exit(1);
}
// 2. 设置地址复用(避坑!)
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(server_fd);
exit(1);
}
// 4. 监听
if (listen(server_fd, 5) < 0) {
perror("listen");
close(server_fd);
exit(1);
}
printf("Echo server listening on port %d...\n", PORT);
// 5. 接受连接
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
close(server_fd);
exit(1);
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 6. 回射循环
while ((n = read(client_fd, buffer, sizeof(buffer))) > 0) {
// 原样写回
write(client_fd, buffer, n);
}
// 7. 清理
close(client_fd);
close(server_fd);
printf("Server closed.\n");
return 0;
}
11.3 代码逐段解析
这段代码不长,但每个环节都有讲究。我按顺序讲几个关键点。
11.3.1 socket() 和 setsockopt()
socket(AF_INET, SOCK_STREAM, 0) 创建了一个TCP socket。AF_INET表示IPv4,SOCK_STREAM表示流式套接字(TCP)。第三个参数0表示使用默认协议。
注意后面的 setsockopt(..., SO_REUSEADDR, ...)。这个设置很重要。我曾经在调试时忘了加它,服务器崩溃重启后,bind总是报“Address already in use”。为什么?因为TCP的TIME_WAIT状态占着端口呢。加上SO_REUSEADDR,就能立即重用地址。
个人习惯:我写TCP服务器,第一件事就是加SO_REUSEADDR。不管是不是调试阶段,养成习惯总没错。
11.3.2 bind() 和 listen()
bind() 把socket和本地地址绑定。这里用 INADDR_ANY 表示监听所有网络接口。如果你只想监听某个特定IP,可以换成具体的地址。
listen() 的第二个参数是backlog,表示连接请求队列的最大长度。5是经典值,但生产环境建议设大一些,比如128或1024。
11.3.3 accept() 和回射循环
accept() 会阻塞,直到有客户端连接进来。它返回一个新的socket描述符 client_fd,后续的读写都用这个。
回射循环是核心:read() 读取客户端发来的数据,write() 原样写回。注意 read() 返回0表示客户端关闭连接,返回-1表示出错。循环条件 n > 0 正好处理这两种情况。
注意:这里的 read() 和 write() 是阻塞调用。如果客户端不发数据,服务器会一直卡在 read() 上。单进程版本只能服务一个客户端,就是这个原因。后面我们会用多线程或epoll来解决。
11.4 客户端代码(测试用)
为了测试服务器,我们写一个简单的客户端。它从标准输入读取一行,发送给服务器,然后打印服务器返回的内容。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sock;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
ssize_t n;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
exit(1);
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sock);
exit(1);
}
printf("Connected to server. Type your message:\n");
while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
write(sock, buffer, strlen(buffer));
n = read(sock, buffer, sizeof(buffer) - 1);
if (n <= 0) break;
buffer[n] = '\0';
printf("Echo: %s", buffer);
}
close(sock);
return 0;
}
11.5 运行测试
先在一个终端启动服务器:
$ gcc echo_server.c -o echo_server
$ ./echo_server
Echo server listening on port 8888...
再开另一个终端运行客户端:
$ gcc echo_client.c -o echo_client
$ ./echo_client
Connected to server. Type your message:
hello
Echo: hello
world
Echo: world
(按Ctrl+D结束输入)
看到没?客户端发什么,服务器就回什么。这就是回射服务器。
11.6 核心流程图
下面这张图展示了TCP回射服务器的完整工作流程。从socket创建到连接关闭,每一步都标出来了。
11.7 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| bind失败:Address already in use | 端口被上一个进程占用,处于TIME_WAIT状态 | 添加SO_REUSEADDR选项 |
| 客户端连接不上 | 服务器没启动,或者防火墙拦截 | 检查服务器进程,用telnet测试端口 |
| 回射数据不完整 | TCP是流式协议,一次read可能没读完 | 使用应用层协议头,或者循环读取直到收完 |
| 服务器只能服务一个客户端 | 单进程阻塞在read()上 | 使用多线程、多进程或I/O多路复用 |
我曾经踩过的坑:有一次线上服务器莫名其妙丢数据。查了半天,发现是read()返回的数据长度小于预期,而write()只写了实际读取的长度。客户端那边却以为收到了完整消息。从那以后,我养成了一个习惯——应用层协议一定要带长度字段,或者用固定分隔符。TCP是流,不是消息边界。
11.8 小结
回射服务器虽然简单,但它把TCP网络编程的骨架完整地展示了一遍:socket创建、bind、listen、accept、read、write、close。每一个环节都是后面复杂应用的基础。
我个人建议你亲手敲一遍代码,然后试着改一改——比如改成并发版本,或者加上日志输出。只有动手写过,才能真正理解这些API的行为。
嗯,这一章就到这里。代码在本地跑起来了吗?有问题随时翻回来看。
公众号:蓝海资料掘金营,微信deep3321