libevent
在了解了libevent的安装后,再来了解一下它的使用,特别是进行多线程的使用。其实这个最大的好处就是他在内部帮你封装好了epoll/select多路复用,并且使用了一些设计模式(比如反应堆模式),用事件机制来简化了socket编程然后要用的时候直接把相应的文件描述符加进去即可!
1.libevent的好处
- 假设有N个客户端同时往服务端通过socket写数据,用了libevent之后,你的server程序里就不用再使用epoll或是select来判断都哪些socket的缓冲区里已经收到了客户端写来的数据。当某个socket的缓冲区里有可读数据时,libevent会自动触发一个“读事件”,通过这个“读事件”来调用相应的代码来读取socket缓冲区里的数据即可。换句话说,libevent自己调用select()或是epoll的函数来判断哪个缓冲区可读了,只要可读了,就自动调用相应的处理程序。
- 对于“写事件”,libevent会监控某个socket的缓冲区是否可写(一般情况下,只要缓冲区没满就可写),只要可写,就会触发“写事件”,通过“写事件”来调用相应的函数,将数据写到socket里。
2.libevent的使用
其实服务器端libevent的使用也就是几个步骤,通俗一点就算再创建了libevent对象之后把监听的socket加进去,然后进入事件循环就好了!那么具体来看看步骤:
(1). 首先我们必须了解socket的一般流程,然后socket、bind、listen,再设置为非阻塞模式。
(2). 创建一个event_base对象
//创建一个event_base
struct event_base *base = event_base_new();
assert(base != NULL);
struct event_base *base = event_base_new()用以创建一个事件处理的全局变量,可以理解为这是一个负责集中处理各种出入IO事件的总管家,它负责接收和派发所有输入输出IO事件的信息,这里调用的是函数event_base_new(), 很多程序里这里用的是event_init(),区别就是前者是线程安全的、而后者是非线程安全的,后者在其官方说明中已经被标志为过时的函数、且建议用前者代替,libevent中还有很多类似的函数,比如建议用event_base_dispatch代替event_dispatch,用event_assign代替event_set和event_base_set等,关于libevent接口的详细说明见其官方说明libevent_doc。
event_base内部有一个循环,循环阻塞在epoll/kqueue等系统调用上,直到有一个或者一些事件发生,然后去处理这些事件。当然,这些事件要被绑定在这个event_base上。每个事件对应一个struct event,可以是监听一个fd或者POSIX信号量之类。struct event使用event_new来创建和绑定,使用event_add来启用。
(3). 创建一个event对象,并且将其监听的socket托管给event_base,指定要监听的事件类型,并绑上相应的回调函数
//创建并绑定一个event
struct event *listen_event;
//参数:event_base, 监听的fd,事件类型及属性,绑定的回调函数,给回调函数的参数
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void*)base);
(4). 通过event_add方法启动监听事件
//参数:event,超时时间(struct timeval *类型的,NULL表示无超时设置)
event_add(listen_event, NULL);
(5). 进入事件循环
需要启动event_base的循环,这样才能开始处理发生的事件。循环的启动使用event_base_dispatch,循环将一直持续,直到不再有需要关注的事件,或者是遇到event_loopbreak()/event_loopexit()函数。
这里事件循环的好处就是写程序的时候就不用采用死循环(while(1)/for(; ;))方式去连接,因为这里会事件循环,有事情触发的时候便会调用相应的回调函数!
//启动事件循环
event_base_dispatch(base);
在执行完这5步之后大致的流程就走完了,然后连接上客户端之后在缓冲区调用wirte()/read()函数就可以发送和接收数据了。同时可以接收多个客户端的连接,因为epoll/selecet多路复用已经封装在libevent库里面了!
3.libevent编写多线程
libevent中多线程的编写最主要的一点就是创建多个base,可以采用在主线程里面创建一个base,然后当有客户端连接的时候就创建一个子线程,重新创建一个base,处理这个客户端的消息,接收或者发送数据。
但是这样写的话会有局限性,来一个客户端就创建线程,如果客户端很多的话,内存可能会崩溃,下面会接着学习处理这个问题。
在写程序之前我们可以养成一个习惯,先试着画一下流程图,它可以帮助你整理一下思路:
然后我贴一下代码:
/*********************************************************************************
* Copyright: (C) 2020 Juan
* All rights reserved.
*
* Filename: G4module_server.c
* Description: This file
*
* Version: 1.0.0(22/07/20)
* Author: wangruijie <1046299465@qq.com>
* ChangeLog: 1, Release initial version on "22/07/20 03:09:18"
*
********************************************************************************/
#include"G4module_server.h"
int main(int argc, char **argv)
{
int sockfd = -1; //赋一个无关紧要的值
int rv = -1;
struct sockaddr_in local_addr; //网络地址结构体
struct event_base* base = event_base_new(); //用以创建一个事件处理的全局变量,可以理解为这是一个负责集中处理各种出入IO事件的总管家
struct event listen_ev; //创建一个event对象
int opt;
char buf[1024];
char *progname = NULL;
int SERVER_PORT;
struct sock_ev_write *sock_ev_write_struct;
struct option long_options[]=
{
{
"port", required_argument, NULL, 'p'},
{
"help", no_argument, NULL, 'h'},
{
NULL, 0, NULL, 0}
};
progname = basename(argv[0]);
while((opt = getopt_long(argc,argv