以tcp_echo_server.c为例,说明创建tcp服务器的程序流程。main函数非常简单:
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: tcp port\n");
return -10;
}
int port = atoi(argv[1]);
hloop_t* loop = hloop_new(0); //创建一个loop
//创建一个用来监听的io,用来处理客户端连接事件
hio_t* listenio = hloop_create_tcp_server(loop, "0.0.0.0", port, on_accept);
if (listenio == NULL) {
return -20;
}
printf("listenfd=%d\n", hio_fd(listenio));
hloop_run(loop); //正常情况下,会一直阻塞在该接口
hloop_free(&loop);
return 0;
}
第一步就是先使用hloop_new创建一个loop,因为libhv是one loop per thread模型,所以该结构体也是整个网络库的核心。大体看下创建流程。
hloop_t* hloop_new(int flags) {
hloop_t* loop;
HV_ALLOC_SIZEOF(loop); //申请内存
hloop_init(loop); //初始化
loop->flags |= flags; //设置flag
return loop;
}
上面的flag因为在本程序中没有用,所以先不说了。核心就是hloop_init:
static void hloop_init(hloop_t* loop) {
loop->status = HLOOP_STATUS_STOP;
loop->pid = hv_getpid(); //获取进程ID
loop->tid = hv_gettid(); //获取线程ID,注意,此线程ID非pthread_self的那个pthread_t类型的线程id
// idles
list_init(&loop->idles); //初始化空闲事件列表
// timers
heap_init(&loop->timers, timers_compare); //初始化定时器堆
// ios
io_array_init(&loop->ios, IO_ARRAY_INIT_SIZE); //初始化io事件集合
// readbuf
loop->readbuf.len = HLOOP_READ_BUFSIZE;
HV_ALLOC(loop->readbuf.base, loop->readbuf.len); //初始化读缓冲区
// iowatcher
iowatcher_init(loop); //初始化io事件监视器
// custom_events
//下面是与custom事件相关的初始化
hmutex_init(&loop->custom_events_mutex);
event_queue_init(&loop->custom_events, CUSTOM_EVENT_QUEUE_INIT_SIZE);
loop->sockpair[0] = loop->sockpair[1] = -1;
if (Socketpair(AF_INET, SOCK_STREAM, 0, loop->sockpair) != 0) {
hloge("socketpair create failed!");
}
// NOTE: init start_time here, because htimer_add use it.
//设置开始时间,这个时间是从Epoch开始的时间,但实际上该时间是可以通过修改系统时间修改的,这就是为什么需要下面的时间
loop->start_ms = gettimeofday_ms();
//这个时间是无法被修改的相对时间,通过这个无法被修改的相对时间,就可以调整上面的开始时间
loop->start_hrtime = loop->cur_hrtime = gethrtime_us();
}
关于线程ID的说明,可以参考陈硕大神的《Linux多线程服务端编程》,或者我的muduo系列博客https://blog.csdn.net/qu1993/article/details/109403603,这里就不再特殊说明了。
空闲事件、定时器事件、custom事件前面都分析过了,实际上这一篇博客主要就是分析io事件的。最后的关于时间的说明比较简单,因为时间相关的内容本身就比较复杂,这篇博客也不多分析了。
所以上面的初始化大多数都已经分析过了,这也是为什么我在写了前面那些篇博客之后才准备分析最重要的服务器创建流程,应该说前面的博客都是为这一篇做铺垫的,这一篇也是本系列博客中最重要的一篇。
初始化之后,libhv提供了一个非常简单的创建服务端的接口,hloop_create_tcp_server
// @tcp_server: socket -> bind -> listen -> haccept
// @see examples/tcp.c
HV_EXPORT hio_t* hloop_create_tcp_server (hloop_t* loop, const char* host, int port, haccept_cb accept_cb);
这个接口封装了创建socket,绑定bind、监听listen、接受客户端accept回调等整个服务端创建流程。下面分析这个创建服务端最重要的接口。
hio_t* hloop_create_tcp_server (hloop_t* loop, const char* host, int port, haccept_cb accept_cb) {
int listenfd = Listen(port, host);
if (listenfd < 0) {
return NULL;
}
hio_t* io = haccept(loop, listenfd, accept_cb);
if (io == NULL) {
closesocket(listenfd);
}
return io;
}
Listen接口获取监听套接字描述符,基本就是那套固定流程 socket -> bind -> listen,不再多说。比较重要的是下面的haccept接口:
hio_t* haccept(hloop_t* loop, int listenfd, haccept_cb accept_cb) {
hio_t* io = hio_get(loop, listenfd);
assert(io != NULL);
if (accept_cb) {
io->accept_cb = accept_cb;
}
hio_accept(io);
return io;
}
hio_get之前就遇到过,但是没有分析,因为本来就打算在这里分析的。
hio_t* hio_get(hloop_t* loop, int fd) {
//因为fd是io array的下标,所以要确保下标是有效的
if (fd >= loop->ios.maxsize) {
int newsize = ceil2e(fd);
io_array_resize(&loop->ios, newsize > fd ? newsize : 2*fd);
}
hio_t* io = loop->ios.ptr[fd];
//判断下标为fd的io结构体指针是否有效,如果为NULL,创建一个新的io结构体
if (io == NULL) {
HV_ALLOC_SIZEOF(io); //申请内存
hio_init(io); //初始化结构体,实际上目前该接口为空
io->event_type = HEVENT_TYPE_IO; //设置事件类型为IO
io->loop = loop; //设置该io结构体所属loop
io->fd = fd; //该io结构体代表的fd
loop->ios.ptr[fd] = io; //将新创建的io结构体加入loop的管理
}
if (!io->ready) {
hio_ready(io);
}
return io;
}
每一个io结构体都代表一个描述符,描述符的值就是该io结构体在loop的io array中的位置。刚初始化后ready为0,所以调用hio_ready:
void hio_ready(hio_t* io) {
if (io->ready) return;
// flags
io->ready = 1;
io->closed = 0;
io->accept = io->connect = io->connectex = 0;
io->recv = io->send = 0;
io->recvfrom = io->sendto = 0;
io->close = 0;
// public:
io->io_type = HIO_TYPE_UNKNOWN;
io->error = 0;
io->events = io->revents = 0;
// callbacks
io->read_cb = NULL;
io->write_cb = NULL;
io->close_cb = NULL;
io->accept_cb = NULL;
io->connect_cb = NULL;
// timers
io->connect_timeout = 0;
io->connect_timer = NULL;
io->close_timeout = 0;
io->close_timer = NULL;
io->keepalive_timeout = 0;
io->keepalive_timer = NULL;
io->heartbeat_interval = 0;
io->heartbeat_fn = NULL;
io->heartbeat_timer = NULL;
// private:
io->event_index[0] = io->event_index[1] = -1;
io->hovlp = NULL;
io->ssl = NULL;
// io_type
fill_io_type(io);
if (io->io_type & HIO_TYPE_SOCKET) {
hio_socket_init(io);
}
}
最开始就是设置各种初始值,有一些已经在前面的博客中提到过了,其他的也先不说了,这里的关键是后面的两个接口,第一个是设置io类型的fill_io_type接口:
static void fill_io_type(hio_t* io) {
int type = 0;
socklen_t optlen = sizeof(int);
int ret = getsockopt(io->fd, SOL_SOCKET, SO_TYPE, (char*)&type, &optlen);
printd("getsockopt SO_TYPE fd=%d ret=%d type=%d errno=%d\n", io->fd, ret, type, socket_errno());
if (ret == 0) { //如果成功,说明是socket io,根据相应的type设置io类型
switch (type) {
case SOCK_STREAM: io->io_type = HIO_TYPE_TCP; break;
case SOCK_DGRAM: io->io_type = HIO_TYPE_UDP; break;
case SOCK_RAW: io->io_type = HIO_TYPE_IP; break;
default: io->io_type = HIO_TYPE_SOCKET; break;
}
}
//如果不是socket io,根据fd设置是输入、输出、错误输出类型,其他认为是文件io
else if (socket_errno() == ENOTSOCK) {
switch (io->fd) {
case 0: io->io_type = HIO_TYPE_STDIN; break;
case 1: io->io_type = HIO_TYPE_STDOUT; break;
case 2: io->io_type = HIO_TYPE_STDERR; break;
default: io->io_type = HIO_TYPE_FILE; break;
}
}
else {
io->io_type = HIO_TYPE_TCP;
}
}
通过getsockopt获取io类型,设置完io类型后,在hio_ready接口的最后会判断是不是socket io,如果是socket io,需要调用hio_socket_init进一步设置:
static void hio_socket_init(hio_t* io) {
// nonblocking
//在libhv网络库中,所有的socket io都是非阻塞的
nonblocking(io->fd);
// fill io->localaddr io->peeraddr
if (io->localaddr == NULL) {
HV_ALLOC(io->localaddr, sizeof(sockaddr_u));
}
if (io->peeraddr == NULL) {
HV_ALLOC(io->peeraddr, sizeof(sockaddr_u));
}
socklen_t addrlen = sizeof(sockaddr_u);
//获取本端地址
int ret = getsockname(io->fd, io->localaddr, &addrlen);
printd("getsockname fd=%d ret=%d errno=%d\n", io->fd, ret, socket_errno());
// NOTE:
// tcp_server peeraddr set by accept
// udp_server peeraddr set by recvfrom
// tcp_client/udp_client peeraddr set by hio_setpeeraddr
if (io->io_type == HIO_TYPE_TCP || io->io_type == HIO_TYPE_SSL) {
// tcp acceptfd
addrlen = sizeof(sockaddr_u);
//获取对端地址
ret = getpeername(io->fd, io->peeraddr, &addrlen);
printd("getpeername fd=%d ret=%d errno=%d\n", io->fd, ret, socket_errno());
}
}
该接口的重点是设置非阻塞属性,libhv库的所有的socket io都会调用该接口,设置为非阻塞的,因为one loop per thread模型,只能阻塞在loop等待事件,读写本身不应该阻塞。
再回到上面的haccept接口,通过hio_get获取了listenfd描述符对应的io结构体后,设置accept的回调,之后调用hio_accept
if (accept_cb) {
io->accept_cb = accept_cb;
}
hio_accept(io);
这里使用的是nio.c文件的hio_accept接口,之后涉及跨平台的,都以linux为例说明,但就流程而言,应该没有太大影响
int hio_accept(hio_t* io) {
io->accept = 1; //设置该io是是用来accept客户端的
//将该io加入到io事件监视器中
hio_add(io, hio_handle_events, HV_READ);
return 0;
}
hio_add就是将io加入到io事件监视器中,并指定自己需要关注的事件类型是HV_READ可读属性。因为当客户端连接服务器时,会令监听套接字成为可读的。当监听套接字的可读事件触发时,会调用hio_handle_events函数。接下来看下hio_add:
int hio_add(hio_t* io, hio_cb cb, int events) {
printd("hio_add fd=%d events=%d\n", io->fd, events);
#ifdef OS_WIN
// Windows iowatcher not work on stdio
if (io->fd < 3) return -1;
#endif
hloop_t* loop = io->loop;
if (!io->active) {
EVENT_ADD(loop, io, cb);
//loop的io个数加1
loop->nios++;
}
if (!io->ready) {
hio_ready(io);
}
if (cb) {
io->cb = (hevent_cb)cb;
}
//加入io事件监视器
iowatcher_add_event(loop, io->fd, events);
io->events |= events;
return 0;
}
多次遇到EVENT_ADD宏了,简单看看吧
#define EVENT_ADD(loop, ev, cb) \
do {\
ev->loop = loop;\
//设置事件ID
ev->event_id = ++loop->event_counter;\
//设置事件回调函数
ev->cb = (hevent_cb)cb;\
//将该io设置为活跃状态
EVENT_ACTIVE(ev);\
} while(0)
#define EVENT_ACTIVE(ev) \
if (!ev->active) {\
ev->active = 1;\
//loop的活跃事件加1
ev->loop->nactives++;\
}\
hio_add接口的ready判断,因为前面已经设置过了,所以这里ready为1。重点是iowatcher_add_event,这个接口将监听套接字加入到io事件监视器中。因为这里具体的监视器涉及到许多不同的实现方式,而且比较容易理解,就不再分析。
到这里hloop_create_tcp_server整个接口就分析完了,我们关心的监听功能的io已经加入到io事件监视器中,剩下的就交给loop的主循环loop_run了。
int hloop_run(hloop_t* loop) {
loop->pid = hv_getpid();
loop->tid = hv_gettid();
// intern events
int intern_events = 0;
if (loop->sockpair[0] != -1 && loop->sockpair[1] != -1) {
hread(loop, loop->sockpair[SOCKPAIR_READ_INDEX], loop->readbuf.base, loop->readbuf.len, sockpair_read_cb);
++intern_events;
}
#ifdef DEBUG
htimer_add(loop, hloop_stat_timer_cb, HLOOP_STAT_TIMEOUT, INFINITE);
++intern_events;
#endif
//设置loop状态
loop->status = HLOOP_STATUS_RUNNING;
//可以通过设置loop状态对其进行控制
while (loop->status != HLOOP_STATUS_STOP) {
if (loop->status == HLOOP_STATUS_PAUSE) {
msleep(HLOOP_PAUSE_TIME);
hloop_update_time(loop);
continue;
}
++loop->loop_cnt;
//判断是否设置了当没有事件时,主动退出标志
if (loop->nactives <= intern_events && loop->flags & HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS) {
break;
}
//处理事件
hloop_process_events(loop);
if (loop->flags & HLOOP_FLAG_RUN_ONCE) {
break;
}
}
loop->status = HLOOP_STATUS_STOP;
loop->end_hrtime = gethrtime_us();
if (loop->flags & HLOOP_FLAG_AUTO_FREE) {
hloop_cleanup(loop);
HV_FREE(loop);
}
return 0;
}
开始的sockpair在custom事件博客中分析过了,还有上面的判断flag是否包含HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS,如果设置了HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS,当没有事件时loop会自动退出。这个标志就是博客开始提到的可以在调用hloop_new时设置的,这个标志正常服务器程序不应该设置。hloop_process_events是本接口的核心,之前也提到过该接口,hloop_process_events就是处理所有的事件的,不管是定时器、idle还是io事件。
static int hloop_process_events(hloop_t* loop) {
// ios -> timers -> idles
int nios, ntimers, nidles;
nios = ntimers = nidles = 0;
// calc blocktime
//处理定时器事件
int32_t blocktime = HLOOP_MAX_BLOCK_TIME;
if (loop->timers.root) {
hloop_update_time(loop);
uint64_t next_min_timeout = TIMER_ENTRY(loop->timers.root)->next_timeout;
int64_t blocktime_us = next_min_timeout - hloop_now_hrtime(loop);
if (blocktime_us <= 0) goto process_timers;
blocktime = blocktime_us / 1000;
++blocktime;
blocktime = MIN(blocktime, HLOOP_MAX_BLOCK_TIME);
}
if (loop->nios) {
nios = hloop_process_ios(loop, blocktime);
}
else {
msleep(blocktime);
}
hloop_update_time(loop);
// wakeup by hloop_stop
if (loop->status == HLOOP_STATUS_STOP) {
return 0;
}
process_timers:
if (loop->ntimers) {
ntimers = hloop_process_timers(loop);
}
int npendings = loop->npendings;
if (npendings == 0) {
if (loop->nidles) {
nidles= hloop_process_idles(loop);
}
}
int ncbs = hloop_process_pendings(loop);
// printd("blocktime=%d nios=%d/%u ntimers=%d/%u nidles=%d/%u nactives=%d npendings=%d ncbs=%d\n",
// blocktime, nios, loop->nios, ntimers, loop->ntimers, nidles, loop->nidles,
// loop->nactives, npendings, ncbs);
return ncbs;
}
最开始处理定时器事件,在分析定时器的博客中已经分析过了,这里主要说下在处理定时器时,如果定时器存在并且没到期,会对距离下一个定时器到期的时间和HLOOP_MAX_BLOCK_TIME进行比较,获取两者中的较小的值,作为io事件监视器的默认阻塞时间。原因是如果有定时器马上到期了,但是loop却阻塞在io事件监视器,会影响定时器的处理,所以这里将loop阻塞的最大时间设置为不超过下次定时器到期的时间,对定时器事件的处理能更及时。
接下来判断是否有io事件,如果存在关心的io事件,调用hloop_process_ios阻塞等待io事件的发生。
static int hloop_process_ios(hloop_t* loop, int timeout) {
int nevents = iowatcher_poll_events(loop, timeout);
if (nevents < 0) {
hloge("poll_events error=%d", -nevents);
}
return nevents < 0 ? 0 : nevents;
}
哈哈,上面我说因为io事件监视器有好几种方式实现的,不再详细说明,但这里必须要详细说下了,以epoll为例,原因是我只看了epoll的实现。。。。。
int iowatcher_poll_events(hloop_t* loop, int timeout) {
epoll_ctx_t* epoll_ctx = (epoll_ctx_t*)loop->iowatcher;
if (epoll_ctx == NULL) return 0;
if (epoll_ctx->events.size == 0) return 0;
int nepoll = epoll_wait(epoll_ctx->epfd, epoll_ctx->events.ptr, epoll_ctx->events.size, timeout);
if (nepoll < 0) {
perror("epoll");
return nepoll;
}
if (nepoll == 0) return 0;
int nevents = 0;
for (int i = 0; i < epoll_ctx->events.size; ++i) {
struct epoll_event* ee = epoll_ctx->events.ptr + i;
int fd = ee->data.fd;
uint32_t revents = ee->events;
if (revents) {
++nevents;
hio_t* io = loop->ios.ptr[fd];
if (io) {
if (revents & (EPOLLIN | EPOLLHUP | EPOLLERR)) {
io->revents |= HV_READ;
}
if (revents & (EPOLLOUT | EPOLLHUP | EPOLLERR)) {
io->revents |= HV_WRITE;
}
EVENT_PENDING(io);
}
}
if (nevents == nepoll) break;
}
return nevents;
}
调用epoll_wait等待事件的发生,最多等待timeout时间,假设在timeout内,有其他的客户端连接我们的服务端,这时候epoll_wait会返回,并将事件放到epoll_ctx->events.ptr中,遍历epoll_ctx->events.ptr。并且判断事件发生的类型,因为客户端连接,所以监听套接字的可读事件发生,即EPOLLIN类型,将该事件通过EVENT_PENDING(io)加入到待处理事件集合中,EVENT_PENDING在事件优先级说明的博客中分析过,这里不再多说。把所有发生的事件遍历完,实际上这里只有一个事件,都加入到待处理集合中后,从该接口返回。所以这里我们可以看到,libhv对事件的获取和处理是分开的。
返回到hloop_process_events中,调用hloop_process_pendings接口处理刚才加入到待处理事件集合中的所有事件。
static int hloop_process_pendings(hloop_t* loop) {
if (loop->npendings == 0) return 0;
hevent_t* cur = NULL;
hevent_t* next = NULL;
int ncbs = 0;
for (int i = HEVENT_PRIORITY_SIZE-1; i >= 0; --i) {
cur = loop->pendings[i];
while (cur) {
next = cur->pending_next;
if (cur->pending) { //表示该事件待处理
if (cur->active && cur->cb) {
cur->cb(cur); //调用注册的回调函数
++ncbs;
}
cur->pending = 0; //将待处理属性清空
if (cur->destroy) {
EVENT_DEL(cur);
}
}
cur = next;
}
loop->pendings[i] = NULL;
}
loop->npendings = 0;
return ncbs;
}
这里就是简单的根据优先级处理各种事件,调用之前注册的回调函数。还记得在hio_accept中注册的回调函数hio_handle_events吗?这里该函数就会被调用。
static void hio_handle_events(hio_t* io) {
if ((io->events & HV_READ) && (io->revents & HV_READ)) {
if (io->accept) {
nio_accept(io);
}
else {
nio_read(io);
}
}
if ((io->events & HV_WRITE) && (io->revents & HV_WRITE)) {
// NOTE: del HV_WRITE, if write_queue empty
if (write_queue_empty(&io->write_queue)) {
iowatcher_del_event(io->loop, io->fd, HV_WRITE);
io->events &= ~HV_WRITE;
}
if (io->connect) {
// NOTE: connect just do once
// ONESHOT
io->connect = 0;
nio_connect(io);
}
else {
nio_write(io);
}
}
io->revents = 0;
}
很容易就能看出,我们的监听套接字io要调用的接口是nio_accept
static void nio_accept(hio_t* io) {
//printd("nio_accept listenfd=%d\n", io->fd);
socklen_t addrlen;
accept:
addrlen = sizeof(sockaddr_u);
//accept客户端,获取与客户端通信的connfd描述符
int connfd = accept(io->fd, io->peeraddr, &addrlen);
hio_t* connio = NULL;
if (connfd < 0) {
if (socket_errno() == EAGAIN) {
//goto accept_done;
return;
}
else {
io->error = socket_errno();
perror("accept");
goto accept_error;
}
}
addrlen = sizeof(sockaddr_u);
getsockname(connfd, io->localaddr, &addrlen);
//创建一个和客户端通信的io结构体
connio = hio_get(io->loop, connfd);
// NOTE: inherit from listenio
connio->accept_cb = io->accept_cb;
connio->userdata = io->userdata;
if (io->io_type == HIO_TYPE_SSL) {
hssl_ctx_t ssl_ctx = hssl_ctx_instance();
if (ssl_ctx == NULL) {
goto accept_error;
}
hssl_t ssl = hssl_new(ssl_ctx, connfd);
if (ssl == NULL) {
goto accept_error;
}
hio_enable_ssl(connio);
connio->ssl = ssl;
ssl_server_handshark(connio);
}
else {
// NOTE: SSL call accept_cb after handshark finished
__accept_cb(connio);
}
goto accept;
accept_error:
hio_close(io);
}
首先就是调用accept,获取与客户端通信的描述符;前面提到过,一个描述符对应一个io结构体,所以这里为该描述符创建了一个io结构体,用来处理与客户端的通信。下面关于SSL的忽略,因为我不懂。。。。 最后调用__accept_cb,这个接口实际上在分析心跳时介绍过,不过这里的重点不同了。我去掉了前面提到的心跳相关的内容:
static void __accept_cb(hio_t* io) {
if (io->accept_cb) {
// printd("accept_cb------\n");
io->accept_cb(io);
// printd("accept_cb======\n");
}
}
这里会调用我们最初注册的回调函数,再回到最初的起点。。。。 我们在调用hloop_create_tcp_server时,注册了一个回调函数,on_accept,这里该函数会被调用。
static void on_accept(hio_t* io) {
printf("on_accept connfd=%d\n", hio_fd(io));
char localaddrstr[SOCKADDR_STRLEN] = {0};
char peeraddrstr[SOCKADDR_STRLEN] = {0};
printf("accept connfd=%d [%s] <= [%s]\n", hio_fd(io),
SOCKADDR_STR(hio_localaddr(io), localaddrstr),
SOCKADDR_STR(hio_peeraddr(io), peeraddrstr));
//设置关闭回调
hio_setcb_close(io, on_close);
//设置读回调
hio_setcb_read(io, on_recv);
//使能读
hio_read(io);
}
该回调的参数是accept获取的那个与客户端通信的io,而不是我们的监听套接字io。这个回调函数是我们自己设置的,可以在这里做一些我们感兴趣的事情,还是以tcp_echo_server.c为例。这里的on_accept回调设置了io关闭回调,读回调以及使能读。调用hio_read后,该io也就加入到io事件监视器中,这样io监视器就有两个关心的事件了,一个是我们的监听套接字io,一个是与客户端通信的io事件。一旦与客户端通信的io收到客户端的信息时,可读触发,调用on_recv回调,过程与刚才的监听套接字io差不多,不再分析了。
暂时先分析这么多吧。。。。。 这里主要涉及了读事件,之后应该再单独分析下写事件,因为写远比读复杂