網絡編程 22_非阻塞 I/O + select 多路複用
目標
理解阻塞和非阻塞的區別
- 阻塞
應用程序調用阻塞的 I/O 獲取資源時,在資源可以獲取之前,應用程序會被掛起,進程進入休眠狀態,讓出 CPU 給其它進程,給人的感覺像是被 “阻塞” 了一樣,當進程等待的資源返回時,進程會被喚醒而繼續運行 - 非阻塞
應用程序調用非阻塞 I/O 獲取資源時,不管資源有沒有準備好,內核都會立即返回,應用進程不掛起,因此要是沒獲取到資源,那麼需要應用進程反覆進行獲取資源的操作
一、非阻塞 I/O + select 多路複用
設置套接字爲非阻塞
fcntl(fd, F_SETFL, O_NONBLOCK);
recv()、recvfrom()、recvmsg():用來從 socket 接收消息,可接收無連接和有連接的套接字上的數據
若 socket 上沒有消息到達,調用方會等待,除非 socket 是非阻塞的(fcntl),這種情況下返回值爲 -1,額外的變量 errno 被設置爲 EAGAIN 或 EWOULDBLOCK
服務端
nonblockingserver.c
#include "common.h"
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) {
return c + 13;
} else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) {
return c - 13;
} else {
return c;
}
}
struct Buffer {
int connect_fd;
char buffer[MAXLINE];
size_t write_index;
size_t read_index;
int readable;
};
struct Buffer *alloc_buffer() {
struct Buffer *buffer = malloc(sizeof(struct Buffer));
if (!buffer) {
return NULL;
}
buffer->connect_fd = 0;
buffer->write_index = buffer->read_index = buffer->readable = 0;
return buffer;
}
int onSocketRead(int fd, struct Buffer *buffer) {
char buf[1024];
int i;
ssize_t result;
while (1) {
// 等同於 read
result = recv(fd, buf, sizeof(buf), 0);
// 讀取到文件末尾(EOF)時返回 0
// 非阻塞 O_NONBLOCK 在讀取不到數據時返回 -1,並設置 errno 爲 EAGAIN
if (result <= 0) {
break;
}
for (i = 0; i < result; i++) {
if (buffer->write_index < sizeof(buffer->buffer)) {
buffer->buffer[buffer->write_index++] = rot13_char(buf[i]);
}
if (buf[i] == '\n') {
buffer->readable = 1; // Buffer 緩衝區可讀
}
}
}
if (result == 0) {
return 1;
} else if (result < 0) {
if (errno == EAGAIN) { // 小於 0,errno 爲 EAGAIN
return 0;
}
return -1;
}
return 0;
}
int onSocketWrite(int fd, struct Buffer *buffer) {
while (buffer->read_index < buffer->write_index) {
ssize_t result = send(fd, buffer->buffer + buffer->read_index, buffer->write_index - buffer->read_index, 0);
if (result < 0) {
if (errno == EAGAIN) {
return 0;
}
return -1;
}
buffer->read_index += result;
}
if (buffer->read_index == buffer->write_index) {
buffer->read_index = buffer->write_index = 0;
}
// 到此,表示 send 完成,且返回值大於 0 的數據全部寫出去了,當前沒有數據可讀
buffer->readable = 0;
return 0;
}
int main(int argc, char **argv) {
int maxfd;
int i;
struct Buffer *buffer[FD_INIT_SIZE];
for (i = 0; i< FD_INIT_SIZE; i++) {
buffer[i] = alloc_buffer();
}
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
// 將監聽套接字設置爲非阻塞
fcntl(listenfd, F_SETFL, O_NONBLOCK);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int bind_rt = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (bind_rt < 0) {
error(1, errno, "bind failed");
}
int listen_rt = listen(listenfd, LISTENQ);
if (listen_rt < 0) {
error(1, errno, "listen failed");
}
signal(SIGPIPE, SIG_IGN);
fd_set readset;
fd_set writeset;
fd_set execptset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_ZERO(&execptset);
while (1) {
maxfd = listenfd;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_ZERO(&execptset);
FD_SET(listenfd, &readset);
for (i = 0; i < FD_INIT_SIZE; i++) {
if (buffer[i]->connect_fd > 0) {
if (buffer[i]->connect_fd > maxfd) {
maxfd = buffer[i]->connect_fd;
}
FD_SET(buffer[i]->connect_fd, &readset);
if (buffer[i]->readable) {
FD_SET(buffer[i]->connect_fd, &writeset);
}
}
}
if (select(maxfd + 1, &readset, &writeset, &execptset, NULL) < 0) {
error(1, errno, "select failed");
}
if (FD_ISSET(listenfd, &readset)) {
printf("listen socket readable\n");
sleep(5);
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listenfd, (struct sockaddr *)&ss, &slen);
if (fd < 0) {
error(1, errno, "accept failed");
} else if (fd > FD_INIT_SIZE) {
error(1, 0, "too many connections 111");
close(fd);
} else {
fcntl(fd, F_SETFL, O_NONBLOCK);
if (buffer[fd]->connect_fd == 0) {
buffer[fd]->connect_fd = fd;
} else {
error(1, 0, "too many connections 222");
}
}
}
for (i = 0; i < maxfd + 1; i++) {
int r = 0;
if (i == listenfd) {
continue;
}
if (FD_ISSET(i, &readset)) {
r = onSocketRead(i, buffer[i]);
}
if (r == 0 && FD_ISSET(i, &writeset)) {
r = onSocketWrite(i, buffer[i]);
}
if (r) {
buffer[i]->connect_fd = 0;
close(i);
}
}
}
}
頭文件 common.h
#ifndef CHAP_22_COMMON_H
#define CHAP_22_COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/select.h> /* for convenience */
#include <sys/sysctl.h>
#include <fcntl.h> /* for nonblocking */
void error(int status, int err, char *fmt, ...);
#define SERV_PORT 43211
#define MAXLINE 4096
#define LISTENQ 1024
#define BUFFER_SIZE 4096
#define FD_INIT_SIZE 128
#endif //CHAP_22_COMMON_H
二、CMake 管理當前項目
① 代碼組成
-CMakeLists.txt
-include:存放頭文件
-src:存放源代碼
CMakeLists.txt
CMAKE_MINIMUM_REQUIRED(VERSION 3.1)
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include)
ADD_SUBDIRECTORY(src)
include 目錄:include/common.h(common.h 上面有)
src 目錄(nonblockingserver.c 上面有)
src/CmakeLists.txt
ADD_EXECUTABLE(nonblockingserver nonblockingserver.c)
TARGET_LINK_LIBRARIES(nonblockingserver)
② 創建並進入 build 目錄
mkdir build && cd build
③ 外部編譯
cmake .. && make
三、測試
可以使用一個或多個 telnet 客戶端連接服務器,檢驗交互是否正常
測試步驟
① 打開三個命令行窗口
② 其中一個窗口先執行服務器命令,輸入命令 ./nonblockingserver
後回車
③ 其餘窗口執行客戶端命令,輸入命令 ./telnet-client 127.0.0.1 43211
後回車
左:服務端;右上、右下:客戶端
總結
- 非阻塞 I/O 可以使用 read、write、accept、connect 等多種不同的場景
- 使用非阻塞 I/O 時,調用者需要通過輪詢的方式去不斷嘗試獲取資源,會導致 CPU 佔用率高,輪詢的過程大多做的是無用功
- 非阻塞 I/O 搭配使用 select 多路複用技術,就可以避免做太多無用的輪詢,select 可以阻塞的監聽多個套接字事件,在非阻塞 I/O 事件發生時,再調用對應事件的處理函數