调用libevent库接口实现TCP通信以及如何解决TCP分包粘包问题。

  • Libevent介绍:

        libevent是一个事件触发的网络库,适用于windows、linux、bsd等多种平台,内部使用select、epoll、kqueue等系统调用管理事件机制。

        在介绍libevent常用接口时,我会以socket为例子。

  • 创建套接字

     libevent接口底层也是封装socket的。因此在调用libevent接口写一个服务器或者客户端时第一步也是需要创建套接字

    //创建一个socket套接字,用于监听客户端的连接
	evutil_socket_t listener;
	listener = socket(AF_INET, SOCK_STREAM, 0);
	if(listener == -1)//出错处理
	{
		printf("failed create socket");
		return -1;
	}

  socket(AF_INET, SOCK_STREAM, 0):AF_INET是指定TCP或者UDP协议;SOCK_STREAM是指定流。因为UDP是报文形式传送数据,因此这里创建的套接字是TCP流协议。

  • 创建方法

        如果没有调用libevent库,一个socket套接字服务还需要调用bind、listen、accept等接口来监听客户端连接,但若掉用了libevent库就不需要调用这些接口,一般情况下直接new一个event_base就可以满足大部分需求了,如果需要配置参数的,可以参见libevent官网。

创建方法:struct event_base *event_base_new(void);

	struct event_base* pBase = event_base_new();
	if (pBase == NULL)
	{
		printf("failed event_base_new");
		return;
	}

        在没有调用libevent等封装好的库情况下,服务器成功连接一个客户端会创建一个线程或者进程与成功连接的客户端通信或者设置IO多路复用模型。在libevent库接口中,其提供了select、poll、epoll等IO复用,linux系统下一般使用epoll模型

  • 获取IO复用模型接口

        const char *event_base_get_method(const event_base *base)

	//获取IO多路复用的模型,linux一般为epoll
	const char *pX = event_base_get_method(pBase); 
	if (pX != NULL)
	{
		printf("METHOD:%s", pX);
	}
  • 设置事件触发类型   

        获取IO复用模型之后,就要设置事件触发类型了,服务器一般都不会主动给客户端发送消息的,所以不用设置写触发事件。

struct event* ev_listen = event_new(pBase, listener, EV_READ | EV_PERSIST, accept_cb, pBase);

接口:struct event *event_new(struct event_base *, evutil_socket_t, short, event_callback_fn, void *);

参数1:监听的事件

参数2:监听的套接字

参数3:监听的时间类型

参数4:事件发生事调用的回调函数。

参数5:传递给回调函数的参数,一般都是将struct event_base* pBase传递过去

  • 事件循环和事件销毁:

        服务器需要不断地循环监听注册上来的事件,因此时间循环是很重要的

                int event_base_dispatch(struct event_base *base);

        事件销毁

        void event_base_free(struct event_base *base);

       以下代码是循环监听注册上来的事件代码。可以参照参照一下

void Server::SetupRouter()
{
	const char * pEventVersion = event_get_version();
	if (pEventVersion == NULL)
	{
		return;
	}
	printf("The libevent version is %s", pEventVersion);

	evutil_socket_t listener = tcp_server_init();
	if (listener == -1)
	{
		printf("failed tcp_server_init");
		return;
	}

	struct event_base* pBase = event_base_new();
	if (pBase == NULL)
	{
		printf("failed event_base_new");
		return;
	}

	const char *pX = event_base_get_method(pBase); 
	if (pX != NULL)
	{
		printf("METHOD:%s", pX);
	}

	struct event* ev_listen = event_new(pBase, listener, EV_READ | EV_PERSIST, accept_cb, pBase);
	
	event_add(ev_listen, NULL);
	event_base_dispatch(pBase);
	event_base_free(pBase);
}
  •  与客户端连接(也就是进行TCP三次握手)

        当循环监听到有新事件注册时,就会调用回调函数accept,在socket套接字服务器中需要调用accept函数与客户端连接。libevent提供的接口也一样

	evutil_socket_t iClient_socketfd;//用来保存客户端的套接字
	struct sockaddr_in stClient;//保存客户端的结构体
#ifdef _MSC_VER
	int iClientLen = sizeof(stClient);
#else
	socklen_t iClientLen = sizeof(stClient);
#endif

	iClient_socketfd = ::accept(fd, (struct sockaddr*)&stClient, &iClientLen);
	if (iClient_socketfd < 0)
	{
		printf("accpet error sockfd:%d", iClient_socketfd);
		return;
	}

accept( SOCKET s, (*addrlen) struct sockaddr FAR * addr, FAR * addrlen );

这个接口第二个和第三个参数都是值-传参,第三个参数为什么要值-传参呢?我的理解是有两个原因:1、是当内核要往第二个参数写数据时,它可以起到提示内核第二个参数的最大长度,不要写超过了。2、是当内核成功写数据到第二参数时,它会修改第三个参数,写了多少数据到第二个参数就改成什么值。

一般成功连接一个客户端之后都要设置为非阻塞模式

	evutil_make_socket_nonblocking(iClient_socketfd);
	dzlog_info("accept a client [%d]", iClient_socketfd);
  • 处理事件

        成功连接客户端之后,要设置监听客户端套接字描述符的读写事件,

//创建一个evbuffer,用来缓冲客户端传递过来的数据  
	struct evbuffer *buf = evbuffer_new();
	//创建一个bufferevent  
	struct bufferevent *pBev = bufferevent_socket_new(pBase, iClient_socketfd, BEV_OPT_CLOSE_ON_FREE);
	bufferevent_setcb(pBev, socket_read_cb, NULL, socket_error_cb, buf);
	//设置类型  需要监听的事件类型添加进来。
	bufferevent_enable(pBev, EV_READ | EV_WRITE | EV_PERSIST);
	//设置水位  
	bufferevent_setwatermark(pBev, EV_READ, 0, 0);

     设置水位接口:void bufferevent_setwatermark(struct bufferevent *bufev, short events,
    size_t lowmark, size_t highmark);水位可以理解为一个水位容器。

设置水位接口lowmark参数很重要,只有水位超过了lowmark才会调用回调函数。因此这里一般设置为0.

下面这部分代码是与连接客户端和客户端成功连接之后的一些细节处理

void accept_cb(evutil_socket_t fd, short events, void* arg)
{
	evutil_socket_t iClient_socketfd;
	struct sockaddr_in stClient;
#ifdef _MSC_VER
	int iClientLen = sizeof(stClient);
#else
	socklen_t iClientLen = sizeof(stClient);
#endif

	iClient_socketfd = ::accept(fd, (struct sockaddr*)&stClient, &iClientLen);
	if (iClient_socketfd < 0)
	{
		printf("accpet error sockfd:%d", iClient_socketfd);
		return;
	}

	evutil_make_socket_nonblocking(iClient_socketfd);
	printf("accept a client [%d]", iClient_socketfd);
	
	struct event_base* pBase = (event_base*)arg;
	
	struct evbuffer *buf = evbuffer_new();
	struct bufferevent *pBev = bufferevent_socket_new(pBase, iClient_socketfd, BEV_OPT_CLOSE_ON_FREE);
	bufferevent_setcb(pBev, socket_read_cb, NULL, socket_error_cb, buf);
	bufferevent_enable(pBev, EV_READ | EV_WRITE | EV_PERSIST);
	bufferevent_setwatermark(pBev, EV_READ, 0, 0);
}

     下面是如何处理通过TCP协议接收客户端发送的包了以及分包粘包问题。

        假设TCP接收服务器的私有报文如下(一般每个TCP通信都有自己定义的私有协议,可以根据私有协议的特性来处理粘包问题):

4fffe815f7ff4893bddaea10d464a139.png

因此我在读缓冲区之前先判断缓冲区数据长度是否长多16个字节,因为在数据内容前面刚好因为包头+数据内容长度+版本号+数据内容格式占了是16个字节长度

获取缓冲区大小代码

static int get_buff_length(struct bufferevent *pBev)
{
	if (pBev == NULL)
	{
		printf("pBev is NULL");
		return -1;
	}
	struct evbuffer * input  = bufferevent_get_input(pBev);
	return evbuffer_get_length(input);
}

struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);和size_t evbuffer_get_length(const struct evbuffer *buf);接口时libevent库提供的。就不做过多解释了,这里目的是怎么处理粘包问题。

        如果缓冲区里的数据超过16个字节就去读取缓冲区内容,先把包头前面16个字节拷贝出来(从缓冲区里拷贝数据,缓冲区的偏移量不会移动,读时缓冲区偏移量才移动,这个我测试过了。)

void socket_read_cb(struct bufferevent *pBev, void *arg)
{
	int nDataLen = get_buff_length(pBev);
	while (nDataLen >= (TCP_HEAD_LENGTH_PACKSIZE*4))
	{
		int iRet = read_packet(pBev, arg);
		if (iRet == -1 || iRet == -2)
		{
			break;
		}
	}
}

拷贝缓冲区数据

static int BuffCopy(struct bufferevent *pBev, char *pBuff, uint32_t iNeedLen)
{
	if (NULL == pBev)
	{
		printf("pBev is NULL");
		return -1;
	}

	struct evbuffer * input = bufferevent_get_input(pBev);
	if (input == NULL)
	{
		return -1;
	}
	uint32_t nReadLen = 0;
	while ((evbuffer_get_length(input) >= iNeedLen) && (nReadLen < iNeedLen))
	{
		int32_t nRet = evbuffer_copyout(input, pBuff + nReadLen, iNeedLen - nReadLen);
		if (nRet == -1)
		{
			return -1;
		}
		nReadLen += nRet;
	}
	return nReadLen;
}

将拷贝出来的数据,进行大小端转换

static void ByteOrder(stHeadData* pHeadData)
{
	if (Config::get_instance()->isBigEndian == BYTE_ORDER_BIG_ENDIAN) 
	{
		pHeadData->iStartByte1 = ntohl(pHeadData->iStartByte1);
		pHeadData->iDatalength = ntohl(pHeadData->iDatalength);
		pHeadData->iVersion = ntohl(pHeadData->iVersion);
		pHeadData->iCommand = ntohl(pHeadData->iCommand);
	}
}

然后获取数据内容长度,只有缓冲区内容长度超过了数据内容长度时才去读缓冲区内容。而且读取缓冲区长度为:包头+数据内容长度+版本号+数据内容+包尾。这样子就可以读出一个完整的包了,如果还需要严格一点的话还可以在读出来的数据中拿出前面四个字节判断是否是包头和后面四个字节数据判断是否包尾

	//读取缓冲区数据
	int iTotalLen = pHeadData->iDatalength + (TCP_HEAD_LENGTH_PACKSIZE * 3);
	int iBuffLen = GetEvtbufLetfLen(pBev);
	if (iTotalLen > iBuffLen)
	{
		printf("iTotalLen=%d less than iBuffLen=%d is not a complete body", iTotalLen, iBuffLen);
		delete pBuff;
		return -2;
	}

	PacketAckMessage packet;
	if (packet.Capacity() < iTotalLen)
	{
		packet.Resize(iTotalLen);
	}
	iRet = bufferevent_read(pBev, (char*)packet.Contents(), iTotalLen);

完整代码如下:

server.cpp

static int get_buff_length(struct bufferevent *pBev)
{
	if (pBev == NULL)
	{
		printf("pBev is NULL");
		return -1;
	}
	struct evbuffer * input  = bufferevent_get_input(pBev);
	return evbuffer_get_length(input);
}

static int BuffCopy(struct bufferevent *pBev, char *pBuff, uint32_t iNeedLen)
{
	if (NULL == pBev)
	{
		printf("pBev is NULL");
		return -1;
	}

	struct evbuffer * input = bufferevent_get_input(pBev);
	if (input == NULL)
	{
		return -1;
	}
	uint32_t nReadLen = 0;
	while ((evbuffer_get_length(input) >= iNeedLen) && (nReadLen < iNeedLen))
	{
		int32_t nRet = evbuffer_copyout(input, pBuff + nReadLen, iNeedLen - nReadLen);
		if (nRet == -1)
		{
			return -1;
		}
		nReadLen += nRet;
	}
	return nReadLen;
}

static void ByteOrder(stHeadData* pHeadData)
{
	if (Config::get_instance()->isBigEndian == BYTE_ORDER_BIG_ENDIAN) 
	{
		pHeadData->iStartByte1 = ntohl(pHeadData->iStartByte1);
		pHeadData->iDatalength = ntohl(pHeadData->iDatalength);
		pHeadData->iVersion = ntohl(pHeadData->iVersion);
		pHeadData->iCommand = ntohl(pHeadData->iCommand);
	}
}

uint32_t GetEvtbufLetfLen(struct bufferevent *pBev)
{
	uint32_t nLeft = 0;
	if (pBev)
	{
		struct evbuffer* input = bufferevent_get_input(pBev);
		if (input)
		{
			nLeft = evbuffer_get_length(input);
		}
	}
	return nLeft;
}

static int read_packet(struct bufferevent *pBev, void *arg)
{
	if (pBev == NULL)
	{
		dzlog_error("pBev is NULL");
		return -1;
	}

	//拷贝包头,检验包头是否正确
	char *pBuff = new char[sizeof(stHeadData)];
	int iRet = BuffCopy(pBev, pBuff, sizeof(stHeadData));
	if (iRet < 0)
	{
		dzlog_error("copy data error");
		delete pBuff;
		return -1;
	}
	stHeadData* pHeadData = (stHeadData*)pBuff;
	ByteOrder(pHeadData);

	//读取缓冲区数据
	int iTotalLen = pHeadData->iDatalength + (TCP_HEAD_LENGTH_PACKSIZE * 3);
	int iBuffLen = GetEvtbufLetfLen(pBev);
	if (iTotalLen > iBuffLen)
	{
		printf("iTotalLen=%d less than iBuffLen=%d is not a complete body", iTotalLen, iBuffLen);
		delete pBuff;
		return -2;
	}

	char *pTotalBuff = new char[iTotalLen];//这一步建议是搞一个对象的变量来接从缓冲去读取的数据
	iRet = bufferevent_read(pBev, pBuff, iTotalLen);

	/*
        这部分是处理读取到数据内容的代码了
    */

	delete pBuff;
    delete pTotalBuff; 
	return iRet;

error:
	std::string strErrMsg;
	ssErrMsg >> strErrMsg;
	dzlog_error("%s", strErrMsg.c_str());
	bufferevent_write(pBev, strErrMsg.c_str(), strErrMsg.size());

	delete pBuff;
    delete pTotalBuff; 
	return -99;
}

void socket_read_cb(struct bufferevent *pBev, void *arg)
{
	int nDataLen = get_buff_length(pBev);
	while (nDataLen >= (TCP_HEAD_LENGTH_PACKSIZE*4))
	{
		int iRet = read_packet(pBev, arg);
		if (iRet == -1 || iRet == -2)
		{
			break;
		}
	}
}

void socket_error_cb(struct bufferevent *bev, short event, void *arg)
{

}

void accept_cb(evutil_socket_t fd, short events, void* arg)
{
	evutil_socket_t iClient_socketfd;
	struct sockaddr_in stClient;
#ifdef _MSC_VER
	int iClientLen = sizeof(stClient);
#else
	socklen_t iClientLen = sizeof(stClient);
#endif

	iClient_socketfd = ::accept(fd, (struct sockaddr*)&stClient, &iClientLen);
	if (iClient_socketfd < 0)
	{
		printf("accpet error sockfd:%d", iClient_socketfd);
		return;
	}

	evutil_make_socket_nonblocking(iClient_socketfd);
	printf("accept a client [%d]", iClient_socketfd);
	
	struct event_base* pBase = (event_base*)arg;

	//设置读取方法和error时候的方法,将buf缓冲区当参数传递 
	//创建一个evbuffer,用来缓冲客户端传递过来的数据  
	struct evbuffer *buf = evbuffer_new();
	//创建一个bufferevent  
	struct bufferevent *pBev = bufferevent_socket_new(pBase, iClient_socketfd, BEV_OPT_CLOSE_ON_FREE);
	bufferevent_setcb(pBev, socket_read_cb, NULL, socket_error_cb, buf);
	//设置类型  需要监听的事件类型添加进来。
	bufferevent_enable(pBev, EV_READ | EV_WRITE | EV_PERSIST);
	//设置水位  
	bufferevent_setwatermark(pBev, EV_READ, 0, 0);
}

void Server::SetupRouter()
{
	const char * pEventVersion = event_get_version();
	if (pEventVersion == NULL)
	{
		return;
	}
	printf("The libevent version is %s", pEventVersion);

	evutil_socket_t listener = tcp_server_init();
	if (listener == -1)
	{
		dzlog_error("failed tcp_server_init");
		return;
	}

	struct event_base* pBase = event_base_new();
	if (pBase == NULL)
	{
		printf("failed event_base_new");
		return;
	}
	//获取IO多路复用的模型,linux一般为epoll
	const char *pX = event_base_get_method(pBase); 
	if (pX != NULL)
	{
		printf("METHOD:%s", pX);
	}

	struct event* ev_listen = event_new(pBase, listener, EV_READ | EV_PERSIST, accept_cb, pBase);
	
	event_add(ev_listen, NULL);
	event_base_dispatch(pBase);
	event_base_free(pBase);
}

int Server::tcp_server_init()
{
	int iErrnoSave;
	evutil_socket_t listener;

	listener = socket(AF_INET, SOCK_STREAM, 0);
	if(listener == -1)
	{
		printf("failed create socket");
		return -1;
	}

	//设置端口重用
	evutil_make_listen_socket_reuseable(listener);
	//将套接字设置为非阻塞状态
	evutil_make_socket_nonblocking(listener);

	struct sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(INADDR_ANY);
	sin.sin_port = htons(iPort);

	if (::bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0)
	{
		printf("failed bind");
		goto error;
	}

	if (::listen(listener, iMaxListenNum) < 0)
	{
		printf("failed listen");
		goto error;
	}

	return listener;

error:
	iErrnoSave = errno;
	evutil_closesocket(listener);
	errno = iErrnoSave;

	return -1;
}

server.h

#pragma pack(1)
typedef struct stHeadData
{
	int32_t iStartByte1;//包头
	int32_t iDatalength;//数据长度
	int32_t iVersion;
	int32_t iCommand;
}stHeadData;
#pragma pack()

class Server {

public:
	Server(std::string strIP, int iPort, int iMaxListenNum)
	{
		this->strIP = strIP;
		this->iPort = iPort;
		this->iMaxListenNum = iMaxListenNum;
	};

	void SetupRouter();

private:
	int tcp_server_init();

public:
	std::string strIP;
	int iPort;
	int iMaxListenNum;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值