基于 Socket 和多线程的简单 Echo 服务器实现

发布于:2025-09-04 ⋅ 阅读:(27) ⋅ 点赞:(0)

在学习网络编程的时候,很多人会遇到一个经典的例子——Echo 服务器。所谓 Echo,就是“你说什么,我就原封不动地回什么”。这篇文章我们将通过一个 C 语言 + Socket + 多线程 的小例子,来实现一个简单的 Echo 服务器,并逐步分析其中的原理。

1. 场景类比

想象一下你去了一家快递公司,那里有一个接待大厅(服务器)。

  • 大厅的门口有个接待员(accept),不断等待新的顾客(客户端)进入。

  • 每来一个顾客,就会安排一个客服人员(线程 pthread_create),专门负责和这个顾客对话。

  • 客服的工作很简单:顾客说一句话(recv),客服就原样重复一句(send)。

这就是 Echo 服务的核心逻辑

2. 代码功能概述

这份代码主要实现了以下功能:

  • 创建 TCP 服务器 Socket(绑定本地 2000 端口)。

  • 监听客户端连接请求

  • 每当有客户端连接,就创建一个线程专门负责和该客户端通信

  • 通信逻辑:客户端发来什么消息,服务器就原样返回什么

也就是说,这是一个最简单的 多线程 TCP Echo 服务器

3. 核心原理

在深入代码之前,我们先理解几个关键点:

(1)Socket

socket(AF_INET, SOCK_STREAM, 0)

  • AF_INET 表示 IPv4。

  • SOCK_STREAM 表示使用 TCP 协议。

  • 得到的 sockfd 就像一个 监听用的电话机

(2)bind 和 listen

  • bind():把这个“电话机”绑定到一个固定号码(IP:Port)。

  • listen():让电话机进入“监听模式”,等待别人拨号。

(3)accept

  • accept():接电话,一旦有客户端打进来,就生成一个新的“分机电话”clientfd,用来和这个客户端通信。

注意:服务器端有两个 socket,一个是 监听 socket(接电话用),一个是 通信 socket(聊天用)。

(4)recv 和 send

  • recv():接收客户端发来的消息。

  • send():把消息再发回去。

(5)多线程

  • pthread_create():每接入一个客户端,就新开一个线程,保证多个客户端互不干扰。

4. 代码分块解析

下面我们逐步拆解代码。

(1)创建 socket 并绑定端口

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(2000);

if(-1 == bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr))){
    printf("bind failed: %s\n",strerror(errno));
}

这段代码创建了一个 TCP socket,并绑定到 0.0.0.0:2000,也就是本机的 2000 端口。

(2)开始监听

listen(sockfd,10);
printf("listen finished\n");

listen() 表示可以同时挂起最多 10 个客户端的连接请求。

(3)等待客户端连接

while(1){
    printf("accept\n");
    int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
    printf("accept finished\n");

    pthread_t thid;
    pthread_create(&thid, NULL, client_thread, &clientfd);
}

每当有一个客户端连接,都会生成一个新的 clientfd,然后用 pthread_create 创建一个新线程处理该连接。

(4)线程处理函数

void *client_thread(void *arg){
    int clientfd = *(int *)arg;

    while(1){
        char buffer[1024] = {0};
        int count = recv(clientfd, buffer, 1024, 0);
        printf("RECV: %s\n", buffer);

        count = send(clientfd, buffer, count, 0);
        printf("SEND: %d\n", count);
    }
}

每个线程的逻辑非常简单:

  • 读取客户端消息(recv)。

  • 原样返回(send)。

这就是 Echo 的精髓。

5. 整体运行流程图

文字流程如下:

客户端A连接 ──┐
客户端B连接 ──┼──> accept() ---> 新线程A <-> 客户端A
客户端C连接 ──┘               新线程B <-> 客户端B
                                 新线程C <-> 客户端C

服务器就像一个前台,每接入一个客户,就派一个新客服(线程)来一对一服务。

6.完整代码

#include <errno.h>          // 提供错误码 errno
#include <stdio.h>          // 标准输入输出库
#include <sys/socket.h>     // socket 相关函数和数据结构
#include <netinet/in.h>     // sockaddr_in 结构体、htons/htonl 等
#include <string.h>         // 字符串处理
#include <pthread.h>        // POSIX 线程库

// ==============================
// 客户端线程函数
// ==============================
void *client_thread(void *arg){

    // 取出传入的参数(clientfd 是客户端连接的 socket 描述符)
    int clientfd = *(int *)arg;

    while(1){
        char buffer[1024] = {0};           // 缓冲区,用于接收数据

        // 接收客户端发送的数据
        int count = recv(clientfd, buffer, 1024, 0);
        printf("RECV: %s\n", buffer);

        // 将接收到的数据原样发送回客户端(Echo)
        count = send(clientfd, buffer, count, 0);
        printf("SEND: %d\n", count);
    }
}

// ==============================
// 主函数
// ==============================
int main(){
    // 1. 创建一个 TCP socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 配置服务器地址结构体
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;              // 使用 IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到本机所有网卡 IP
    servaddr.sin_port = htons(2000);            // 监听端口 2000

    // 3. 绑定 socket 和端口
    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){
        // 如果绑定失败,打印错误信息
        printf("bind failed: %s\n", strerror(errno));
    }

    // 4. 开始监听,最大等待队列长度为 10
    listen(sockfd, 10);
    printf("listen finished\n");

    // 用于存储客户端信息
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

#if 0   // 方式一:只接受一次客户端请求,然后收发一次数据后退出
    printf("accept\n");
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf("accept finished\n");

    char buffer[1024] = {0};
    int count = recv(clientfd, buffer, 1024, 0);
    printf("RECV:%s\n", buffer);

    count = send(clientfd, buffer, count, 0);
    printf("SEND:%d\n", count);

#elif 0 // 方式二:循环处理多个客户端,但串行执行(一个客户端处理完才处理下一个)
    while(1){
        printf("accept\n");
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        printf("accept finished\n");

        char buffer[1024] = {0};
        int count = recv(clientfd, buffer, 1024, 0);
        printf("RECV:%s\n", buffer);

        count = send(clientfd, buffer, count, 0);
        printf("SEND: %d\n", count);
    }

#else   // 方式三(最终版本):每个客户端使用一个线程来处理
    while(1){
        printf("accept\n");

        // 阻塞等待客户端连接
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        printf("accept finished\n");

        // 创建线程处理该客户端连接
        pthread_t thid;
        pthread_create(&thid, NULL, client_thread, &clientfd);
        // 注意:此处传入 &clientfd 存在风险,最好用 malloc 分配空间再传入
    }
#endif

    // 等待用户输入,防止程序提前退出
    getchar();

    printf("exit\n");
    return 0;
}

7. 总结与扩展

这段代码实现了一个最小可用的 多线程 TCP Echo 服务器,主要用来理解:

  • Socket 的创建与绑定。

  • accept 的作用(连接建立)。

  • recv/send 的通信逻辑。

  • pthread 的多线程处理。

0voice · GitHub


网站公告

今日签到

点亮在社区的每一天
去签到