5、TCP回射服务器:实现一个简单的echo服务器,客户端发送什么就返回什么。

各位同学,今天我们来聊一个网络编程里最经典的入门案例——TCP回射服务器。说白了,就是客户端发什么,服务器就原封不动地返回什么。听起来很简单对吧?但别小看它,这个模型是所有复杂网络服务的基础骨架。

我个人习惯把这种服务器叫做“照镜子服务器”。你对着它喊一声“hello”,它就回你一声“hello”。嗯,这里要注意,虽然功能简单,但背后涉及的socket创建、绑定、监听、接受连接、读写数据这些流程,一个都少不了。

5.1 核心流程:服务器到底在干什么?

我们先理清思路。一个TCP回射服务器的工作流程,其实就五步:

  1. 创建socket——拿一个通信的“门牌号”
  2. 绑定地址——把门牌号挂到具体的IP和端口上
  3. 监听——告诉系统,我准备好接客了
  4. 接受连接——有客户端敲门,就开门
  5. 读写数据——读客户端发来的数据,再原样写回去

我刚开始学的时候,总觉得第4步和第5步应该分开写,结果代码跑起来总是各种段错误。后来才明白,accept()返回的是一个新的socket描述符,专门用来和这个客户端通信。原来的监听socket还在继续等别人敲门呢。

下面这张图,把整个流程串起来了:

TCP回射服务器核心流程 socket() 创建套接字 bind() 绑定地址端口 listen() 开始监听 accept() 等待连接 socket() 创建套接字 connect() 连接服务器 send() 发送数据 recv() 接收回射数据 TCP连接建立 客户端发送数据 服务器回射数据

5.2 代码实现:手写一个echo服务器

好了,理论说完了,咱们直接上代码。我习惯把服务器代码分成几个清晰的模块,这样调试起来也方便。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.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];
    int n;

    // 1. 创建socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 绑定地址
    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);
    }

    // 3. 监听
    if (listen(server_fd, 5) < 0) {
        perror("listen监听失败");
        close(server_fd);
        exit(1);
    }

    printf("服务器已启动,监听端口 %d...\n", PORT);

    // 4. 接受连接并处理
    while (1) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd < 0) {
            perror("accept接受连接失败");
            continue;
        }

        printf("客户端已连接: %s:%d\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

        // 5. 回射数据
        while ((n = read(client_fd, buffer, BUFFER_SIZE - 1)) > 0) {
            buffer[n] = '\0';
            printf("收到: %s", buffer);
            write(client_fd, buffer, n);
        }

        printf("客户端断开连接\n");
        close(client_fd);
    }

    close(server_fd);
    return 0;
}

关键点解析:

  • INADDR_ANY 表示绑定到本机所有IP地址,这样不管是内网还是外网都能访问
  • htonl()htons() 是字节序转换函数,网络字节序是大端模式
  • read() 返回0表示对方关闭了连接,返回-1表示出错

5.3 客户端代码:怎么跟服务器玩?

有服务器就得有客户端。不然你拿什么测试?我自己写客户端的时候,喜欢加上命令行参数,这样测试起来更灵活。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {
    int sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    int n;

    if (argc != 2) {
        fprintf(stderr, "用法: %s <服务器IP>\n", argv[0]);
        exit(1);
    }

    // 1. 创建socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);

    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect连接失败");
        close(sock_fd);
        exit(1);
    }

    printf("已连接到服务器 %s:%d\n", argv[1], PORT);

    // 3. 发送数据并接收回射
    while (1) {
        printf("请输入消息: ");
        if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {
            break;
        }

        write(sock_fd, buffer, strlen(buffer));

        n = read(sock_fd, buffer, BUFFER_SIZE - 1);
        if (n <= 0) {
            printf("服务器断开连接\n");
            break;
        }

        buffer[n] = '\0';
        printf("服务器回射: %s", buffer);
    }

    close(sock_fd);
    return 0;
}

5.4 避坑指南:我曾经踩过的坑

写这个echo服务器的时候,我遇到过几个特别坑的问题,分享给大家:

坑1:端口被占用

我曾经在调试的时候,程序崩溃了没来得及释放端口。第二次启动就报“Address already in use”。解决办法是在bind之前设置socket选项:

int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

坑2:read() 不保证一次读完所有数据

TCP是流式协议,没有消息边界。你发送一个“hello”,对方可能分两次收到“he”和“llo”。所以实际项目中,需要自己设计应用层协议来界定消息边界。不过我们这个echo服务器比较简单,一次读写就够了。

小技巧: 测试的时候可以用 telnet 命令快速验证服务器是否正常工作:

telnet 127.0.0.1 8888

输入任意字符,看服务器是否原样返回。如果返回了,说明你的echo服务器跑起来了。

5.5 运行效果演示

假设服务器IP是192.168.1.100,运行效果如下:

终端 输出内容
服务器端 服务器已启动,监听端口 8888...
客户端已连接: 192.168.1.101:54321
收到: hello
收到: world
客户端断开连接
客户端 已连接到服务器 192.168.1.100:8888
请输入消息: hello
服务器回射: hello
请输入消息: world
服务器回射: world
请输入消息: ^C

你看,客户端发什么,服务器就回什么。这就是最朴素的echo模型。你想想看,很多高级的协议,比如HTTP、FTP,底层都是基于这种请求-响应模式的。只不过它们加上了更复杂的头部信息和状态管理。

我个人建议,初学者一定要亲手把这段代码敲一遍,然后编译运行。遇到问题不要怕,用 perror() 打印错误信息,用 gdb 调试,慢慢就熟悉了。

公众号:蓝海资料掘金营,微信deep3321