一般步骤
- 创建套接字:使用系统调用创建一个套接字,指定协议族(如
AF_INET
用于IPv4)和套接字类型(如 SOCK_STREAM
用于TCP ,SOCK_DGRAM
用于UDP)。例如在C语言中使用 socket
函数:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- 设置非阻塞模式:通过修改套接字选项,将其设置为非阻塞模式。在Linux下可以使用
fcntl
函数:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
- 绑定与监听(对于服务器端):如果是服务器端,需要将套接字绑定到指定的地址和端口,并开始监听连接请求。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(SERVER_PORT);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(sockfd, BACKLOG);
- 处理I/O操作:
- 接受连接(服务器端):使用
accept
函数(对于TCP)接受客户端连接。由于是非阻塞模式,accept
函数会立即返回,如果没有新连接,会返回错误(如 EAGAIN
或 EWOULDBLOCK
)。
- 发送与接收数据:使用
send
或 recv
函数进行数据的发送和接收。同样,这些函数会立即返回,如果没有足够的数据可接收或发送缓冲区已满,也会返回错误。例如:
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 没有数据可读,继续处理其他任务
} else if (n > 0) {
// 处理接收到的数据
}
- 事件驱动机制:使用事件多路复用技术(如
select
,poll
,epoll
等)来监听套接字上的事件(如可读、可写等)。以 epoll
为例:
int epfd = epoll_create1(0);
struct epoll_event ev;
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == sockfd) {
// 处理新连接或数据
}
}
避免线程阻塞
- 非阻塞I/O操作:如上述步骤中设置套接字为非阻塞模式,使得I/O操作不会等待数据就绪,而是立即返回,让线程可以继续执行其他任务。
- 事件多路复用:使用
select
,poll
,epoll
等机制,线程可以等待多个套接字上的事件,只有当有事件发生时才会被唤醒处理,而不是在每个I/O操作上阻塞等待。
常见应用场景
- 网络爬虫:在爬取大量网页时,爬虫需要同时与多个服务器建立连接获取页面内容。
- 非阻塞I/O优势:
- 高效性:爬虫可以在等待一个连接的数据返回时,同时发起其他连接的请求,而不是阻塞等待单个连接的数据接收完成,大大提高了爬取效率。
- 资源利用率:减少了线程在I/O等待上的时间浪费,使得单个线程可以处理多个并发的网络请求,降低了系统资源的消耗,特别是在需要处理大量连接的情况下。