acl/lib_fiber/README.md

17 KiB
Raw Blame History

高性能网络协程库,支持 Linux/BSD/Mac/Windows

English

4.3、编译使用 io_uring

1、概述

本协程库为 acl 工程 的协程模块,该模块提供了 C 库和 C++ 库,其中 C 库实现了有关协程的核心功能C++ 库是对 C 库的封装从而更方便使用者快速开发协程类应用。目前支持的操作系统有LinuxFreeBSDMacOS 和 Windows支持的事件类型有selectpollepollkqueueiocp 及 Windows GUI 窗口消息。通过 libfiber 网络协程库,用户可以非常容易地写出高性能、高可靠的网络通信服务。因为使用了同步顺序编程的思维方式,相对于异步模式(无论是 reactor 模型还是 proactor 模型),编写网络应用更加简单。
libfiber 不仅支持常见的 IO 事件引擎,而且支持 Win32 GUI 界面消息引擎,这样当你使用 MFCwtl 或其它 GUI 界面库编写界面网络应用时,也会变得异常简单,这的确是件令人兴奋的事。

2、支持的事件引擎

以下为 libfiber 所支持的事件引擎:

  • Linux: select/poll/epoll(Linux2.6+)/io_uring(Linux5.1+)
  • BSD: select/poll/kqueue
  • Mac: select/poll/kqueue
  • Windows: select/poll/IOCP/Windows GUI Message

3、示例

3.1、基于协程的网络服务器

// fiber_server.c

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "fiber/lib_fiber.h"
#include "patch.h" // in lib_fiber/samples/patch.h

static size_t      __stack_size  = 128000;
static const char *__listen_ip   = "127.0.0.1";
static int         __listen_port = 9001;

static void fiber_client(ACL_FIBER *fb, void *ctx)
{
	SOCKET *pfd = (SOCKET *) ctx;
	char buf[8192];

	while (1) {
#if defined(_WIN32) || defined(_WIN64)
		int ret = acl_fiber_recv(*pfd, buf, sizeof(buf), 0);
#else
		int ret = recv(*pfd, buf, sizeof(buf), 0);
#endif
		if (ret == 0) {
			break;
		} else if (ret < 0) {
			if (acl_fiber_last_error() == FIBER_EINTR) {
				continue;
			}
			break;
		}
#if defined(_WIN32) || defined(_WIN64)
		if (acl_fiber_send(*pfd, buf, ret, 0) < 0) {
#else
		if (send(*pfd, buf, ret, 0) < 0) {
#endif			
			break;
		}
	}

	socket_close(*pfd);
	free(pfd);
}

static void fiber_accept(ACL_FIBER *fb, void *ctx)
{
	const char *addr = (const char *) ctx;
	SOCKET lfd = socket_listen(__listen_ip, __listen_port);

	assert(lfd >= 0);

	for (;;) {
		SOCKET *pfd, cfd = socket_accept(lfd);
		if (cfd == INVALID_SOCKET) {
			printf("accept error %s\r\n", acl_fiber_last_serror());
			break;
		}
		pfd = (SOCKET *) malloc(sizeof(SOCKET));
		*pfd = cfd;

		// create and start one fiber to handle the client socket IO
		acl_fiber_create(fiber_client, pfd, __stack_size);
	}

	socket_close(lfd);
	exit (0);
}

// FIBER_EVENT_KERNEL represents the event type on
// Linux(epoll), BSD(kqueue), Mac(kqueue), Windows(iocp)
// FIBER_EVENT_POLL: poll on Linux/BSD/Mac/Windows
// FIBER_EVENT_SELECT: select on Linux/BSD/Mac/Windows
// FIBER_EVENT_WMSG: Win GUI message on Windows
// acl_fiber_create/acl_fiber_schedule_with are in `lib_fiber.h`.
// socket_listen/socket_accept/socket_close are in patch.c of the samples path.

int main(void)
{
	int event_mode = FIBER_EVENT_KERNEL;

#if defined(_WIN32) || defined(_WIN64)
	socket_init();
#endif

	// create one fiber to accept connections
	acl_fiber_create(fiber_accept, NULL, __stack_size);

	// start the fiber schedule process
	acl_fiber_schedule_with(event_mode);

#if defined(_WIN32) || defined(_WIN64)
	socket_end();
#endif

	return 0;
}

3.2、基于协程的客户端程序

// fiber_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "fiber/lib_fiber.h"
#include "patch.h" // in lib_fiber/samples/patch.h

static const char *__server_ip   = "127.0.0.1";
static int         __server_port = 9001;

// socket_init/socket_end/socket_connect/socket_close are in patch.c of the samples path

static void fiber_client(ACL_FIBER *fb, void *ctx)
{
	SOCKET cfd = socket_connect(__server_ip, __server_port);
	const char *s = "hello world\r\n";
	char buf[8192];
	int i, ret;

	if (cfd == INVALID_SOCKET) {
		return;
	}

	for (i = 0; i < 1024; i++) {
#if defined(_WIN32) || defined(_WIN64)
		if (acl_fiber_send(cfd, s, strlen(s), 0) <= 0) {
#else
		if (send(cfd, s, strlen(s), 0) <= 0) {
#endif			
			printf("send error %s\r\n", acl_fiber_last_serror());
			break;
		}

#if defined(_WIN32) || defined(_WIN64)
		ret = acl_fiber_recv(cfd, buf, sizeof(buf), 0);
#else
		ret = recv(cfd, buf, sizeof(buf), 0);
#endif		
		if (ret <= 0) {
			break;
		}
	}

#if defined(_WIN32) || defined(_WIN64)
	acl_fiber_close(cfd);
#else
	close(cfd);
#endif
}

int main(void)
{
	int event_mode = FIBER_EVENT_KERNEL;
	size_t stack_size = 128000;

	int i;

#if defined(_WIN32) || defined(_WIN64)
	socket_init();
#endif

	for (i = 0; i < 100; i++) {
		acl_fiber_create(fiber_client, NULL, stack_size);
	}

	acl_fiber_schedule_with(event_mode);

#if defined(_WIN32) || defined(_WIN64)
	socket_end();
#endif

	return 0;
}

3.3、使用 acl 网络库例子

上面的例子中因为使用了系统原生的网络 API所以感觉代码有些臃肿下面的例子使用 acl 库中提供的网络 API显得更为简单些

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>

class fiber_client : public acl::fiber
{
public:
	fiber_client(acl::socket_stream* conn) : conn_(conn) {}

protected:
	// @override
	void run(void)
	{
		printf("fiber-%d-%d running\r\n", get_id(), acl::fiber::self());

		char buf[8192];
		while (true) {
			int ret = conn_->read(buf, sizeof(buf), false);
			if (ret == -1) {
				break;
			}
			if (conn_->write(buf, ret) == -1) {
				break;
			}
		}

		delete conn_;
		delete this;
	}

private:
	acl::socket_stream* conn_;

	~fiber_client(void) {}
};

class fiber_server : public acl::fiber
{
public:
	fiber_server(acl::server_socket& ss) : ss_(ss) {}
	~fiber_server(void) {}

protected:
	// @override
	void run(void)
	{
		while (true) {
			acl::socket_stream* conn = ss_.accept();
			if (conn == NULL) {
				printf("accept error %s\r\n", acl::last_serror());
				break;
			}

			printf("accept ok, fd: %d\r\n", conn->sock_handle());
			// create one fiber for one connection
			fiber_client* fc = new fiber_client(conn);
			// start the fiber
			fc->start();
		}
	}

private:
	acl::server_socket& ss_;
};

static void usage(const char* procname)
{
	printf("usage: %s -h [help] -s listen_addr\r\n", procname);
}

int main(int argc, char *argv[])
{
	int  ch;

	acl::acl_cpp_init();
	acl::string addr("127.0.0.1:9006");
	acl::log::stdout_open(true);

	while ((ch = getopt(argc, argv, "hs:")) > 0) {
		switch (ch) {
		case 'h':
			usage(argv[0]);
			return 0;
		case 's':
			addr = optarg;
			break;
		default:
			break;
		}
	}

	acl::server_socket ss;
	if (ss.open(addr) == false) {
		printf("listen %s error %s\r\n", addr.c_str(), acl::last_serror());
		return 1;
	}
	printf("listen %s ok\r\n", addr.c_str());

	fiber_server fs(ss);
	fs.start();		// start listen fiber

	acl::fiber::schedule();	// start fiber schedule

	return 0;
}

3.4、使用C++11示例

如果使用C++11的特性则示上面例更为简单如下

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>

static void fiber_client(acl::socket_stream* conn)
{
	printf("fiber-%d running\r\n", acl::fiber::self());

	char buf[8192];
	while (true) {
		int ret = conn->read(buf, sizeof(buf), false);
		if (ret == -1) {
			break;
		}
		if (conn->write(buf, ret) == -1) {
			break;
		}
	}

	delete conn;
}

static void fiber_server(acl::server_socket& ss)
{
	while (true) {
		acl::socket_stream* conn = ss.accept();
		if (conn == NULL) {
			printf("accept error %s\r\n", acl::last_serror());
			break;
		}

		printf("accept ok, fd: %d\r\n", conn->sock_handle());

		go[=] {
			fiber_client(conn);
		};
	}
}

static void usage(const char* procname)
{
	printf("usage: %s -h [help] -s listen_addr\r\n", procname);
}

int main(int argc, char *argv[])
{
	int  ch;

	acl::acl_cpp_init();
	acl::string addr("127.0.0.1:9006");
	acl::log::stdout_open(true);

	while ((ch = getopt(argc, argv, "hs:")) > 0) {
		switch (ch) {
		case 'h':
			usage(argv[0]);
			return 0;
		case 's':
			addr = optarg;
			break;
		default:
			break;
		}
	}

	acl::server_socket ss;
	if (ss.open(addr) == false) {
		printf("listen %s error %s\r\n", addr.c_str(), acl::last_serror());
		return 1;
	}
	printf("listen %s ok\r\n", addr.c_str());

	go[&] {
		fiber_server(ss);
	};

	acl::fiber::schedule();	// start fiber schedule

	return 0;
}

3.5、基于协程的 Windows 界面网络程序

示例目录 下为基于协程的 Windows 界面网络程序,程序运行截屏如图

该 Windows 界面程序包含网络服务器网络客户端两个功能。在运行时,服务模块和客户端模块运行在 Windows 界面线程中,因为协程库使用了 Windows 界面消息泵,所以协程模块可以与界面上元素成为一体而不必跨越线程,也不必使用令人烦恼的异步套接字 API。

3.6、更多例子

acl工程中,有更多的示例来描述网络协程编程,当然,这些例子还大量使用了 acl 库中的其它库的 API。

4、编译协程库

4.1、在 Unix 平台编译

$cd lib_fiber
$make
$cd samples
$make

则会在 lib_fiber/lib/ 目录下生成 libfiber.alibfiber_cpp.a,分别对应着 C 和 C++ 版本的协程库,其中 libfiber_cpp.a 是用 C++ 对 libfiber.a 的二次封装。

下面给出了一个例子的 Makefile 内容:

fiber_server: fiber_server.c
	gcc -o fiber_server fiber_server.c patch.c -I{path_of_fiber_header} -L{path_of_fiber_lib) -lfiber -ldl -lpthread

fiber_client: fiber_client.c
	gcc -o fiber_client fiber_client.c patch.c -I{path_of_fiber_header} -L{path_of_fiber_lib) -lfiber -ldl -lpthread

如果编译并使用 acl 基础库和 libfiber_cpp.a则需要注意库的依赖关系

fiber_server: fiber_server.cpp
	g++ -o fiber_server fiber_server.cpp -lfiber_cpp -lacl_cpp -lprotocol -lacl -lfiber

在该 Makefile 中,-lfiber_cpp 放到最前面,是因为其依赖于其它几个库,而 -lfiber 放在最后,是因为该库需要 hook 系统 IO 操作。

4.2、在 Windows 平台编译

目前可以使用 vc2012/vc2013/vc2015 分别打开 fiber_vc2012.sln /fiber_vc2013.sln/fiber_vc2015.sln 编译 libfiber 库。

4.3、编译使用 io_uring

在 Linux5.1 以上版本开始支持新的事件引擎 io_uring该引擎为IO异步完成模型同时支持网络套接口及文件IO操作效率基本与 epoll 相当,但因为 io_uring 本身提供的系统 API 较为复杂,所以 Jens Axboe磁盘压测工具fio作者提供了二次封装从而使调用过程变得简单可以从https://github.com/axboe/liburing 下载),目前在 Acl 协程中也是通过调用 liburing 中的 API 来使用系统中的 io_uring 功能;下面给出了在 Acl 协程中编译及使用 io_uring 的过程如下:

  • 可从 Ubuntu 官网下下载最新版本的 Ubuntu已经支持了高版本的Linux内核从而支持了io_uring
  • https://github.com/axboe/liburing 下载 liburing 库,并编译安装;
  • https://github.com/acl-dev/acl 下载 acl 源码;
    • 在 shell 环境中设置环境变量打开 acl 中支持 io_uring 的编译条件:export HAS_IO_URING=yes;
    • 进入 acl/lib_fiber/c/ 目录运行 make 编译 acl 协程库因为上面已经设置了支持io_uring的环境变量在Makefile文件中自动增加编译选项 -DHAS_IO_URING
    • 进入 acl/lib_fiber/samples/ 目录,然后再分别进入 server2, client2, file 三个例子编译(需在设置了 HAS_IO_URING=yes 的 shell 环境中编译);
    • 进入 server2/ 目录运行时加上io_uring启动参数./server -e io_uring
    • 进入 client2/ 目录运行时加上io_uring启动参数./client -e io_uring

5、性能测试

下面仅做了简单的 IOPS (网络 IO 性能)的测试,同时和其它协程库做了简单的对比:
Benchmark
其它的网络协程库有:libmillgolang 和 libco。其中,各个库的压测示例:

  1. 基于 libmill 和 libco 的压测用例在 目录 下;
  2. 基于 Golang 的压测用例在 目录中;
  3. 基于 libfiber 的压测用例:示例;
  4. 客户端压测程序:https://github.com/acl-dev/acl/tree/master/lib_fiber/samples/client2

6、API 列表

6.1、Base API

  • acl_fiber_create
  • acl_fiber_self
  • acl_fiber_status
  • acl_fiber_kill
  • acl_fiber_killed
  • acl_fiber_signal
  • acl_fiber_yield
  • acl_fiber_ready
  • acl_fiber_switch
  • acl_fiber_schedule_init
  • acl_fiber_schedule
  • acl_fiber_schedule_with
  • acl_fiber_scheduled
  • acl_fiber_schedule_stop
  • acl_fiber_set_specific
  • acl_fiber_get_specific
  • acl_fiber_delay
  • acl_fiber_last_error
  • acl_fiber_last_serror

6.2、IO API

  • acl_fiber_recv
  • acl_fiber_recvfrom
  • acl_fiber_read
  • acl_fiber_readv
  • acl_fiber_recvmsg
  • acl_fiber_write
  • acl_fiber_writev
  • acl_fiber_send
  • acl_fiber_sendto
  • acl_fiber_sendmsg
  • acl_fiber_select
  • acl_fiber_poll
  • acl_fiber_close

6.3、Net API

  • acl_fiber_socket
  • acl_fiber_listen
  • acl_fiber_accept
  • acl_fiber_connect
  • acl_fiber_gethostbyname_r
  • acl_fiber_getaddrinfo
  • acl_fiber_freeaddrinfo

6.4、Channel API

  • acl_channel_create
  • acl_channel_free
  • acl_channel_send
  • acl_channel_send_nb
  • acl_channel_recv
  • acl_channel_recv_nb
  • acl_channel_sendp
  • acl_channel_recvp
  • acl_channel_sendp_nb
  • acl_channel_recvp_nb
  • acl_channel_sendul
  • acl_channel_recvul
  • acl_channel_sendul_nb
  • acl_channel_recvul_nb

6.5、Sync API

ACL_FIBER_MUTEX

  • acl_fiber_mutex_create
  • acl_fiber_mutex_free
  • acl_fiber_mutex_lock
  • acl_fiber_mutex_trylock
  • acl_fiber_mutex_unlock

ACL_FIBER_RWLOCK

  • acl_fiber_rwlock_create
  • acl_fiber_rwlock_free
  • acl_fiber_rwlock_rlock
  • acl_fiber_rwlock_tryrlock
  • acl_fiber_rwlock_wlock
  • acl_fiber_rwlock_trywlock
  • acl_fiber_rwlock_runlock
  • acl_fiber_rwlock_wunlock

ACL_FIBER_EVENT

  • acl_fiber_event_create
  • acl_fiber_event_free
  • acl_fiber_event_wait
  • acl_fiber_event_trywait
  • acl_fiber_event_notify

ACL_FIBER_SEM

  • acl_fiber_sem_create
  • acl_fiber_sem_free
  • acl_fiber_sem_wait
  • acl_fiber_sem_post
  • acl_fiber_sem_num

6.6、关于 API Hook

在 Linux/MacOS/FreeBSD 平台上,很多与 IO 和网络相关的的系统 API 被 hook 了,因此,在编译连接时将 libfiber 加上,这样你的应用程序中仅需使用系统标准 IO API便可以使你的网络程序自动协程化。下面是一些被 hook 的系统 API 列表:

  • socket/listen/accept/connect
  • select/poll/epoll: epoll_create, epoll_ctl, epoll_wait
  • read/readv/recv/recvfrom/recvmsg/write/writev/send/sendto/sendmsg
  • pread/pwrite/splice/sendfile64
  • gethostbyname(_r)/getaddrinfo/freeaddrinfo
  • open/openat/close/unlink/rename/renameat/renameat2/stat/statx/mkdir/mkdirat
  • sleep

7、更多参考