用户自定义协议client/server代码示例
用户自定义协议client/server代码示例
代码参考链接:https://github.com/sogou/workflow
message.h
message.cc
server.cc
client.cc
关于user_defined_protocol
本示例设计一个简单的通信协议,并在协议上构建server和client。server将client发送的消息转换成大写并返回。
协议的格式
协议消息包含一个4字节的head和一个message
body。head是一个网络序的整数,指明body的长度。
请求和响应消息的格式一致。
协议的实现
用户自定义协议,需要提供协议的序列化和反序列化方法,这两个方法都是ProtocolMeessage类的虚函数。
另外,为了使用方便,强烈建议用户实现消息的移动构造和移动赋值(用于std::move())。 在ProtocolMessage.h里,序列化反序列化接口如下:
namespace protocol
{
class ProtocolMessage
: public CommMessageOut,
public CommMessageIn
{
private:
virtual
int encode(struct iovec
vectors[], int max);
/*
You have to implement one of the 'append' functions, and the first one
* with
arguement 'size_t *size' is recommmended. */
virtual
int append(const void
*buf, size_t *size);
virtual int append(const void
*buf, size_t size);
...
};
}
序列化函数encode
- encode函数在消息被发送之前调用,每条消息只调用一次。
- encode函数里,用户需要将消息序列化到一个vector数组,数组元素个数不超过max。目前max的值为8192。
- 结构体struct iovec定义在请参考系统调用readv和writev。
- encode函数正确情况下的返回值在0到max之间,表示消息使用了多少个vector。
- 如果是UDP协议,请注意总长度不超过64k,并且使用不超过1024个vector(Linux一次writev只能1024个vector)。
- UDP协议只能用于client,无法实现UDP
server。 - encode返回-1表示错误。返回-1时,需要置errno。如果返回值>max,将得到一个EOVERFLOW错误。错误都在callback里得到。
- 为了性能考虑vector里的iov_base指针指向的内容不会被复制。所以一般指向消息类的成员。
反序列化函数append
- append函数在每次收到一个数据块时被调用。因此,每条消息可能会调用多次。
- buf和size分别是收到的数据块内容和长度。用户需要把数据内容复制。
- 如果实现了append(const void *buf, size_t *size)接口,可以通过修改*size来告诉框架本次消费了多少长度。收到的size
- 消耗的size = 剩余的size,剩余的那部分buf会由下一次append被调用时再次收到。此功能更方便协议解析,当然用户也可以全部复制自行管理,则无需修改*size。 - append函数返回0表示消息还不完整,传输继续。返回1表示消息结束。-1表示错误,需要置errno。
- 总之append的作用就是用于告诉框架消息是否已经传输结束。不要在append里做复杂的非必要的协议解析。
errno的设置
- encode或append返回-1或其它负数都会被理解为失败,需要通过errno来传递错误原因。用户会在callback里得到这个错误。
- 如果是系统调用或libc等库函数失败(比如malloc),libc肯定会设置好errno,用户无需再设置。
- 一些消息不合法的错误是比较常见的,比如可以用EBADMSG,EMSGSIZE分别表示消息内容错误,和消息太大。
- 用户可以选择超过系统定义errno范围的值来表示一些自定义错误。一般大于256的值是可以用的。
- 请不要使用负数errno。因为框架内部用了负数来代表SSL错误。
示例里,消息的序列化反序列化都非常的简单。
头文件message.h里,声明了request和response类:
namespace protocol
{
class TutorialMessage
: public ProtocolMessage
{
private:
virtual
int encode(struct iovec
vectors[], int max);
virtual
int append(const void
*buf, size_t size);
...
};
using TutorialRequest = TutorialMessage;
using TutorialResponse = TutorialMessage;
}
request和response类,都是同一种类型的消息。直接using就可以。
注意request和response必须可以无参数的被构造,也就是说需要有无参数的构造函数,或完全没有构造函数。
此外,通讯过程中,如果发生重试,response对象会被销毁并重新构造。因此,它最好是一个RAII类。否则处理起来会比较复杂。
message.cc里包含了encode和append的实现:
namespace protocol
{
int TutorialMessage::encode(struct iovec
vectors[], int max/*max==8192*/)
{
uint32_t
n = htonl(this->body_size);
memcpy(this->head,
&n, 4);
vectors[0].iov_base = this->head;
vectors[0].iov_len = 4;
vectors[1].iov_base = this->body;
vectors[1].iov_len = this->body_size;
return
2; /* return the number of vectors used, no more then max.
*/
}
int TutorialMessage::append(const void
*buf, size_t size)
{
if
(this->head_received
< 4)
{
size_t
head_left;
void
*p;
p = &this->head[this->head_received];
head_left = 4 - this->head_received;
if
(size < 4 - this->head_received)
{
memcpy(p,
buf, size);
this->head_received += size;
return
0;
}
memcpy(p,
buf, head_left);
size -= head_left;
buf = (const char
*)buf + head_left;
p = this->head;
this->body_size = ntohl(*(uint32_t *)p);
if
(this->body_size
> this->size_limit)
{
errno = EMSGSIZE;
return
-1;
}
this->body = (char *)malloc(this->body_size);
if
(!this->body)
return
-1;
this->body_received = 0;
}
size_t
body_left = this->body_size - this->body_received;
if
(size > body_left)
{
errno = EBADMSG;
return
-1;
}
memcpy(this->body,
buf, body_left);
if
(size < body_left)
return
0;
return
1;
}
}
encode的实现非常简单,固定使用了两个vector,分别指向head和body。需要注意iov_base指针必须指向消息类的成员。
append需要保证4字节的head接收完整,再读取message body。而且我们并不能保证第一次append一定包含完整的head,所以过程略为繁琐。
append实现了size_limit功能,超过size_limit的会返回EMSGSIZE错误。用户如果不需要限制消息大小,可以忽略size_limit这个域。
由于要求通信协议是一来一回的,所谓的“TCP包”问题不需要考虑,直接当错误消息处理。
现在,有了消息的定义和实现,就可以建立server和client了。
server和client的定义
有了request和response类,我们就可以建立基于这个协议的server和client。前面的示例里介绍过Http协议相关的类型定义:
using WFHttpTask =
WFNetworkTask<protocol::HttpRequest,
protocol::HttpResponse>;
using http_callback_t
= std::function<void (WFHttpTask *)>;
using WFHttpServer =
WFServer<protocol::HttpRequest,
protocol::HttpResponse>;
using http_process_t
= std::function<void (WFHttpTask *)>;
同样的,对这个Tutorial协议,数据类型的定义并没有什么区别:
using WFTutorialTask =
WFNetworkTask<protocol::TutorialRequest,
protocol::TutorialResponse>;
using tutorial_callback_t
= std::function<void (WFTutorialTask
*)>;
using WFTutorialServer =
WFServer<protocol::TutorialRequest,
protocol::TutorialResponse>;
using tutorial_process_t
= std::function<void (WFTutorialTask
*)>;
server端
server与普通的http
server没有什么区别。优先IPv6启动,这不影响IPv4的client请求。另外限制请求最多不超过4KB。
代码请自行参考server.cc
client端
client端的逻辑是从标准IO接收用户输入,构造出请求发往server并得到结果。
为了简单,读取标准输入的过程都在callback里完成,因此我们会先发出一条空请求。同样为了安全我们限制server回复包不超4KB。
client端唯一需要了解的就是怎么产生一个自定义协议的client任务,在WFTaskFactory.h有三个接口可以选择:
template<class REQ, class RESP>
class WFNetworkTaskFactory
{
private:
using
T = WFNetworkTask<REQ, RESP>;
public:
static
T *create_client_task(TransportType type,
const std::string& host,
unsigned short
port,
int retry_max,
std::function<void (T *)> callback);
static
T *create_client_task(TransportType type,
const std::string& url,
int retry_max,
std::function<void (T *)> callback);
static
T *create_client_task(TransportType type,
const URI& uri,
int retry_max,
std::function<void (T *)>
callback);
...
};
其中,TransportType指定传输层协议,目前可选的值包括TT_TCP,TT_UDP,TT_SCTP和TT_TCP_SSL。
三个接口的区别不大,在这个示例里暂时不需要URL,用域名和端口来创建任务。
实际的调用代码如下。派生了WFTaskFactory类,但这个派生并非必须的。
using namespace
protocol;
class MyFactory
: public WFTaskFactory
{
public:
static
WFTutorialTask *create_tutorial_task(const std::string& host,
unsigned short
port,
int retry_max,
tutorial_callback_t callback)
{
using
NTF = WFNetworkTaskFactory<TutorialRequest, TutorialResponse>;
WFTutorialTask *task = NTF::create_client_task(TT_TCP, host, port,
retry_max,
std::move(callback));
task->set_keep_alive(30
* 1000);
return
task;
}
};
可以看到用了WFNetworkTaskFactory<TutorialRequest,
TutorialResponse>类来创建client任务。
接下来通过任务的set_keep_alive()接口,让连接在通信完成之后保持30秒,否则,将默认采用短连接。
client的其它代码涉及的知识点在之前的示例里都包含了。请参考client.cc
内置协议的请求是怎么产生的
现在系统中内置了http,
redis,mysql,kafka四种协议。可以通过相同的方法产生一个http或redis任务吗?比如:
WFHttpTask *task = WFNetworkTaskFactory<protocol::HttpRequest,
protocol::HttpResponse>::create_client_task(...);
需要说明的是,这样产生的http任务,会损失很多的功能,比如,无法根据header来识别是否用持久连接,无法识别重定向等。
同样,如果这样产生一个MySQL任务,可能根本就无法运行起来。因为缺乏登录认证过程。
一个kafka请求可能需要和多台broker有复杂的交互过程,这样创建的请求显然也无法完成这一过程。
可见每一种内置协议消息的产生过程都远远比这个示例复杂。同样,如果用户需要实现一个更多功能的通信协议,还有许多代码要写。
用户自定义协议client/server代码示例的更多相关文章
- DTLS协议中client/server的认证过程和密钥协商过程
我的总结:DTLS的握手就是协商出一个对称加密的秘钥(每个客户端的秘钥都会不一样),之后的通信就要这个秘钥进行加密通信.协商的过程要么使用非对称加密算法进行签名校验身份,要么通过客户端和服务器各自存对 ...
- JAVA NIO工作原理及代码示例
简介:本文主要介绍了JAVA NIO中的Buffer, Channel, Selector的工作原理以及使用它们的若干注意事项,最后是利用它们实现服务器和客户端通信的代码实例. 欢迎探讨,如有错误敬请 ...
- DotNetty 实现 Modbus TCP 系列 (四) Client & Server
本文已收录至:开源 DotNetty 实现的 Modbus TCP/IP 协议 Client public class ModbusClient { public string Ip { get; } ...
- socket模块实现基于UDP聊天模拟程序;socketserver模块实现服务端 socket客户端代码示例
socket模块 serSocket.setblocking(False) 设置为非阻塞: #coding=utf-8 from socket import * import time # 用来存储所 ...
- Java基础知识强化之IO流笔记72:NIO之 NIO核心组件(NIO使用代码示例)
1.Java NIO 由以下几个核心部分组成: Channels(通道) Buffers(缓冲区) Selectors(选择器) 虽然Java NIO 中除此之外还有很多类和组件,Channel,Bu ...
- SFTP客户端代码示例
参考链接:SFTP客户端代码示例 操作系统:Windows7/8,VS2013 环境:libssh2 1.4.3.zlib-1.2.8.openssl-1.0.1g 原文: “从http://www. ...
- Ice简介+Qt代码示例
1.ICE是什么? ICE是ZEROC的开源通信协议产品,它的全称是:The Internet Communications Engine,翻译为中文是互联网通信引擎,是一个面向对象的中间件,它封装并 ...
- socket 建立网络连接,client && server
client代码: package socket; import java.io.IOException; import java.net.Socket; /** * 客户端_聊天室 * * @aut ...
- 深入浅出 Redis client/server交互流程
综述 最近笔者阅读并研究redis源码,在redis客户端与服务器端交互这个内容点上,需要参考网上一些文章,但是遗憾的是发现大部分文章都断断续续的非系统性的,不能给读者此交互流程的整体把握.所以这里我 ...
随机推荐
- 过 DNF TP 驱动保护(一)
过 DNF TP 驱动保护(一) 文章目录: 01. 博文简介: 02. 环境及工具准备: 03. 分析 TP 所做的保护: 04. 干掉 NtOpenProc ...
- Windows核心编程 第三章 内核对象
第3章内核对象 在介绍Windows API的时候,首先要讲述内核对象以及它们的句柄.本章将要介绍一些比较抽象的概念,在此并不讨论某个特定内核对象的特性,相反只是介绍适用于所有内核对象的特性. 首先介 ...
- Node-Web应用框架Express
Express 是 node.js Web应用框架, 帮助你创建各种 Web 应用,和丰富的 HTTP 工具. 使用 Express 可以快速地搭建一个完整功能的网站. Express 框架核心特性: ...
- layui中时间插件laydate的使用
1.加载layui.js 2.html部分 <div class="layui-inline"> <label class="layui-form-la ...
- 有关80386cpu在保护模式下的虚拟地址,线性地址和实际物理地址的关系
80386cpu是8086cpu的升级版,其具有32位的寄存器.(32根地址线和32根数据线) 8086cpu其是16位的寄存器但是其地址线有20根,其寻址范围为2的20次方,但是有一个16位的寄存器 ...
- Java 并发编程(一) → LockSupport 详解
开心一刻 今天突然收到花呗推送的消息,说下个月 9 号需要还款多少钱 我就纳了闷了,我很长时间没用花呗了,怎么会欠花呗钱? 后面我一想,儿子这几天玩了我手机,是不是他偷摸用了我的花呗 于是我找到儿子问 ...
- 探索GaussDB(DWS)的过程化SQL语言能力
摘要:在当前GaussDB(DWS)的能力中主要支持两种过程化SQL语言,即基于PostgreSQL的PL/pgSQL以及基于Oracle的PL/SQL.本篇文章我们通过匿名块,函数,存储过程向大家介 ...
- Linux(深度)系统安装富士施乐(网络)打印机
一般来讲,linux系统识别打印机没有问题,重点难点在于后面设置.此文特别感谢:河北石龙的陈一繁销售代表.P288dw施乐官网并未提供Linux的驱动并在安装过程中遇到很多问题,其不厌其烦的为我联系厂 ...
- pytorch实现LeNet5分类CIFAR10
关于LeNet-5 LeNet5的Pytorch实现在网络上已经有很多了,这里记录一下自己的实现方法. LeNet-5出自于Gradient-Based Learning Applied to Doc ...
- 如何安装多个jdk并方便切换系统jdk版本
如何安装多个jdk并方便切换系统jdk版本 前言 在安装myeclipse时,压缩包中附带1.8.0的jdk,顺便安装并配置环境变量后发现系统默认的jdk变为了1.8.0.随后发现eclipse只支持 ...