12、TCP回射客户端:实现对应的TCP回射客户端,连接服务器并发送数据。
上一章我们把回射服务器搭好了,像个老实的接线员——你喊什么,他重复什么。但光有接线员不行啊,总得有人打电话过去吧?这一章,我们就来写那个打电话的人:TCP回射客户端。
说白了,客户端的工作就三件事:连上去、发数据、收回来。听起来简单,但实际写起来,坑也不少。我当年第一次写客户端,忘了处理服务器断连的情况,结果程序卡在那等回复,等了一宿……嗯,从那以后我再也不敢小看客户端的异常处理了。
客户端核心流程
先看一张流程图,把整个调用链理清楚。你写代码的时候,脑子里得有这张图。
你看,客户端比服务器少了一个 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 是流协议,没有消息边界 | 需要自己设计应用层协议,比如加长度头 |
一个小技巧: 调试网络程序时,可以用 strace 或 tcpdump 抓包看看。我经常用 strace -e trace=network ./client 来观察系统调用的情况,一目了然。
总结
这一章我们写了一个最基础的 TCP 回射客户端。别看它简单,它包含了网络编程的四个核心步骤:创建套接字、连接服务器、收发数据、关闭连接。后面的所有客户端程序,都是在这个骨架上加东西。
我个人建议你把这段代码多跑几遍,故意制造一些异常场景——比如先启动客户端再启动服务器,或者中途把服务器 kill 掉。只有亲手踩过坑,才能真正理解这些错误处理代码的价值。
好了,客户端和服务器的基本架子都搭好了。下一章我们会在回射模型上加点料,让它更实用一些。
公众号:蓝海资料掘金营,微信deep3321