TCP编程基础(下):accept()、connect()、send()、recv()函数详解与实战

好,咱们接着聊。上一章我们把 socket() 和 bind() 讲透了,还搭了个 listen() 的架子。今天这章,才是真正让客户端和服务端「握手」的关键环节。

说实话,我当年刚学 TCP 编程时,最头疼的就是这四个函数。明明看着名字都认识,但一写代码就各种报错。尤其是 accept() 和 connect() 的阻塞行为,坑了我好几个通宵。今天咱们就把它们彻底掰开揉碎。

TCP 编程核心函数调用流程 服务端流程 socket() → bind() → listen() accept() — 阻塞等待连接 recv() / send() 数据收发 close() 关闭连接 客户端流程 socket() 创建套接字 connect() — 发起连接 send() / recv() 数据收发 close() 关闭连接 三次握手 核心:accept() 被动等待,connect() 主动发起,send/recv 全双工通信

1. accept() — 服务端的「迎宾员」

accept() 这个函数,说白了就是服务端用来「接客」的。它从 listen() 维护的已完成连接队列里,取出一个已经完成三次握手的连接。

函数原型:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

返回值:成功返回一个新的套接字描述符,失败返回 -1。

这里有个关键点,我当年踩过坑——accept() 返回的是一个新套接字,不是原来那个监听套接字。原来的 sockfd 继续监听,新套接字才用来和客户端通信。

我的经验:每次 accept() 成功后,记得把新套接字存起来。我曾经犯过傻,直接用监听套接字去 recv(),结果一直收不到数据,排查了半天才发现是套接字用错了。

accept() 默认是阻塞的。如果没有客户端连接,它会一直等在那里。你可以把它想象成前台服务员,没客人时就站着等,来了客人就领进去。

2. connect() — 客户端的「敲门砖」

connect() 是客户端主动发起连接用的。它会触发 TCP 三次握手,握手完成后返回。

函数原型:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

返回值:成功返回 0,失败返回 -1。

connect() 也是阻塞的。它会一直等到三次握手完成,或者超时失败。默认超时时间一般是 75 秒,嗯,这个时间有点长,后面讲非阻塞时会教你怎么改。

注意:connect() 失败后,这个套接字就不能再用了。必须 close() 掉,重新 socket() 再 connect()。我见过有人 connect 失败后直接重试,结果一直报错,就是因为没重建套接字。

3. send() 和 recv() — 数据的「搬运工」

连接建立好了,接下来就是收发数据。send() 和 recv() 是 TCP 编程中最常用的读写函数。

函数原型:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

返回值:成功返回实际发送/接收的字节数,失败返回 -1。

这里有个很多人容易忽略的点:send() 不一定能一次发完所有数据。它返回的是实际写入内核缓冲区的字节数。如果缓冲区满了,send() 可能只发了一部分。

我的习惯:写一个 while 循环来保证数据全部发送。就像这样:

int send_all(int fd, char *data, int len) {
    int sent = 0;
    while (sent < len) {
        int n = send(fd, data + sent, len - sent, 0);
        if (n <= 0) return -1;
        sent += n;
    }
    return sent;
}

recv() 也一样。它返回 0 表示对方关闭了连接,返回 -1 表示出错。你想想看,如果 recv() 返回 0 你还继续读,那就出大问题了。

4. 实战:完整的回声服务器

光说不练假把式。咱们写一个完整的回声服务器——客户端发什么,服务端就原样返回什么。

服务端代码:

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

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 1. 创建套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_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 = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        exit(1);
    }

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

    printf("服务器已启动,等待连接...\n");

    // 4. 接受连接
    client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
    if (client_fd < 0) {
        perror("accept");
        exit(1);
    }

    printf("客户端已连接\n");

    // 5. 收发数据
    int n;
    while ((n = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) {
        send(client_fd, buffer, n, 0);
        memset(buffer, 0, BUFFER_SIZE);
    }

    if (n == 0) {
        printf("客户端断开连接\n");
    } else {
        perror("recv");
    }

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

客户端代码:

#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 sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

    // 1. 创建套接字
    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, "127.0.0.1", &server_addr.sin_addr);

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

    printf("已连接到服务器\n");

    // 3. 发送并接收回声
    while (1) {
        printf("请输入消息: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = 0;

        if (strcmp(buffer, "quit") == 0) break;

        send(sock_fd, buffer, strlen(buffer), 0);
        
        memset(buffer, 0, BUFFER_SIZE);
        recv(sock_fd, buffer, BUFFER_SIZE, 0);
        printf("服务器回声: %s\n", buffer);
    }

    close(sock_fd);
    return 0;
}

5. 常见问题与避坑指南

问题 原因 解决方案
accept() 返回 -1 监听套接字被信号中断 检查 errno == EINTR,重新调用 accept()
connect() 返回 -1 目标端口未监听或网络不通 检查服务端是否启动,防火墙是否放行
send() 返回小于 len 内核缓冲区满 循环发送直到全部发完
recv() 返回 0 对方关闭连接 关闭本端套接字,退出循环

我曾经踩过的坑:有一次线上服务突然不响应了,排查了半天,发现是 accept() 返回了 -1,但代码里没处理 EINTR 信号。结果 accept() 被 SIGCHLD 信号中断后直接退出了。从那以后,我所有 accept() 调用都会加一个 while 循环处理 EINTR。

6. 阻塞与非阻塞的思考

默认情况下,这四个函数都是阻塞的。阻塞的好处是代码简单,坏处是效率低——一个线程只能处理一个连接。

你想想看,如果服务端 accept() 之后,recv() 一直等数据,这时候第二个客户端来连接,就没人理了。这就是为什么实际项目中要用多线程或者 IO 多路复用。

不过别急,这些高级话题后面会专门讲。今天先把阻塞版本吃透,这是基础中的基础。

我的建议:初学者先别碰非阻塞。先把阻塞版本的 accept/connect/send/recv 玩熟,理解它们的阻塞行为。等你能闭着眼写出回声服务器了,再考虑非阻塞和 epoll。

好了,今天的内容就到这。这四个函数是 TCP 编程的基石,一定要亲手敲代码跑一遍。回声服务器的代码我建议你手打一遍,别复制粘贴。打一遍代码,比你读十遍理论都管用。


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