凌峰创科服务平台

Linux socket服务器端如何高效处理多连接?

目录

  1. 核心概念: 了解 TCP 服务器通信的基本流程。
  2. 完整代码: 一个最简单的单线程回显服务器。
  3. 代码分步详解: 逐行解释代码的每一部分。
  4. 编译与运行: 如何编译和测试这个服务器。
  5. 进阶与改进: 如何让服务器更健壮、更高效。
    • 处理客户端断开连接
    • 使用 select 实现简单的 I/O 多路复用
    • 使用多线程处理多个客户端

核心概念

一个 TCP 服务器的工作流程可以概括为以下几个步骤,就像一个电话总机:

Linux socket服务器端如何高效处理多连接?-图1
(图片来源网络,侵删)
  1. 创建套接字: 就像总机安装了一部电话机,使用 socket() 函数创建一个通信端点。
  2. 绑定地址和端口: 就像给总机分配了一个电话号码和地址,使用 bind() 函数将套接字与一个特定的 IP 地址和端口号关联起来,这样客户端才能找到它。
  3. 监听连接: 就像总机开始等待来电,使用 listen() 函数让套接字进入被动监听状态,准备接收客户端的连接请求。
  4. 接受连接: 就像总机接线员接起电话,使用 accept() 函数从等待队列中取出一个客户端连接请求,并创建一个新的套接字与这个客户端进行一对一通信,原来的监听套接字继续等待其他客户端的连接。
  5. 收发数据: 就像接线员和通话方对话,使用 read() (或 recv()) 和 write() (或 send()) 函数通过新创建的套接字与客户端交换数据。
  6. 关闭连接: 就像挂断电话,当通信结束时,使用 close() 函数关闭套接字,释放资源。

完整代码 (单线程回显服务器)

这是一个最基础的服务器,它只能同时处理一个客户端,当它与一个客户端通信时,其他客户端必须等待。

// server.c
#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 8080
#define BUFFER_SIZE 1024
int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字文件描述符
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 设置套接字选项,允许地址重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    // 2. 绑定地址和端口
    address.sin_family = AF_INET; // IPv4
    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);
    }
    // 3. 开始监听连接
    if (listen(server_fd, 3) < 0) { // 3 是最大等待连接数
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 4. 接受新的连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    // 打印连接的客户端信息
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &address.sin_addr, client_ip, INET_ADDRSTRLEN);
    printf("Client connected: IP = %s, Port = %d\n", client_ip, ntohs(address.sin_port));
    // 5. 循环收发数据 (回显服务器)
    int valread;
    while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
        printf("Received from client: %s", buffer);
        send(new_socket, buffer, valread, 0); // 将收到的数据回写给客户端
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    if (valread == 0) {
        printf("Client disconnected.\n");
    } else if (valread < 0) {
        perror("read error");
    }
    // 6. 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

代码分步详解

包含的头文件

#include <sys/socket.h>  // socket(), bind(), listen(), accept(), connect()
#include <netinet/in.h>  // struct sockaddr_in, IPPROTO_TCP, htons(), ntohl()
#include <arpa/inet.h>   // inet_addr(), inet_ntop()
#include <unistd.h>      // read(), write(), close()
#include <stdio.h>       // perror(), printf()
#include <stdlib.h>      // exit()
#include <string.h>      // memset()

创建套接字 (socket)

if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
    // ... 错误处理
}
  • AF_INET: 指定使用 IPv4 地址族。
  • SOCK_STREAM: 指定使用面向连接的、可靠的 TCP 协议。
  • 0: 通常设为 0,表示让系统自动选择与 AF_INETSOCK_STREAM 对应的协议,也就是 TCP。
  • server_fd: 是一个文件描述符,后续所有操作都通过它来进行。

绑定地址和端口 (bind)

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) {
    // ... 错误处理
}
  • struct sockaddr_in: 是 struct sockaddr 的 IPv4 版本,包含了 IP 地址和端口号。
  • sin_family: 地址族,设为 AF_INET
  • sin_addr.s_addr: IP 地址。INADDR_ANY 是一个特殊的宏,表示服务器将监听所有网络接口(如 0.0.1168.x.x)上的连接请求。
  • sin_port: 端口号。重要:计算机内存中存储多字节数据的顺序(字节序)可能和网络传输的标准顺序(网络字节序,大端序)不同。htons() (host to network short) 函数将主机字节序的端口号转换为网络字节序。

监听连接 (listen)

if (listen(server_fd, 3) < 0) {
    // ... 错误处理
}
  • server_fd: 要监听的套接字。
  • 3: backlog 参数,表示内核中等待队列的最大长度,即,当服务器繁忙时,最多可以有多少个客户端连接请求在队列中等待。3 是一个常用的值。

接受连接 (accept)

if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
    // ... 错误处理
}
  • server_fd: 监听套接字。
  • new_socket: accept() 会返回一个新的套接字文件描述符,服务器将通过这个 new_socket 与客户端进行后续的数据收发,而 server_fd 继续负责监听新的连接。
  • address: 一个输出参数,用于存放连接客户端的地址信息(IP 和端口)。
  • addrlen: 输入输出参数,传入 sizeof(address),返回实际地址的长度。

收发数据 (read / writerecv / send)

int valread;
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
    send(new_socket, buffer, valread, 0);
    memset(buffer, 0, BUFFER_SIZE);
}
Linux socket服务器端如何高效处理多连接?-图2
(图片来源网络,侵删)
分享:
扫描分享到社交APP
上一篇
下一篇