12、TCP回射客户端:实现对应的TCP回射客户端,连接服务器并发送数据。

上一章我们把回射服务器搭好了,像个老实的接线员——你喊什么,他重复什么。但光有接线员不行啊,总得有人打电话过去吧?这一章,我们就来写那个打电话的人:TCP回射客户端

说白了,客户端的工作就三件事:连上去、发数据、收回来。听起来简单,但实际写起来,坑也不少。我当年第一次写客户端,忘了处理服务器断连的情况,结果程序卡在那等回复,等了一宿……嗯,从那以后我再也不敢小看客户端的异常处理了。

客户端核心流程

先看一张流程图,把整个调用链理清楚。你写代码的时候,脑子里得有这张图。

TCP回射客户端核心流程 socket() 创建套接字 connect() 连接服务器 send() / write() 发送数据 recv() / read() 接收回射数据 close() 关闭连接 ① 创建 ② 握手 ③ 发送 ④ 接收 ⑤ 关闭

你看,客户端比服务器少了一个 bind()listen()。为什么?因为客户端不需要固定端口,操作系统会自动分配一个临时端口给你。我个人习惯在 connect() 之前不显式调用 bind(),让内核去选端口,省心。

代码实现

直接上代码。这个例子我写得很克制,没有加一堆花哨的功能,就聚焦在「连接-发送-接收」这个闭环上。

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

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char send_buf[BUFFER_SIZE];
    char recv_buf[BUFFER_SIZE];
    int ret;

    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    // 3. 连接服务器
    ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (ret < 0) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("已连接到服务器 %s:%d\n", SERVER_IP, SERVER_PORT);

    // 4. 循环发送并接收回射数据
    while (1) {
        printf("请输入要发送的消息 (输入 'quit' 退出): ");
        fgets(send_buf, BUFFER_SIZE, stdin);
        // 去掉换行符
        send_buf[strcspn(send_buf, "\n")] = '\0';

        if (strcmp(send_buf, "quit") == 0) {
            break;
        }

        // 发送数据
        ret = send(sockfd, send_buf, strlen(send_buf), 0);
        if (ret <= 0) {
            perror("send");
            break;
        }
        printf("发送了 %d 字节\n", ret);

        // 接收回射数据
        memset(recv_buf, 0, BUFFER_SIZE);
        ret = recv(sockfd, recv_buf, BUFFER_SIZE - 1, 0);
        if (ret <= 0) {
            printf("服务器断开连接或接收出错\n");
            break;
        }
        recv_buf[ret] = '\0';
        printf("收到回射: %s (共 %d 字节)\n", recv_buf, ret);
    }

    // 5. 关闭套接字
    close(sockfd);
    printf("客户端已退出\n");
    return 0;
}

关键点解析

代码不长,但有几个地方值得拎出来说说。

1. connect() 的阻塞特性

connect() 默认是阻塞的。什么意思?就是它会一直等,直到三次握手完成或者超时失败。我在项目中遇到过一种情况:服务器没启动,客户端 connect() 卡了大概 75 秒才返回错误。那叫一个煎熬。后来我学会了用非阻塞模式加超时控制,但那是后话了。初学者先用默认阻塞模式,简单直接。

2. send() 和 recv() 的返回值

这两个函数的返回值一定要检查。我曾经犯过一个错:以为 send() 一定能把所有数据发出去。结果在高并发场景下,send() 只发了部分数据就返回了。你想想看,如果只发了半个消息,服务器那边收到的是什么?乱码!所以严谨的做法是循环发送,直到所有数据都发完。不过这个例子数据量小,一次发完的概率很高,我就没写循环,但你要有这个意识。

重要原则: send() 返回的是实际发送的字节数,不一定等于你传入的长度。recv() 返回 0 表示对方关闭了连接,返回 -1 表示出错。这两个返回值,每次都要检查。

3. 处理服务器断连

当服务器突然挂掉,或者网络断开,recv() 会返回 0。这时候客户端不能傻等,必须退出循环并关闭套接字。我见过一些新手写的客户端,服务器都关了,它还在那循环 recv(),结果程序就卡死了。嗯,这里要注意:收到 0 就是对方说拜拜了,你也该走了

运行测试

先启动上一章的服务器,再启动这个客户端。你会看到这样的交互:

已连接到服务器 127.0.0.1:8888
请输入要发送的消息 (输入 'quit' 退出): hello
发送了 5 字节
收到回射: hello (共 5 字节)
请输入要发送的消息 (输入 'quit' 退出): world
发送了 5 字节
收到回射: world (共 5 字节)
请输入要发送的消息 (输入 'quit' 退出): quit
客户端已退出

完美!你发什么,服务器就回什么。这就是回射模型最朴素的样子。

常见问题与避坑

问题 原因 解决办法
connect() 返回 Connection refused 服务器没启动,或者端口不对 先确认服务器在运行,检查端口号是否一致
send() 返回 -1 连接已经断开,或者信号中断 检查 errno,如果是 EPIPE 说明对端已关闭
recv() 一直阻塞 服务器没发数据,或者网络延迟 检查服务器逻辑,或者设置超时
收到的数据不完整 TCP 是流协议,没有消息边界 需要自己设计应用层协议,比如加长度头

一个小技巧: 调试网络程序时,可以用 stracetcpdump 抓包看看。我经常用 strace -e trace=network ./client 来观察系统调用的情况,一目了然。

总结

这一章我们写了一个最基础的 TCP 回射客户端。别看它简单,它包含了网络编程的四个核心步骤:创建套接字、连接服务器、收发数据、关闭连接。后面的所有客户端程序,都是在这个骨架上加东西。

我个人建议你把这段代码多跑几遍,故意制造一些异常场景——比如先启动客户端再启动服务器,或者中途把服务器 kill 掉。只有亲手踩过坑,才能真正理解这些错误处理代码的价值。

好了,客户端和服务器的基本架子都搭好了。下一章我们会在回射模型上加点料,让它更实用一些。


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