引言 计算机四大基础:
计算机网络在实际中的体现就是计算机网络理论与网络编程。但工作中,我们终究不可能使用最基础的socket进行网络编译。因而学习一个高效的网络库是十分有必要的。为初步了解这个libevent就从最简单的回声服务器开始吧
回声服务器 本身libevent也是基于socket的编程,所以创建Tcp连接的过程同样与是需要遵从创建连接的函数调用顺序,即如下所示:
同样的,对于客户端同样有着一样的调用顺序的要求: libevent 中则相对的简化了这样的繁杂的创建连接的过程,他将这些操作进行了封闭。对于用户而言,我们只需要在传入已实现的回调函数,剩下的即可交由libevent去完成。这也是与libevent设计理念是一致的,即用户无需关心内存事件的检测与处理,只需要去关注事件的具体处理即可 在使用上,有许多的网络库可以使用的:
libev libuv libevent libev是一个linux极为优秀的网络库,Python的协程库gevent
就是一个基于libev的优秀作品。libuv是同样作为nodejs的核心组件,也是功能强大的。但是libev对于Windows平台的支持太差了,而libuv的优秀之处的异步IO与方便的nodejs集成特性,我都不需要,因此最后选择了libevent,可以利用libevent高性能的特性,对性能做到极致的追求!
服务器 回声服务器的功能是: 客户端将消息发送给服务器后,服务器将消息完整的返回给客户端 首先需要先初始化libevent是基础的结构: event_base
1 2 struct event_base * base ;base = event_base_new ()
这时event_base_new()会对event_base中的参数进行初始化,也就相当于创建了一个socket。那么接下来,我们就应该为这个socket 绑定端口与IP了。 这里可使用libevent提供的:evconnlistener_new_bind的接口。其内部的大致的实现流程是:
evconnlistener_new_bind 使用eventlistener_event 存储连接信息 将连接到来时的回调函数实现设置下去 event_assign 将事件(event)与基本事件(event_base)绑定 为处理连接到来,我们还需要提供一个回调函数,根据listen.h中回调函数的定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef void (*evconnlistener_cb) (struct evconnlistener *, evutil_socket_t , struct sockaddr *, int socklen, void *) ;
这个回调函数中,我们需要处理两件事件
读取客户端发送来的数据 将数据原样返回给客户端 依照前言所述,每个client都会分配一个bufferevent去处理连接,因此我们需要使用bufferevent_socket_new()
去创建,但需要先获取我们当前客户端的event_base,因此代码如下:
1 2 3 4 5 6 7 void on_accpet_cb (struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx) { struct event_base * base = evconnlistener_get_base (listener); struct bufferevent * bev = bufferevent_socket_new (base, fd, BEV_OPT_CLOSE_ON_FREE); }
接下来就是设置为读回调与事件回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void EchoServer::on_read_cb (struct bufferevent *bev, void *ctx) { struct evbuffer *input = bufferevent_get_input (bev); struct evbuffer *output = bufferevent_get_output (bev); size_t len = evbuffer_get_length (input); char *data = (char *)malloc (len); evbuffer_copyout (input, data, len); printf ("The message from client is : %s" , data); evbuffer_add_buffer (output, input); free (data); } void EchoServer::echo_event_cb (struct bufferevent *bev, short events, void *ctx) { if (events & BEV_EVENT_ERROR) { perror ("Error" ); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { bufferevent_free (bev); } }
那么回调已经提供了,就可以回到on_accpet_cb中,将回调函数设置下去
1 2 3 4 5 6 // 设置读回调函数和事件回调函数 bufferevent_setcb(bev, on_read_cb, NULL, echo_event_cb, NULL); // 开始监听读事件 bufferevent_enable(bev, EV_READ | EV_WRITE);
参数与回调都已提供后,我们就可以将event绑定到指定的event_base上了
1 2 3 4 5 6 7 8 9 struct sockaddr_in sin ;int port = 8888 ;memset (&sin, 0 , sizeof (sin));sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl (INADDR_ANY); sin.sin_port = htons (port); listener = evconnlistener_new_bind (base, on_accept, NULL , LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSABLE, -1 , (struct sockaddr*)&sin, sizeof (sin));
最后就是启动事件循环,等循环结束后,将listener与event_base清除即可
1 2 3 4 5 event_base_dispatch (base);evconnlistener_free (listener);event_base_free (base);
服务器完整的代码在:EchoServer.cpp
客户端 回声客户端所需的功能只有两个:
输入字符并将字符发送给服务器 获取服务器返回的字符 那么围绕这两个功能,我们还与服务不同的一点的是。我们是客户端并不需要像服务器那般,需要为每个连接都创建一个bufferevent。因此在客户端处,我们仅需要创建一个公用的bufferevent即可。 之后再通过bufferevent_socket_connect()
来创建一个基于socket的buffer event,内部的实现上就是创建了一个bufferevent
对象,用于处理底层socket的事件、数据的读出与写入。因此读取数据就需要传入一个回调函数的实现给bufferevent,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void read_cb (struct bufferevent *bev, void *ctx) { struct evbuffer *input = bufferevent_get_input (bev); size_t len = evbuffer_get_length (input); char *data = new char [len + 1 ]; evbuffer_copyout (input, data, len); data[len] = '\0' ; evbuffer_drain (input, len); std::cout << "Received: " << data << std::endl; delete [] data; } struct event_base *base = event_base_new (); if (!base) { std::cerr << "Could not initialize event base." << std::endl; return ; } struct bufferevent *bev = bufferevent_socket_new ( base, -1 , BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); bufferevent_setcb (bev, read_cb, nullptr , event_cb, base); struct sockaddr_in sin ; memset (&sin, 0 , sizeof (sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr (Ip); sin.sin_port = htons (atoi (port)); if (bufferevent_socket_connect (bev, reinterpret_cast <struct sockaddr *>(&sin), sizeof (sin)) < 0 ) { printf ("My errno is %d" , errno); perror ("Error connecting to server " ); bufferevent_free (bev); exit (1 ); }
解决完读取与客户端与服务器连接问题,我们还需要去处理一下写入事件,即处理用户输入。这里我们就需要在原有的event中添加一个stdin_even
t事件,通过event_add
添加到event_base中。具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void stdin_read_cb (evutil_socket_t fd, short event, void *arg) { char message[1024 ]; struct bufferevent *bev = static_cast <struct bufferevent *> (arg); struct evbuffer *output = bufferevent_get_output (bev); if (bufferevent_getfd (bev) == -1 ) { return ; } if (fgets (message, sizeof (message), stdin) != nullptr ) { evbuffer_add (output, message, strlen (message)); bufferevent_flush (bev, EV_WRITE, BEV_FLUSH); } else { event_base_loopbreak (event_get_base (static_cast <struct event *>(arg))); } } struct event *stdin_event = event_new (base, fileno (stdin), EV_READ | EV_PERSIST, stdin_read_cb, bev); event_add (stdin_event, nullptr );
客户端设计到这里基本上是完成了,但是我们还需要开启bev的可读事件。如果不开启,那么绑定到bufferevent上的read_cb也就不会生效了。代码如下
1 2 3 4 5 6 7 8 9 10 11 // 启用bufferevent读写事件 bufferevent_enable(bev, EV_READ); // 启动事件循环 event_base_dispatch(base); // 释放事件与连接 bufferevent_free(bev); event_base_free(base); event_free(stdin_event);
服务器完整的代码在:Client.cpp
总结 总体来说,在熟悉libevent的基本流程后,写出这样一个demo还是很轻松的。写这样一个demo时,要明确的知道bufferevent的使用,以及对于 socket创建连接的流程。