3、TCP服务端实现:创建服务端socket、绑定地址、监听、接受连接、收发数据

好,咱们今天来聊聊TCP服务端的实现。这部分可以说是网络编程的基石,你想想看,几乎所有互联网服务——从Web服务器到聊天软件——底层都是这套流程。

我个人习惯把TCP服务端编程总结为五个步骤:创建socket、绑定地址、监听、接受连接、收发数据。这五个步骤环环相扣,少一步都不行。我在项目中遇到过不少新手,上来就写收发数据,结果socket都没创建好,折腾半天找不到问题。

3.1 创建服务端socket

第一步,用socket()函数创建一个套接字。服务端和客户端的创建方式其实一样,区别在于后续的操作。

#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket");
    exit(EXIT_FAILURE);
}

这里AF_INET表示IPv4协议族,SOCK_STREAM表示流式套接字——说白了就是TCP。第三个参数0表示让系统自动选择协议。

小提示:创建socket后,我建议立即设置SO_REUSEADDR选项。为什么?因为服务端崩溃重启时,端口可能还处于TIME_WAIT状态,不设置这个选项会绑定失败。我曾经因为这个坑,在线上环境折腾了半小时才反应过来。
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

3.2 绑定地址

创建完socket,接下来要把它和一个地址绑定起来。地址包含IP和端口,告诉操作系统:这个socket监听哪个IP的哪个端口。

struct sockaddr_in server_addr;
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(8888);               // 端口8888

if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    perror("bind");
    close(sockfd);
    exit(EXIT_FAILURE);
}

嗯,这里要注意几个细节:

  • INADDR_ANY表示监听本机所有IP地址。如果你只想监听某个特定IP,可以换成具体的地址。
  • htonl()htons()是字节序转换函数。网络字节序是大端,而x86机器是小端,不转换会出问题。
  • 端口号建议用1024以上的,避免权限问题。1024以下需要root权限。
注意:bind失败最常见的原因是端口被占用。可以用netstat -tlnp | grep 8888检查一下。我曾经在调试时忘了关上一个服务端进程,结果bind一直返回-1,查了半天才发现。

3.3 监听

绑定成功后,调用listen()让socket进入监听状态。这个函数告诉内核:准备好接受连接请求了。

if (listen(sockfd, 10) == -1) {
    perror("listen");
    close(sockfd);
    exit(EXIT_FAILURE);
}

第二个参数是backlog,表示连接请求队列的最大长度。什么意思呢?当多个客户端同时连接时,来不及处理的请求会先排队。backlog就是这个队列的大小。

backlog值 说明
5 小型服务,并发量低
10 一般服务,够用
128 高并发场景

我个人习惯设成10或20。设太大其实没意义,因为真正的并发处理能力取决于后续的accept和业务逻辑。

3.4 接受连接

监听之后,用accept()接受客户端的连接。这个函数会阻塞,直到有客户端连上来。

struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_fd == -1) {
    perror("accept");
    close(sockfd);
    exit(EXIT_FAILURE);
}

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

accept返回一个新的socket描述符client_fd,后续的收发数据都用这个新socket。原来的sockfd继续监听,等待下一个连接。

核心理解:一个服务端socket可以接受多个客户端连接。每个连接对应一个独立的client_fd。这就是并发的基础。

3.5 收发数据

连接建立后,用read()write()收发数据。这两个函数和文件操作一样,因为Linux下一切皆文件。

char buffer[1024];
int n;

// 接收数据
n = read(client_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
    buffer[n] = '\0';
    printf("收到: %s\n", buffer);
} else if (n == 0) {
    printf("客户端关闭连接\n");
} else {
    perror("read");
}

// 发送数据
const char *response = "Hello, client!";
write(client_fd, response, strlen(response));

这里有几个要点:

  • read()返回0表示对方关闭了连接,这时候要记得close(client_fd)。
  • 返回-1表示出错,需要处理错误。
  • buffer要留一个字节给'\0',方便打印字符串。
经验之谈:实际项目中,我不会直接用read/write,而是用send()recv()。它们多了flags参数,可以控制更多行为。比如MSG_WAITALL标志可以保证读取指定长度的数据。

3.6 完整代码示例

把上面这些串起来,就是一个完整的TCP服务端:

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

int main() {
    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置端口复用
    int opt = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 2. 绑定地址
    struct sockaddr_in server_addr;
    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(8888);

    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 3. 监听
    if (listen(sockfd, 10) == -1) {
        perror("listen");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("服务端启动,监听端口8888...\n");

    // 4. 接受连接
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);
    int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd == -1) {
        perror("accept");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("客户端连接: %s:%d\n",
           inet_ntoa(client_addr.sin_addr),
           ntohs(client_addr.sin_port));

    // 5. 收发数据
    char buffer[1024];
    int n = read(client_fd, buffer, sizeof(buffer) - 1);
    if (n > 0) {
        buffer[n] = '\0';
        printf("收到: %s\n", buffer);
        write(client_fd, "已收到你的消息", strlen("已收到你的消息"));
    }

    close(client_fd);
    close(sockfd);
    return 0;
}

3.7 核心流程总结

为了让你更直观地理解整个流程,我画了一张图:

TCP服务端核心流程 1. socket() 创建套接字 2. bind() 绑定IP和端口 3. listen() 进入监听状态 4. accept() 接受客户端连接 5. read() / write() 收发数据 关键说明 • socket() 创建监听套接字 • accept() 返回新套接字 • 原套接字继续监听 • 新套接字用于通信 • 记得关闭套接字 💡 这五个步骤是TCP服务端的标准模板,几乎所有服务端程序都遵循这个流程

这张图把整个流程串起来了。你注意看,socket()创建的是监听套接字,accept()返回的是通信套接字。这两个角色不同,千万别搞混。

避坑指南:我曾经在写一个聊天服务器时,误把监听套接字直接用来收发数据,结果客户端发来的数据全丢了。排查了半天才发现,原来accept返回的client_fd才是真正用来通信的。记住:监听套接字只负责接客,不负责聊天。

好了,TCP服务端的五个步骤就讲完了。代码虽然简单,但每个细节都值得推敲。你写代码时,建议一步步来,每完成一步就打印个日志,这样出了问题也好定位。

最后提醒:实际项目中,服务端通常不会只处理一个连接。后面我们会讲多线程、epoll等高并发方案。但今天这五个步骤是基础中的基础,一定要吃透。

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