用std::function<>
实现
头文件在#include <functional>
通过typedef std::function<void(event_infor *)> EventCallback;
宏定义一个EventCallback
回调函数 <void(event_infor *)>
返回值为void ,传入参数为自定义的事件结构体,目前暂定为下:
struct event_infor {
std::string ip; //ip地址
u_int port; //端口
int fd; //文件描述符
EventCallback eventCallback; // 回调函数
int events; //事件的性质 比如EPOLLIN
//其实应该还要有其它的定义,日后有需求再说
};
把event_infor
的eventCallback
函数参数为event_infor
这样就可以在函数中获取这些信息,不管用没用到。
epoll_event.events
和event_infor.events
一样、epoll_event.data.ptr
指针指向event_infor
,注意在写代码中一定要用new分配内存来定义一个event_infor
,在定义epoll_event
时可以是局部变量因为在执行epoll_ctl()
后会把epoll_event
拷贝添加进内部的红黑树,会复制epoll_event.data.ptr
但不会复制所指向的event_infor
,所以用new开辟一块内存空间存放event_infor
,然后返回指针把epoll_event.data.ptr
指向它。这个坑我弄了一整天才发现,如果是局部变量的话在外面调用回调函数会出现段错误,或者结构体里面的变量默认初始化为0,这样的话当回调函数用这个里面的默认fd = 0
,结果导致很多错误,比如read()
一个fd = 0
而本来客户端的fd = 5
,那么程序会永远卡死在这里等待标准输入stdin(因为标准输入文件描述符为0)。图一在event_infor
的生命周期有效,当执行回调函数把*event_infor
传进去后,地址没变但内容全部为0,因为在这里它生命周期已经结束。
图1:
传统的绑定方法:
1.C语言的函数指针
2.std::bind()
3.lambda表达式
第一种就不深入探讨了,在C语言中只能用这个,而在c++中函数的绑定更为简单std::bind()
为传统做法,lambda是c++11后才出现的,用这两个区别《effective modern c++ 》中有详细的讨论,作者是建议用lambda的,但是用之前要完全了解lambda的方方面面避免错误。
lambda使用示例:
l_infor.eventCallback = [&](event_infor *infor){ acceptconn(infor);};
通过[&]来额外引入其它变量,最后在调用实际功能函数,实际功能函数参数可以随便你,lambda的作用就是两者的粘合剂,优点简洁。
黏包的在客户端发送频率低的情况下粘包不明显,下面是原来的服务器中epoll一个事件可读的回调函数recvdata(event_infor *infor)
,当调用这个函数然后recv()
函数读缓冲区的数据,指定大小为BUFF_MAX-1
,而BUFF_MAX
我设置为1024,一般单次接收数据小于这个值,所以基本上就是读取了所有缓冲区数据,接收过程:收到数据->epoll可读->调用回调函数->读取缓冲区所有数据->空缓冲区等待下一次接收。在处理客户端发来的聊天消息时还没有发生粘包现象,处理文件传输时一直粘包,问题分析是客户端高速循环调用send()
发送数据,也就是说服务器其实recv()
多条数据,比如说发送{"code":1}
和{"code":2}
服务器读一次可能是{"code":1}{"co
。
原服务器代码:
void Server::recvdata(event_infor *infor) {
int n = recv(infor->fd, infor->buff, BUFF_MAX - 1, 0);
if (n > 0) { //收到的数据
infor->buff[n] = '\0';
LOG(INFO) << infor->ip << " 发来一条消息: " << infor->buff;
_messageQueue->push(infor->buff, infor);
} else if (n == 0) {
LOG(INFO) << "fd: " << infor->fd << " 连接关闭";
close(infor->fd);
infor->status = false;
} else {
close(infor->fd);
infor->status = false;
LOG(INFO) << "fd: " << infor->fd << " 连接关闭";
}
bzero(infor->buff, BUFF_MAX - 1);
}
解决办法见下:
//结构体定义
struct MessageStruct{
int jsonLen;
char json[0]; //不能换成char *json
};
客户端代码:
string data = "{\"code\":0,\"data\":{\"passwd\":\"123456\",\"name\":\"daimiaopeng\"}}";
int len = sizeof(MessageStruct) + data.size();
MessageStruct *messageStruct = static_cast<struct MessageStruct *>(malloc(len));
messageStruct->jsonLen = data.size();
strncpy(messageStruct->json, data.c_str(), data.size());
//也可以用memcpy(),这里是string->char* 而不是char*->char*
for (int i = 0; i < 1000; i++) {
send(sockfd, messageStruct, len, 0);
}
free(messageStruct);
改进后的服务器代码:
void Server::recvdata(event_infor *infor) {
MessageStruct messageStruct;
//读取结构体
int n = recv(infor->fd, &messageStruct, sizeof(MessageStruct), 0);
if (n > 0) { //收到的数据
//获取结构体中的json长度字段
char *jsonData = static_cast<char *>(malloc(messageStruct.jsonLen));
//再读json数据
if (jsonData== nullptr)
return;
recv(infor->fd, jsonData, messageStruct.jsonLen, 0);
_messageQueue->push(string(jsonData, messageStruct.jsonLen), infor);
free(jsonData);
} else if (n == 0) {
LOG(INFO) << "fd: " << infor->fd << " 连接关闭";
close(infor->fd);
infor->status = false;
} else {
close(infor->fd);
infor->status = false;
LOG(INFO) << "fd: " << infor->fd << " 连接关闭";
}
}
总的来说是通过定义一个结构体然后二进制发送,读messageStruct
的大小数据,sizeof(messageStruct)
大小就是int
类型的大小,为4,实际上后面还跟了数据,char json[0]
不占用空间,而char *json
是占用8个字节的,所以不能用char *json
代替char json[0]
这里是个坑注意一下,转换完成读取jsonLen的值,再读jsonLen大小的数据这样就完成分包了。这里比较难理解,建议网上多看几篇通过结构体解决粘包的文章,结合本文的代码理解,注意在windows平台下的vs编译器会出现“非法的大小为零的数组”错误提示,因为不支持char json[0];写法。