libhv tcp实战小项目

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;
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值