一、协议编写
1、Msg.proto,每一行的意思都写得很清楚了。Msg可以理解成一个顶层消息容器,里面可以放置登录、识别等消息。
syntax = "proto3"; // 指定ProtoBuf得版本,省略本行默认为2版本,如果使用3版本这句不可以省略
option optimize_for = LITE_RUNTIME; // 使用清凉版,没有反射等高级功能
package VxIVideo; // 包名,其实proto.exe编译后变成了命名空间
import "Login.proto"; // 导入依赖得包
import "Logout.proto"; // 导入依赖得包
import "Identify.proto"; // 导入依赖得包
message Msg // 定义一个消息,这里是朱消息
{
enum eMsgType // 枚举消息类型
{
MSG_TYPE_LOGIN_REQ = 0x0000;
MSG_TYPE_LOGIN_RSP = 0x0001;
MSG_TYPE_LOGOUT_NOTIFY = 0x0002;
MSG_TYPE_IDENTIFY_REQ = 0x0003;
MSG_TYPE_IDENTIFY_RSP = 0x0004;
}
eMsgType msg_type = 1; // 消息类型
LoginReq login_req = 2; // 登录请求
LoginRsp login_rsp = 3; // 登录响应
LogoutNotify logout_notify = 4; // 登出通知
IdentifyReq identify_req = 5; // 识别请求
IdentifyRsp identify_rsp = 6; // 识别响应
}
2、Login.proto,登录协议
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
package AI;
message LoginReq
{
string username = 1;
string password = 2;
}
message LoginRsp
{
enum eLoginRet
{
LOGIN_SUCCESS = 0x0000;
LOGIN_ACCOUNT_NULL = 0x0001;
LOGIN_ACCOUNT_LOCK = 0x0002;
LOGIN_PASSWORD_ERROR = 0x0003;
LOGIN_LOGIN_ERROR = 0x0004;
}
eLoginRet ret = 1;
}
3、Logout.proto,登出协议
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
package AI;
message LogoutNotify
{
}
4、Identify.proto,识别协议
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
package AI;
message IdentifyReq
{
message Image
{
int32 format = 1;
uint32 size = 2;
bytes data = 3;
}
int32 camera_id = 1;
repeated Image img = 2;
}
message IdentifyRsp
{
enum eIdentifyRet
{
IDENTIFY_SUCCESS = 0x0000;
IDENTIFY_FAILED = 0x0001;
}
eIdentifyRet ret = 1;
string result = 2;
}
二、生成C++和Python协议文件
// $SRC_DIR: .proto 所在的源目录
// --cpp_out: 生成 c++ 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/xxx.proto
生产如下文件,将生成的文件添加到工程中即可。
三、编码实现
1、服务端
# !usr/bin/python
# -*- coding:utf-8 -*-
""" A tcp dientify server"""
__author__ = "huzhenhong@2019-12-16"
import socketserver
import time
import struct
import Msg_pb2
class ClientMsgHandle(socketserver.BaseRequestHandler):
def handle(self):
while True: # 每个新的连接都有自己的消息循环
try:
self.data = self.request.recv(1024)
data_len = int(self.data[:4].decode('ascii'), 16)
print(data_len)
self.data = self.data[4:]
if data_len + 4 > 1024:
# 还需要读取的长度
need_more_data_len = data_len - (1024 - 4)
read_sum = int(need_more_data_len / 1024)
surplus_len = int(need_more_data_len % 1024)
for _ in range(read_sum):
self.data += self.request.recv(1024)
self.data += self.request.recv(surplus_len)
msg = Msg_pb2.Msg()
msg.ParseFromString(self.data)
if msg.eMsgType.MSG_TYPE_LOGIN_REQ == msg.msg_type:
self.handle_login_req(msg.login_req)
elif msg.eMsgType.MSG_TYPE_LOGOUT_NOTIFY == msg.msg_type:
self.handle_logout_notify()
break # 退出消息循环
elif msg.eMsgType.MSG_TYPE_IDENTIFY_REQ == msg.msg_type:
self.handle_identify_req(msg.identify_req)
else:
print('error message type!')
except Exception as e:
print(self.client_address,"连接已断开")
identify_server.shutdown()
break
finally:
print('finish handle')
time.sleep(0.1)
self.request.close()
def handle_login_req(self, login_req):
"""处理登录请求:param login_req::return:"""
print('login req, username: {}, password: {}'.format(login_req.username, login_req.password))
login_rsp_msg = Msg_pb2.Msg()
login_rsp_msg.msg_type = login_rsp_msg.eMsgType.MSG_TYPE_LOGIN_RSP
login_rsp_msg.login_rsp.ret = login_rsp_msg.login_rsp.eLoginRet.LOGIN_SUCCESS
self.send_msg(login_rsp_msg)
def handle_identify_req(self, identify_req):
"""处理识别请求:param identify_req::return:"""
print('identify req, camera_id: {}'.format(identify_req.camera_id))
imgs = identify_req.img
[print('image size: {}, format: {}'.format(im.size, im.format)) for im in imgs]
img = imgs[0]
f = open("out.png", 'wb') # 二进制写模式
f.write(bytes(img.data)) # 二进制写
identify_rsp_msg = Msg_pb2.Msg()
identify_rsp_msg.msg_type = identify_rsp_msg.eMsgType.MSG_TYPE_IDENTIFY_RSP
identify_rsp_msg.identify_rsp.ret = identify_rsp_msg.identify_rsp.eIdentifyRet.IDENTIFY_SUCCESS
identify_rsp_msg.identify_rsp.result = 'normal'
self.send_msg(identify_rsp_msg)
def handle_logout_notify(self):
"""处理登出:return:"""
print(self.client_address, "主动断开")
identify_server.shutdown()
def send_msg(self, msg):
"""发送请求响应:param rsp_bytes::return:"""
rsp_bytes = msg.SerializeToString()
# 数据长度unsinged int 转 bytes
head = struct.pack(">I", len(rsp_bytes))
self.request.sendall(head + rsp_bytes)
HOST, PORT = "172.30.1.173", 8888
# identify_server = socketserver.ThreadingTCPServer((HOST, PORT), ClientMsgHandle)
identify_server = socketserver.ForkingTCPServer((HOST, PORT), ClientMsgHandle)
identify_server.serve_forever()
2、客户端
// 头文件#pragma once#include #include #include #include "Msg.pb.h"
class CIdentifyClient
{
public:
static CIdentifyClient * Instance();
bool Initialize(const std::string & ip, const unsigned int port);
void Uninitialize();
void Login(const std::string & username, const std::string & password);
void Logout();
void Identify();
private:
void SendMsg(const AI::Msg & msg);
void OnLoginResponse(const AI::LoginRsp& loginRsp);
void OnIdentifyResponse(const AI::IdentifyRsp& identifyRsp);
void HandelMsg();
unsigned int GetDataLen(const std::string & head);
private:
CIdentifyClient()
: m_pThread(nullptr)
{
}
~CIdentifyClient(){}
CIdentifyClient(const CIdentifyClient &) = delete;
CIdentifyClient & operator=(const CIdentifyClient &) = delete;
private:
SOCKET m_socket;
std::thread * m_pThread;
};
// cpp文件#include "IdentifyClient.h"#pragma comment(lib,"ws2_32.lib")#pragma warning(disable:4996)#include #include #include #include "spdlog/spdlog.h"
CIdentifyClient * CIdentifyClient::Instance()
{
static CIdentifyClient instance;
return &instance;
}
bool CIdentifyClient::Initialize(const std::string & ip, const unsigned int port)
{
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD(1, 1);
int err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
spdlog::info("WSAStartup failed.");
return false;
}
if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) != 1)
{
spdlog::info("wsaData.wVersion failed.");
WSACleanup();
return false;
}
m_socket = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr(ip.data());
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(port);
int ret = connect(m_socket, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
if (SOCKET_ERROR == ret)
{
spdlog::info("connect failed.");
return false;
}
std::thread networkThread(&CIdentifyClient::HandelMsg, this);
networkThread.detach();
return true;
}
void CIdentifyClient::Uninitialize()
{
closesocket(m_socket);
WSACleanup();
}
void CIdentifyClient::Login(const std::string & username, const std::string & password)
{
AI::LoginReq * pLoginReq = new AI::LoginReq;
pLoginReq->set_username(username);
pLoginReq->set_password(password);
AI::Msg msg;
msg.set_msg_type(AI::Msg::MSG_TYPE_LOGIN_REQ);
msg.set_allocated_login_req(pLoginReq);
SendMsg(msg);
}
void CIdentifyClient::Logout()
{
AI::LogoutNotify * pLogoutNotify = new AI::LogoutNotify;
AI::Msg msg;
msg.set_msg_type(AI::Msg::MSG_TYPE_LOGOUT_NOTIFY);
msg.set_allocated_logout_notify(pLogoutNotify);
SendMsg(msg);
}
void CIdentifyClient::Identify()
{
// 加载图片 std::ifstream infile("D:\\MyStudy\\protobuf\\ProtolbufTest\\x64\\Release\\in.png", std::ios::binary);
if (!infile)
{
spdlog::info("open img failed.");
return;
}
infile.seekg(0, std::ios::end);
int fileSize = infile.tellg();
infile.seekg(std::ios::beg);
char * pBmp = (char *)malloc(fileSize);
infile.read(pBmp, fileSize);
infile.close();
// 设置图片 AI::IdentifyReq * pIdentifyReq = new AI::IdentifyReq;
AI::IdentifyReq::Image * pImg = pIdentifyReq->add_img();
pImg->set_data(pBmp, fileSize);
pImg->set_format(1);
pImg->set_size(fileSize);
// 设置消息 AI::Msg msg;
msg.set_msg_type(AI::Msg::MSG_TYPE_IDENTIFY_REQ);
msg.set_allocated_identify_req(pIdentifyReq);
SendMsg(msg);
}
void CIdentifyClient::SendMsg(const AI::Msg & msg)
{
auto data = msg.SerializeAsString();
char head[4];
sprintf(head, "%4x", data.size());
std::string sendData = head + data;
send(m_socket, sendData.data(), sendData.size(), 0);
}
void CIdentifyClient::OnLoginResponse(const AI::LoginRsp & loginRsp)
{
auto ret = loginRsp.ret();
std::cout << "login ret : " << ret << std::endl;
Identify();
}
void CIdentifyClient::OnIdentifyResponse(const AI::IdentifyRsp & identifyRsp)
{
auto ret = identifyRsp.ret();
auto result = identifyRsp.result();
std::cout << "identify ret : " << ret << " result : " << result << std::endl;
}
void CIdentifyClient::HandelMsg()
{
while (true)
{
std::string recvData;
char buf[1024];
int ret = recv(m_socket, buf, 1024, 0);
if (ret <= 0)
{
Sleep(100);
continue;
}
int haveReadLen = ret - 4;
std::string head;
head.resize(4);
head[0] = buf[0];
head[1] = buf[1];
head[2] = buf[2];
head[3] = buf[3];
//int i = std::stoi(head, 0, 16);
int dataLen = GetDataLen(head);
while (haveReadLen != dataLen)
{
ret = recv(m_socket, buf, 1024, 0);
if (SOCKET_ERROR == ret)
{
continue;
}
haveReadLen += ret;
}
std::string data;
data.resize(dataLen);
for (int i = 0; i < dataLen; ++i)
{
data[i] = buf[i + 4];
}
AI::Msg msg;
auto result = msg.ParseFromString(data);
if (!result)
{
// return -1; }
auto type = msg.msg_type();
switch (msg.msg_type())
{
case AI::Msg::MSG_TYPE_LOGIN_RSP:
{
OnLoginResponse(msg.login_rsp());
break;
}
case AI::Msg::MSG_TYPE_IDENTIFY_RSP:
{
OnIdentifyResponse(msg.identify_rsp());
break;
}
default:
break;
}
Sleep(100);
}
}
unsigned int CIdentifyClient::GetDataLen(const std::string & head)
{
unsigned int sum = 0;
for (int i = 0; i < head.size(); ++i)
{
unsigned int tmp = head[i];
tmp << (head.size() - 1 - i);
sum += tmp;
}
return sum;
}
3、主程序
#include "spdlog/spdlog.h"#include "IdentifyClient.h"
int main()
{
std::string ip = "172.30.1.173";
unsigned int port = 8888;
if (!CIdentifyClient::Instance()->Initialize(ip, port))
{
spdlog::error("Network connect failed!");
return -1;
}
std::string username = "admin";
std::string password = "1234";
CIdentifyClient::Instance()->Login(username, password);
getchar();
return 0;
}
四、总结
1、每个消息类在使用完成后都会自动析构,比如
VxMessage::~VxMessage() {
// @@protoc_insertion_point(destructor:VxIVideo.VxMessage) SharedDtor();
}
void VxMessage::SharedDtor() {
#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER if (this != &default_instance()) {
#else if (this != default_instance_) {
#endif delete login_req_;
delete login_rsp_;
delete logout_notify_;
delete identify_req_;
delete identify_rsp_;
}
}
亦即会调用delete删除每一个字段,然如果字段定义在栈上,此时就崩溃了。有两种处理方式,一种是上述采用的,每个字段都在堆上new出来,另外还有一种方式是SendMsg(msg)之后马上调用VxMsg.release_identify_rsp()来释放掉该字段,这样在析构的时候identify_rsp_字段就是NULL,可能是protobuf重载过delete了,但是又没找到重载的地方,此时delete就不会出错。(有清楚的童鞋请不吝赐教)
2、对消息的解析就算成功也可能返回false,网上查证说是protobuf3的一个bug
3、char ch = '\xa';直接强转就可以得到十进制数值。
4、数据发送时在源数据之上添加固定长度为4的消息头,以确保可能拿到完成的有效数据进行解析。简单解决TCP粘包问题,对TCP协议有了更深的了解。
5、python socketserver 模块已经封装好了网络和多线程多进程,C++的实现上等待后续添加短线重连、线程池。