網絡編程 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 上面有)
include 目錄

src 目錄(nonblockingserver.c 上面有)
src目錄

src/CmakeLists.txt

ADD_EXECUTABLE(nonblockingserver nonblockingserver.c)
TARGET_LINK_LIBRARIES(nonblockingserver)

② 創建並進入 build 目錄

mkdir build && cd build

創建並進入 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 事件發生時,再調用對應事件的處理函數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章