理解 C 语言中的阻塞套接字和非阻塞套接字
Blocking Sockets
阻塞套接字(也称为同步套接字)遵循一个简单的范式:I/O 操作会暂停程序的执行,直到操作完成。当您从阻塞套接字读取数据或向其写入数据时,程序会暂停,直到有数据可供读取或写入操作完成。这种同步行为简化了程序的流程,使其对开发人员(尤其是网络编程新手)来说更加直观易懂
阻塞式套接字的主要特点包括:
- 阻塞行为:I/O 操作会阻塞程序的执行,直到操作完成。
- 同步操作:操作以同步方式执行,这意味着程序会等待每个操作完成后再继续执行。
- 简单性:阻塞套接字简单易懂,因此对于网络编程初学者来说是一个不错的选择。
然而,阻塞套接字的简单性是有代价的。设想这样一种场景:使用阻塞套接字同时与多个客户端通信。如果某个客户端的操作耗时过长,可能会阻塞整个程序,进而导致其他客户端的响应延迟。 为了说明这一点,我们来看一个在 TCP 客户端-服务器应用程序中使用阻塞套接字的基本示例:
当你调用 accept()、recv()、send() 等系统调用时,如果条件不满足,函数会一直等待(阻塞)直到操作完成。
也就是说:
- accept() 会等到有新连接;
- recv() 会等到有数据;
- send() 会等到缓冲区可写。
下面是一段最基础的阻塞 socket 服务端代码(用 C 实现):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUF_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
char buffer[BUF_SIZE];
socklen_t addr_len = sizeof(client_addr);
// 1. 创建 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 2. 绑定地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 3. 监听
listen(server_fd, 5);
printf("Server listening on port %d...\n", PORT);
while (1) {
// 4. 接受连接(阻塞直到有客户端)
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
printf("New client connected.\n");
// 5. 处理客户端请求(阻塞)
while (1) {
int n = recv(client_fd, buffer, BUF_SIZE, 0);
if (n <= 0) {
printf("Client disconnected.\n");
close(client_fd);
break;
}
buffer[n] = '\0';
printf("Received: %s\n", buffer);
send(client_fd, buffer, n, 0); // echo 回去
}
}
close(server_fd);
return 0;
}
假设现在有多个客户端来连接:
-
第一个客户端连上,accept() 返回;
-
服务器进入 recv() 等待第一个客户端发数据;
-
此时第二个客户端连接过来……但服务器还卡在 recv()!
因为 recv() 是阻塞的:
-
服务器此时完全“卡死”在第一个客户端;
-
第二个客户端的连接还在等待 accept();
-
第三个、第四个也会被阻塞;
-
整个服务看起来就像“挂了”
# 启动服务器
./server
# 在另一个终端
nc 127.0.0.1 8080
# 输入一些文字,服务端打印后回显
# 再打开第三个终端
nc 127.0.0.1 8080
# 你会发现它迟迟没有反应,直到第一个客户端断开
Non-blocking Sockets
与阻塞式套接字不同,非阻塞式套接字以异步方式运行。当在非阻塞式套接字上发起 I/O 操作时,无论操作是否成功,程序都会立即继续执行。这种异步行为允许程序在等待 I/O 操作完成的同时执行其他任务,从而提高整体效率和响应速度
非阻塞套接字的主要特性包括:
-
非阻塞行为:I/O 操作会立即返回,即使它们无法立即完成。
-
异步操作:操作以异步方式执行,使程序能够继续执行,而无需等待每个操作完成。
-
复杂性增加:非阻塞套接字会给程序逻辑带来额外的复杂性,因为它需要处理操作可能无法立即完成的情况。 非阻塞套接字关闭时
非阻塞套接字虽然响应速度更快、资源利用率更高,但对异步事件的处理却十分谨慎。开发者必须实现相应的机制来有效管理非阻塞套接字的异步特性,例如采用事件循环或使用诸如 select() 或 poll() 之类的多路复用技术。
// nonblock_echo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define MAX_EVENTS 1024
#define BUF_SIZE 1024
// 设置套接字为非阻塞
int set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl(F_GETFL)");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl(F_SETFL)");
return -1;
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <port>\n", argv[0]);
exit(1);
}
int port = atoi(argv[1]);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
// 允许端口复用
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
close(listen_fd);
exit(1);
}
if (listen(listen_fd, 128) < 0) {
perror("listen");
close(listen_fd);
exit(1);
}
set_nonblock(listen_fd);
int epfd = epoll_create1(0);
if (epfd < 0) {
perror("epoll_create1");
exit(1);
}
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
printf("🚀 Non-blocking Echo Server listening on port %d\n", port);
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds < 0) {
if (errno == EINTR)
continue;
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; ++i) {
int fd = events[i].data.fd;
// 有新客户端连接
if (fd == listen_fd) {
while (1) {
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break; // 没有更多连接
else {
perror("accept");
break;
}
}
printf("🟢 New client connected: fd=%d\n", client_fd);
set_nonblock(client_fd);
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
}
}
// 客户端可读
else if (events[i].events & EPOLLIN) {
char buf[BUF_SIZE];
while (1) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n == 0) {
printf("🔴 Client fd=%d disconnected\n", fd);
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
} else if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break; // 数据读完
perror("recv");
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
}
// 回显
send(fd, buf, n, 0);
}
}
}
}
close(epfd);
close(listen_fd);
return 0;
}