消息传输格式定义
发送数据前,给数据附加两字节的长度:
4字节 | 2个字节 | 4个字节 | |
---|---|---|---|
FBEB | 事件ID | 数据长度N | 数据内容 |
- 包标识(FBEB): 包头部的特殊标识,用来标识包的开始
- 事件类型:事件ID, 固定两个字节表示
- 数据长度:数据包的大小, 固定长度4字节。
- 数据内容:数据内容, 长度为数据头定义的长度大小。
// 用于应用程序协议
#define MESSAGE_HEADER_LEN 10
#define MESSAGE_HEADER_ID "FBEB"
a)发送端:先发送包标识、时间类型和长度,再发送数据内容。
b)接收端:先解析本次数据包的时间ID、大小N,再读取N个字节,这N个字节就是一个完整的数据内容。
服务器端在接收到第一个数据后会先取出前四个字节判断是否是"FBEB" 。如果不是,则数据出错,断开该连接。如果是,则继续下面的处理,取出事件ID,保存事件ID之后根据事件ID做出具体的响应操作。取出数据长度,循化等待接收数据,直至接收到的数据长度等于数据长度。
封装
class NetworkInterface
{
public:
NetworkInterface();
~NetworkInterface();
bool start(int port);
void close();
static void listener_cb(struct evconnlistener* listener, evutil_socket_t fd,
struct sockaddr* sock, int socklen, void* arg);
static void handle_request(struct bufferevent* bev, void* arg); //读请求回调
static void handle_response(struct bufferevent* bev, void* arg); //发送响应回调
static void handle_error(struct bufferevent* bev, short event, void* arg); //处理错误回调
void network_event_dispatch();
// 发送线程池的响应结果
void send_response_message(ConnectSession* cs);
private:
struct evconnlistener* listener_;
struct event_base* base_;
};
监听事件
void NetworkInterface::listener_cb(struct evconnlistener* listener, evutil_socket_t fd,
struct sockaddr* sock, int socklen, void* arg) {
struct event_base* base = (struct event_base*)arg; // libevent的事务处理框架,负责事件注册、删除等
LOG_DEBUG("accept a client %d\n", fd);
// 为这个客户端分配一个bufferevent
// if fd = -1 则表示客户端需要用bufferevent_socket_connect连接服务器
struct bufferevent* bev = bufferevent_socket_new(base, fd,
BEV_OPT_CLOSE_ON_FREE);
ConnectSession* cs = session_init(fd, bev);
cs->session_stat = SESSION_STATUS::SS_REQUEST; // 可以开始接收数据
cs->req_stat = MESSAGE_STATUS::MS_READ_HEADER; // 数据头部
strcpy(cs->remote_ip, inet_ntoa(((sockaddr_in*)sock)->sin_addr)); // 复制IP到当前会话,‘\0’是停止拷贝的终止条件,同时也会将 '\0' 也复制到目标空间
LOG_DEBUG("remote ip : %s\n", cs->remote_ip);
// 设定bufferevent接收数据回调函数
// readcb、writecb和eventcb函数将分别在已经读取足够的数据 、已经写入足够的数据 ,或者发生错误时被调用
bufferevent_setcb(bev, handle_request, handle_response, handle_error, cs);
// 默认情况下,新创建的 bufferevent 的写入是启用的,但是读取没有启用
// 其实是调用了event_add 将相应读写事件加入事件监听队列poll
bufferevent_enable(bev, EV_READ | EV_PERSIST); //将 bev 时间添加到epoll EV_PERSIST:事件是“持久的”
// //设置读、写事件的超时时间
bufferevent_settimeout(bev, 10, 10);//超时值应设置在配置文件
}
读请求
/*****************************************************
* 4字节 2个字节 4个字节
* 请求格式:FBEB 事件ID 数据长度N 数据内容
* 1.包标识: 包头部的特殊标识, 用来标识包的开始
* 2.事件类型:事件ID, 固定两个字节表示
* 3.数据长度:数据包的大小, 固定长度4字节。
* 4.数据内容:数据内容, 长度为数据头定义的长度大小。
*
* ****************************************************/
void NetworkInterface::handle_request(struct bufferevent* bev, void* arg)
{
ConnectSession* cs = (ConnectSession*)arg;
if (cs->session_stat != SESSION_STATUS::SS_REQUEST) // 是否可以接收
{
LOG_WARN("NetworkInterface::handle_request - wrong session state[%d].", cs->session_stat);
return;
}
if (cs->req_stat == MESSAGE_STATUS::MS_READ_HEADER)
{
// 从底层的套接字缓冲区中读取数据,并将其复制到应用层的缓冲区中
i32 len = bufferevent_read(bev, cs->header + cs->read_header_len, MESSAGE_HEADER_LEN - cs->read_header_len);
cs->read_header_len += len;
cs->header[cs->read_header_len] = '\0';
LOG_DEBUG("recv from client<<<< %s\n", cs->header);
if (cs->read_header_len == MESSAGE_HEADER_LEN)
{
if (strncmp(cs->header, MESSAGE_HEADER_ID, strlen(MESSAGE_HEADER_ID)) == 0) // 判断是否等于FBEB
{
cs->eid = *((u16*)(cs->header + 4)); // 读取事件 ID
cs->message_len = *((i32*)(cs->header + 6)); // 读取消息长度
LOG_DEBUG("NetworkInterface::handle_request - read %d bytes in header, message len: %d\n", cs->read_header_len, cs->message_len);
if (cs->message_len<1 || cs->message_len > MAX_MESSAGE_LEN) {
LOG_ERROR("NetworkInterface::handle_request wrong message, len: %u\n", cs->message_len);
bufferevent_free(bev);
session_free(cs);
return;
}
cs->read_buf = new char[cs->message_len];
cs->req_stat = MESSAGE_STATUS::MS_READ_MESSAGE;
cs->read_message_len = 0;
}
else
{
LOG_ERROR("NetworkInterface::handle_request - Invalid request from %s\n", cs->remote_ip);
//直接关闭请求,不给予任何响应,防止客户端恶意试探
bufferevent_free(bev);
session_free(cs);
return;
}
}
}
if (cs->req_stat == MESSAGE_STATUS::MS_READ_MESSAGE && evbuffer_get_length(bufferevent_get_input(bev)) > 0) //取出输入缓冲区,开始读数据
{
i32 len = bufferevent_read(bev, cs->read_buf + cs->read_message_len, cs->message_len - cs->read_message_len);
cs->read_message_len += len;
LOG_DEBUG("NetworkInterface::handle_request - bufferevent_read: %d bytes, message len: %d read len: %d\n", len, cs->message_len, cs->read_message_len);
if (cs->message_len == cs->read_message_len) //接收数据完成
{
cs->session_stat = SESSION_STATUS::SS_RESPONSE;
iEvent* ev = DispatchMsgService::getInstance()->parseEvent(cs->read_buf, cs->read_message_len, cs->eid); // 解析事件
delete[] cs->read_buf;
cs->read_buf = nullptr;
cs->read_message_len = 0;
if (ev)
{
ev->set_args(cs);
DispatchMsgService::getInstance()->enqueue(ev); // 投递到队列中
}
else
{
LOG_ERROR("NetworkInterface::handle_request - ev is null. remote ip: %s, eid: %d\n", cs->remote_ip, cs->eid);
//直接关闭请求,不给予任何响应,防止客户端恶意试探
bufferevent_free(bev);
session_free(cs);
return;
}
}
}
}
处理错误
//超时、连接关闭、读写出错等异常情况指定的回调函数
void NetworkInterface::handle_error(struct bufferevent* bev, short event, void* arg)
{
ConnectSession* cs = (ConnectSession*)arg;
LOG_DEBUG("NetworkInterface::handle_error ...\n");
//连接关闭
if (event & BEV_EVENT_EOF)
{
LOG_DEBUG("connection closed\n");
}
else if ((event & BEV_EVENT_TIMEOUT) && (event & BEV_EVENT_READING))
{
LOG_WARN("NetworkInterface::reading timeout ...\n");
}
else if ((event & BEV_EVENT_TIMEOUT) && (event & BEV_EVENT_WRITING))
{
LOG_WARN("NetworkInterface::writting timeout ...\n");
}
else if (event & BEV_EVENT_ERROR)
{
LOG_ERROR("NetworkInterface:: some other error ...\n");
}
//这将自动close套接字和free读写缓冲区
bufferevent_free(bev);
session_free(cs);
}
轮询
void NetworkInterface::network_event_dispatch()
{
// 执行循环的时候,函数重复地检查是否有任何已经注册的事件被触发(比如说,读事件的文件描述符已经就绪,可以读取了;
// 或者超时事件的超时时间即将到达)。如果有事件被触发,函数标记被触发的事件为“激活的”,并且执行这些事件。
// EVLOOP_NONBLOCK,循环不会等待事件被触发:循环将仅仅检测是否有事件已经就绪,可以立即触发,如果有,则执行事件的回调。
event_base_loop(base_, EVLOOP_NONBLOCK);
//处理响应事件,回复响应消息 - 未完待续
DispatchMsgService::getInstance()->handleAllResponseEvent(this);
}
发送响应
void NetworkInterface::send_response_message(ConnectSession* cs)
{
if (cs->response == nullptr)
{
bufferevent_free(cs->bev);
if (cs->request)
{
delete cs->request;
}
session_free(cs);
}
else
{
// 将内存中从 data 处开始的 size 字节数据添加到输出缓冲区的末尾 。
bufferevent_write(cs->bev, cs->write_buf, cs->message_len + MESSAGE_HEADER_LEN);
session_reset(cs);
}
}
相关
在内核中有读缓冲区和写缓冲区,减少用户态和内核态的切换。
- 用户态读缓冲区的存在是为了处理粘包的问题,因为网络协议栈是不知道用户界定数据包的格式,没法确定一个完整的数据包。
- 用户态写缓冲区的存在是因为用户根本不清楚内核写缓存区的状态,需要把没有写出去的数据缓存起来等待下次写事件时把数据写出去。
buffer的设计有三种类型:
(1)固定数组,固定长度。 限定了处理数据包的能力,没有动态伸缩的能力;需要频繁挪动数据。
(2)ring buffer。可伸缩性差。
(3)chain buffer。 解决可伸缩性差的问题,避免频繁挪动数据;同时也引进了新的问题,一个数据可能在多个buffer中都有,即数据分割,这会导致多次系统调用,从而引起中断上下文的切换。解决办法是使用readv()将内核中连续的buffer读到用户态不连续的buffer中,writev()把用户态不连续的buffer写到内核连续的buffer中;从而减少系统调用。
libevent是一个事件通知库;封装了reactor。
libevent API 提供了一种机制,用于在文件描述符上发生特定事件或达到超时后执行回调函数。此外,libevent还支持由于信号或常规超时而导致的回调。
libevent 旨在替换在事件驱动的网络服务器中找到的事件循环。应用程序只需要调用event_dispatch(),然后动态添加或删除事件,而无需更改事件循环。
IO函数使用与网络原理
(1)有了libevent可以不使用IO函数。因为如果使用IO函数,既需要知道这些IO函数里面的系统调用返回值的含义。
(2)有了libevent就可以不清楚数据拷贝原理。
(3)有了libevent就可以不清楚网络原理以及网络编程流程。
(4)有了libevent只需要知道事件处理,IO操作完全交由libevent处理。
https://blog.csdn.net/u010710458/article/details/80067885
https://aceld.gitbooks.io/libevent/content/chapter1.html
https://blog.csdn.net/Long_xu/article/details/127142798