TCP编程基础(下):accept()、connect()、send()、recv()函数详解与实战
好,咱们接着聊。上一章我们把 socket() 和 bind() 讲透了,还搭了个 listen() 的架子。今天这章,才是真正让客户端和服务端「握手」的关键环节。
说实话,我当年刚学 TCP 编程时,最头疼的就是这四个函数。明明看着名字都认识,但一写代码就各种报错。尤其是 accept() 和 connect() 的阻塞行为,坑了我好几个通宵。今天咱们就把它们彻底掰开揉碎。
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