一、问题
在使用libevent实现websocket服务器时,发生了并发访问的问题。
服务器程序功能主要包括实时响应Websocket客户端的控制请求,同时发送温度到客户端。
现象:
不加上温度发送功能时,程序正常运行
加上温度发送功能后,就会出现段错误,而且检查后发现bufferevent并不为空
二、原因
在我的代码中,temp_cb()
用于发送温度,read_cb()
用于读取客户端发送来的数据(包括连接请求和led控制等),在没有加上温度发送功能前,程序中bufferevent
只用处理错误事件和接收数据事件,加上后还要处理发送温度的事件。如果在发送温度事件时,同时可能有其他事件或回调函数正在修改或访问相同的 bev
变量,可能会导致竞态条件或意外的状态更改,从而引发段错误,导致并发问题。
代码如下:
static void temp_cb(evutil_socket_t fd, short events, void *arg)
{
struct bufferevent *bev = (struct bufferevent *)arg;
if (!bev) {
log_error("Received NULL buffer event in temp_cb\n");
return;
}
send_temperature(bev);
}
static void read_cb (struct bufferevent *bev, void *ctx)
{
wss_session_t *session = bev->cbarg;
if( !session->handshaked )
{
do_wss_handshake(session);
return ;
}
do_parser_frames(session);
return ;
}
static void event_cb (struct bufferevent *bev, short events, void *ctx)
{
wss_session_t *session = bev->cbarg;
if( events&(BEV_EVENT_EOF|BEV_EVENT_ERROR) )
{
if( session )
log_warn("remote client %s closed\n", session->client);
bufferevent_free(bev);
}
return ;
}
static void accept_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *addr, int len, void *arg)
{
struct event_base *ebase = arg;
struct bufferevent *recv_bev, *send_bev;
struct event *temp_event = NULL;
struct timeval tv={10, 0};
struct sockaddr_in *sock = (struct sockaddr_in *)addr;
wss_session_t *session;
if( !(session = malloc(sizeof(*session))) )
{
log_error("malloc for session failure:%s\n", strerror(errno));
close(fd);
return ;
}
memset(session, 0, sizeof(*session));
snprintf(session->client, sizeof(session->client), "[%d->%s:%d]", fd, inet_ntoa(sock->sin_addr), ntohs(sock->sin_port));
log_info("accpet new socket client %s\n", session->client);
bev_accpt = bufferevent_socket_new(ebase, fd, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS);
if( !bev_accpt )
{
log_error("create bufferevent for client for %s failed\n", session->client);
return;
}
session->bev = bev_accpt;
bufferevent_setcb(bev_accpt, read_cb, NULL, event_cb, session);
bufferevent_enable(bev_accpt, EV_READ|EV_WRITE);
temp_event = event_new(ebase, -1, EV_PERSIST, temp_cb, bev_accpt);
if (!temp_event)
{
log_error("failed to create temp event\n");
return;
}
event_add(temp_event, &tv);
return;
}
三、分析
libevent是通过I/O多路复用来实现高效的事件处理,事件循环(event loop)会不断调用底层的 I/O 多路复用函数( select
、poll
或 epoll
),等待事件的发生。
libevent本身确实是一个单线程事件驱动模型。但是也可能出现并发问题:
- 多线程环境:尽管 libevent 的核心是单线程的,但如果程序是多线程的,并且多个线程尝试访问和操作同一个
bufferevent
,就会出现并发问题。 - 事件回调重入:如果事件回调函数执行的时间较长,而在此期间另一个事件被触发并尝试访问同一个资源,可能会导致重入问题。
- 非线程安全代码:即使在单线程环境中,某些操作可能会触发不安全的并发访问,例如在回调中操作全局变量或共享资源。
四、解决方法
解决并发访问的问题最常用的方法是加锁,libevent 提供了一些机制来确保 bufferevent
在多线程环境下的安全性。例如,可以使用 bufferevent_lock
和 bufferevent_unlock
函数来显式地对 bufferevent
进行加锁和解锁操作。
我选择的方式是把接收的bufferevent
和发送的bufferevent
分开,将接收和发送操作独立进行,避免一个操作阻塞另一个操作,提高整体响应速度。