libhv Tcp小项目实战
一、概述
1、包头
2、心跳
3、任务队列
二、包头
2字节 | 2字节 | 4字节 | 4字节 | 2字节 | 1字节 | 1字节 |
---|---|---|---|---|---|---|
起始标志 | 版本号(N) | Command(命令ID) | 消息体长度(N) | 预留 | Crc校验位 | 结束标志 |
- 起始标志:0xef.
- 版本号:0x0001。
- Command:命令号,用于表示一个特定的命令。请求和响应成对出现。
- 消息体长度:表示protobuf消息体的长度,该长度不能超过100*1024个字节。
- 预留:留作他用,目前全部填0,
- Crc校验位:将前面所有字节内容相加,如果内容之和超过1个字节,直接翻转即可。
- 结束标志:“#”代表结束标志。
注:在交互过程中,包头各字段在网络中以网络字节顺序传输。
// 自行扩展
enum SOCKET_MSG_TYPE
{
SOCKET_MSG_TYPE_NULLPTR = 0,
SOCKET_MSG_TYPE_HB = 1,
};
// 基础结构体、类
#pragma pack(1) //下面的结构体1字节对其
// 包头结构体
struct HeadData
{
HeadData() :
m_nBeginMark(0xef),
m_cVersion(1),
m_nCmdId(SOCKET_MSG_TYPE_NULLPTR),
m_nLen(),
m_arrResover(),
m_cCrc(),
m_cFinishMark(0x23)
{}
unsigned short m_nBeginMark; //起始标志
unsigned short m_cVersion; //目前填0
unsigned int m_nCmdId; // 消息类型 见枚举 SOCKET_MSG_TYPE
unsigned int m_nLen; //负载数据长度,不包含头
unsigned char m_arrResover[2]; //预留位
unsigned char m_cCrc; //私有数据头的校验字节
unsigned char m_cFinishMark; //结束标志
};
#pragma pack()
三、心跳包
socket,以双方发送心跳确认对端是否关闭。
该项目心跳包只发送当前时间,其他业务根据自己需求扩展。
// 心跳结构体
struct SocketHB
{
SocketHB() : m_strTimetemp() {}
std::string m_strTimetemp; // 时间戳
};
四、任务队列
任务队列是在队列的基础上加锁,实现线程安全。
class SocketQueueMessage
{
public:
SocketQueueMessage();
~SocketQueueMessage();
/**
* @brief 插入队列
* @param p_stMsg InviewSocketDataMessage数据
*/
void addMsg(InviewSocketDataMessage& p_stMsg);
/**
* @brief 获取队列元素
* @param p_stMsg InviewSocketDataMessage数据
* @return true :成功 false :失败
*/
bool getMsg(InviewSocketDataMessage& p_stMsg);
/**
* @brief 清空队列元素
*/
void clearMsg();
private:
std::list<SocketDataMessage> m_listDataMessage; // 消息队列
std::mutex m_mutexDataThread; // 消息线程锁
};
五、libhv 官方tcp简单小例子
项目地址:https://github.com/ithewei/libhv.git
码云镜像:https://gitee.com/libhv/libhv.git
#include "TcpServer.h"
using namespace hv;
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s port\n", argv[0]);
return -10;
}
int port = atoi(argv[1]);
TcpServer srv;
int listenfd = srv.createsocket(port);
if (listenfd < 0) {
return -20;
}
printf("server listen on port %d, listenfd=%d ...\n", port, listenfd);
srv.onConnection = [](const SocketChannelPtr& channel) {
std::string peeraddr = channel->peeraddr();
if (channel->isConnected()) {
printf("%s connected! connfd=%d\n", peeraddr.c_str(), channel->fd());
} else {
printf("%s disconnected! connfd=%d\n", peeraddr.c_str(), channel->fd());
}
};
srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
// echo
printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
channel->write(buf);
};
srv.onWriteComplete = [](const SocketChannelPtr& channel, Buffer* buf) {
printf("> %.*s\n", (int)buf->size(), (char*)buf->data());
};
srv.setThreadNum(4);
srv.start();
while (1) sleep(1);
return 0;
}
六、实战小项目
项目以上面tcp框架小例子为主
1、tcpServer
- SocketManager.h
#pragma once
#include <iostream>
#include <list>
#include <sstream>
#include <iomanip>
#include "hv/TcpServer.h" // tcp
using namespace std;
using namespace hv;
#define TEST_TLS 0
// 自行扩展
enum SOCKET_MSG_TYPE
{
SOCKET_MSG_TYPE_NULLPTR = 0,
SOCKET_MSG_TYPE_HB = 1,
};
// 基础结构体、类
#pragma pack(1) //下面的结构体1字节对其
// 包头结构体
struct HeadData
{
HeadData() :
m_nBeginMark(0xef),
m_cVersion(1),
m_nCmdId(SOCKET_MSG_TYPE_NULLPTR),
m_nLen(),
m_arrResover(),
m_cCrc(),
m_cFinishMark(0x23)
{}
unsigned short m_nBeginMark; //起始标志
unsigned short m_cVersion; //目前填0
unsigned int m_nCmdId; //消息类型 见枚举 SOCKET_MSG_TYPE
unsigned int m_nLen; //负载数据长度,不包含头
unsigned char m_arrResover[2]; //预留位
unsigned char m_cCrc; //私有数据头的校验字节
unsigned char m_cFinishMark; //结束标志
};
#pragma pack()
// 消息队列结构体
struct SocketDataMessage
{
SocketDataMessage() : m_stIo(nullptr), m_strSendData() {}
hio_t* m_stIo; // socket id
std::string m_strSendData; // 收到的数据
};
// 心跳结构体
struct SocketHB
{
SocketHB() : m_strTimetemp() {}
std::string m_strTimetemp; // 时间戳
};
class SocketQueueMessage
{
public:
SocketQueueMessage();
~SocketQueueMessage();
/**
* @brief 插入队列
* @param p_stMsg SocketDataMessage数据
*/
void addMsg(SocketDataMessage& p_stMsg);
/**
* @brief 获取队列元素
* @param p_stMsg SocketDataMessage数据
* @return true :成功 false :失败
*/
bool getMsg(SocketDataMessage& p_stMsg);
/**
* @brief 清空队列元素
*/
void clearMsg();
private:
std::list<SocketDataMessage> m_listDataMessage; // 消息队列
std::mutex m_mutexDataThread; // 消息线程锁
};
class SocketManager
{
public:
SocketManager();
~SocketManager();
/**
* @brief 初始化
*/
bool Init();
/**
* @brief tcp初始化
*/
bool TcpInit();
/**
* @brief 运行
*/
void Start();
private:
/**
* @brief 解包头
* @param p_pszBuf 传入参,接收数据字符串
* @param p_nBufLen 传入参,接收数据字符串长度
* @param p_stResData 传出参,包头结构体
* @return 0 :成功 -1:失败
*/
int PraseHeadMessage(char* p_pszBuf, unsigned int p_nBufLen, HeadData& p_stResData);
/**
* @brief 组包头
* @param p_strResMessage 传入参,发送数据字符串
* @param p_strRes 传出参,组包结果返回
* @param p_stResData 传出参,包头结构体
* @return 0 :成功 -1:失败
*/
int TobufHeadMessage(const std::string p_strResMessage, std::string& p_strRes, HeadData& p_stResData);
/**
* @brief tcp工作线程
*/
void tcpWork();
/**
* @brief tcp发送函数
* @param p_ioIo libhv IO
* @param p_strSend 发送字符串
*/
void SendMsg(hio_t* p_ioIo, std::string p_strSend);
/**
* @brief 获取当前时间字符串
* @return 当前时间字符串
*/
std::string getCurrentTime();
private:
/**
* @brief 组装心跳包
* @param p_strRes 传出参,组装好的心跳数据
*/
void testHb(std::string& p_strRes);
private:
std::thread* m_threadTcpWork; // tcp工作线程
std::mutex m_mutexTcpWork; // tcp工作线程锁
bool m_bTcpWorkFlag; // tcp工作线程标识
TcpServer* m_tcpServer; // tcp连接管理
SocketQueueMessage m_tcpQueue; // tcp消息队列
};
- SocketManager.cpp
#include "SocketManager.h"
/******************************************SocketQueueMessage*************************************/
SocketQueueMessage::SocketQueueMessage()
{
std::list<SocketDataMessage>().swap(m_listDataMessage);
}
SocketQueueMessage::~SocketQueueMessage()
{
std::list<SocketDataMessage>().swap(m_listDataMessage);
}
void SocketQueueMessage::addMsg(SocketDataMessage& p_stMsg)
{
std::lock_guard<std::mutex> lock(m_mutexDataThread);
m_listDataMessage.push_back(p_stMsg);
}
bool SocketQueueMessage::getMsg(SocketDataMessage& p_stMsg)
{
if (m_listDataMessage.empty())
{
return false;
}
std::lock_guard<std::mutex> lock(m_mutexDataThread);
p_stMsg = m_listDataMessage.front();
m_listDataMessage.pop_front();
return true;
}
void SocketQueueMessage::clearMsg()
{
std::lock_guard<std::mutex> lock(m_mutexDataThread);
m_listDataMessage.clear();
}
SocketManager::SocketManager()
{
m_threadTcpWork = nullptr;
m_bTcpWorkFlag = false;
m_tcpServer = nullptr;
}
SocketManager::~SocketManager()
{
m_bTcpWorkFlag = true;
if (m_threadTcpWork->joinable())
{
m_threadTcpWork->join();
delete m_threadTcpWork;
m_threadTcpWork = nullptr;
}
delete m_tcpServer;
}
// 初始化
bool SocketManager::Init()
{
if (TcpInit())
{
std::cout << "tcp init success." << std::endl;
return true;
}
return false;
}
// tcp
bool SocketManager::TcpInit()
{
if (m_tcpServer != nullptr)
return false;
m_tcpServer = new TcpServer;
int listenfd = m_tcpServer->createsocket(8888);
if (listenfd < 0)
{
return false;
}
m_tcpServer->getChannelById(listenfd);
printf("server listen on port 8888, listenfd=%d ...\n", listenfd);
// 新连接接入
m_tcpServer->onConnection = [this](const SocketChannelPtr& channel)
{
std::string peeraddr = channel->peeraddr();
if (channel->isConnected())
{
// 心跳包
channel->setHeartbeat(3000, [this, channel]()
{
// 组装心跳包
std::string strRes;
testHb(strRes);
SendMsg(channel->io(), strRes);
});
printf("%s connected! connfd=%d id=%d tid=%ld\n", peeraddr.c_str(), channel->fd(), channel->id(), currentThreadEventLoop->tid());
}
else
{
printf("%s disconnected! connfd=%d id=%d tid=%ld\n", peeraddr.c_str(), channel->fd(), channel->id(), currentThreadEventLoop->tid());
}
};
// 接收数据处理
m_tcpServer->onMessage = [this](const SocketChannelPtr& channel, Buffer* buf)
{
// 从客户端接收到的数据,发送到消息队列处理
SocketDataMessage stMsg;
stMsg.m_stIo = channel->io();
stMsg.m_strSendData = std::string((char*)buf->data(), buf->size());
m_tcpQueue.addMsg(stMsg);
};
m_tcpServer->setThreadNum(4);
m_tcpServer->setLoadBalance(LB_LeastConnections);
#if TEST_TLS
hssl_ctx_opt_t ssl_opt;
memset(&ssl_opt, 0, sizeof(hssl_ctx_opt_t));
ssl_opt.crt_file = "cert/server.crt";
ssl_opt.key_file = "cert/server.key";
ssl_opt.verify_peer = 0;
m_tcpServer->withTLS(&ssl_opt);
#endif
m_tcpServer->start();
return true;
}
void InviewSocketManager::SendMsg(hio_t* p_ioIo, std::string p_strSend)
{
hio_write(p_ioIo, p_strSend.c_str(), p_strSend.size());
}
void SocketManager::tcpWork()
{
if (m_threadTcpWork != nullptr)
return;
m_threadTcpWork = new std::thread([this]()
{
SocketDataMessage stMsg;
while (!m_bTcpWorkFlag)
{
// 从消息队列获取数据
if (m_tcpQueue.getMsg(stMsg))
{
if (stMsg.m_strSendData.empty())
{
// TODO 返回错误给tools
std::cout << "PraseHeadMessage send data is empty.\n";
continue;
}
// 1、解包
HeadData stHeadData;
int nRet = PraseHeadMessage((char*)stMsg.m_strSendData.c_str(), stMsg.m_strSendData.size(), stHeadData);
if (nRet < 0)
{
std::cout << "PraseHeadMessage error! nRet = [" << nRet << "]";
continue;
}
// 2、消息处理
std::string strProtobufData;
switch (stHeadData.m_nCmdId)
{
case SOCKET_MSG_TYPE_HB:
{
std::cout << "Recv client hb.\n";
strProtobufData = getCurrentTime();
break;
}
}
// 3、组包
std::string strResData;
nRet = TobufHeadMessage(strProtobufData, strResData, stHeadData);
if (nRet < 0)
{
std::cout << "TobufHeadMessage error! nRet = [" << nRet << "]\n";
}
// 4、发送
hio_write(stMsg.m_stIo, strResData.c_str(), strResData.size());
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
}
// 运行
void SocketManager::Start()
{
tcpWork();
}
// 解包头
int SocketManager::PraseHeadMessage(char* p_pszBuf, unsigned int p_nBufLen, HeadData& p_stResData)
{
if (nullptr == p_pszBuf || p_nBufLen == 0)
{
std::cout << "PraseHeadMessage nullptr == p_pszBuf || p_nBufLen == 0\n";
return -1;
}
//开始解析私有头
int headLen = 0;
HeadData* privateHeader = (HeadData*)(p_pszBuf + headLen);
//用其他字段做简单校验,消息是否正常
if (!(privateHeader->m_cVersion <= 0x0f && privateHeader->m_nBeginMark <= 0xef && privateHeader->m_cFinishMark == char('#')))
{
std::cout << "message version or beginmark or finismark error! privateHeader->m_cVersion = [" << privateHeader->m_cVersion
<< "], privateHeader->m_nBeginMark = [ " << privateHeader->m_nBeginMark << "], privateHeader->m_cFinishMark = ["
<< privateHeader->m_cFinishMark << "]\n";
return -1;
}
p_stResData = *privateHeader;
return 0;
}
// 组包头
int SocketManager::TobufHeadMessage(const std::string p_strResMessage, std::string& p_strRes, HeadData& p_stResData)
{
// 组包
p_stResData.m_nLen = p_strResMessage.size();
char szBuf[sizeof(HeadData)] = { 0 };
memcpy(szBuf, &p_stResData, sizeof(HeadData));
p_strRes = std::string(szBuf, sizeof(HeadData)) + p_strResMessage;
return 0;
}
std::string SocketManager::getCurrentTime()
{
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::stringstream ss;
ss << std::put_time(std::localtime(&t), "%F %T");
return ss.str();
}
// 组装心跳包
void SocketManager::testHb(std::string& p_strRes)
{
// 组转心跳包
char cbuf[256] = { 0 };
SocketHB stHb;
stHb.m_strTimetemp = getCurrentTime();
memcpy(cbuf, &stHb, sizeof(SocketHB));
std::string nHbData = std::string(cbuf);
HeadData stHeadData;
stHeadData.m_nCmdId = SOCKET_MSG_TYPE_HB;
TobufHeadMessage(nHbData, p_strRes, stHeadData);
}
- main.cpp
#include <iostream>
#include "SocketManager.h"
using namespace std;
int main()
{
SocketManager stMsg;
stMsg.Init();
stMsg.Start();
while (1)
Sleep(1);
}
2、tcpClient
#include <iostream>
#include <sstream>
#include <iomanip>
#include "hv/TcpClient.h"
#include "hv/htime.h"
#define TEST_RECONNECT 1
#define TEST_TLS 0
using namespace hv;
// 自行扩展
enum SOCKET_MSG_TYPE
{
SOCKET_MSG_TYPE_NULLPTR = 0,
SOCKET_MSG_TYPE_HB = 1,
};
// 基础结构体、类
#pragma pack(1) //下面的结构体1字节对其
// 包头结构体
struct HeadData
{
HeadData() :
m_nBeginMark(0xef),
m_cVersion(1),
m_nCmdId(SOCKET_MSG_TYPE_NULLPTR),
m_nLen(),
m_arrResover(),
m_cCrc(),
m_cFinishMark(0x23)
{}
unsigned short m_nBeginMark; //起始标志
unsigned short m_cVersion; //目前填0
unsigned int m_nCmdId; // 消息类型 见枚举 SOCKET_MSG_TYPE
unsigned int m_nLen; //负载数据长度,不包含头
unsigned char m_arrResover[2]; //预留位
unsigned char m_cCrc; //私有数据头的校验字节
unsigned char m_cFinishMark; //结束标志
};
#pragma pack()
// 心跳结构体
struct SocketHB
{
SocketHB() : m_strTimetemp() {}
std::string m_strTimetemp; // 时间戳
};
// 解包头
int PraseHeadMessage(char* p_pszBuf, unsigned int p_nBufLen, HeadData& p_stResData)
{
if (nullptr == p_pszBuf || p_nBufLen == 0)
{
std::cout << "PraseHeadMessage nullptr == p_pszBuf || p_nBufLen == 0\n";
return -1;
}
//开始解析私有头
int headLen = 0;
HeadData* privateHeader = (HeadData*)(p_pszBuf + headLen);
//用其他字段做简单校验,消息是否正常
if (!(privateHeader->m_cVersion <= 0x0f && privateHeader->m_nBeginMark <= 0xef && privateHeader->m_cFinishMark == char('#')))
{
std::cout << "message version or beginmark or finismark error! privateHeader->m_cVersion = [" << privateHeader->m_cVersion
<< "], privateHeader->m_nBeginMark = [ " << privateHeader->m_nBeginMark << "], privateHeader->m_cFinishMark = ["
<< privateHeader->m_cFinishMark << "]\n";
return -1;
}
p_stResData = *privateHeader;
return 0;
}
// 组包头
int TobufHeadMessage(const std::string p_strResMessage, std::string& p_strRes, HeadData& p_stResData)
{
// 组包
p_stResData.m_nLen = p_strResMessage.size();
char szBuf[sizeof(HeadData)] = { 0 };
memcpy(szBuf, &p_stResData, sizeof(HeadData));
p_strRes = std::string(szBuf, sizeof(HeadData)) + p_strResMessage;
return 0;
}
std::string getCurrentTime()
{
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::stringstream ss;
ss << std::put_time(std::localtime(&t), "%F %T");
return ss.str();
}
int main(int argc, char* argv[])
{
int port = 8888;
TcpClient cli;
int connfd = cli.createsocket(port);
if (connfd < 0) {
return -20;
}
printf("client connect to port %d, connfd=%d ...\n", port, connfd);
cli.onConnection = [&cli](const SocketChannelPtr& channel) {
std::string peeraddr = channel->peeraddr();
if (channel->isConnected()) {
printf("connected to %s! connfd=%d\n", peeraddr.c_str(), channel->fd());
// 心跳包
channel->setHeartbeat(3000, [channel]()
{
std::string strRes;
// 组转心跳包
char cbuf[256] = { 0 };
SocketHB stHb;
stHb.m_strTimetemp = getCurrentTime();
memcpy(cbuf, &stHb, sizeof(SocketHB));
std::string nHbData = std::string(cbuf);
HeadData stHeadData;
stHeadData.m_nCmdId = SOCKET_MSG_TYPE_HB;
TobufHeadMessage(nHbData, strRes, stHeadData);
hio_write(channel->io(), strRes.c_str(), strRes.size());
});
}
else {
printf("disconnected to %s! connfd=%d\n", peeraddr.c_str(), channel->fd());
}
if (cli.isReconnect()) {
printf("reconnect cnt=%d, delay=%d\n", cli.reconn_setting->cur_retry_cnt, cli.reconn_setting->cur_delay);
}
};
cli.onMessage = [](const SocketChannelPtr& channel, Buffer* buf)
{
// TODO
// 解析
printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
};
#if TEST_RECONNECT
// reconnect: 1,2,4,8,10,10,10...
reconn_setting_t reconn;
reconn_setting_init(&reconn);
reconn.min_delay = 1000;
reconn.max_delay = 10000;
reconn.delay_policy = 2;
cli.setReconnect(&reconn);
#endif
#if TEST_TLS
cli.withTLS();
#endif
cli.start();
std::string str;
while (std::getline(std::cin, str)) {
if (str == "close") {
cli.closesocket();
}
else if (str == "start") {
cli.start();
}
else if (str == "stop") {
cli.stop();
break;
}
else {
if (!cli.isConnected()) break;
cli.send(str);
}
}
return 0;
}