用户自定义协议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代码示例相关推荐

  1. 完整mes代码(含客户端和server端_Ice简介+Qt代码示例

    一.ICE是什么? ICE是ZEROC的开源通讯协议产品,它的全称是:The Internet Communications Engine,翻译为中文是互联网通讯引擎,是一个面向对象的中间件,它封装并 ...

  2. http协议php短信接口调用代码示例

    1. 提交方式与编码 http请求都应以POST形式提交,编码为UTF-8 2. 请求地址 请求地址(host)为:  http://www.lx198.com/sdk/ 不同的协议拥有不同的提交地址 ...

  3. java dtls server_DTLS协议(基于UDP)中client/server的认证过程和密钥协商过程

    我的总结:DTLS的握手就是协商出一个对称加密的秘钥(每个客户端的秘钥都会不一样),之后的通信就要这个秘钥进行加密通信.协商的过程要么使用非对称加密算法进行签名校验身份,要么通过客户端和服务器各自存对 ...

  4. java dtls server_DTLS协议中client/server的认证过程和密钥协商过程

    1.DTLS介绍 1.1 DTLS的作用 互联网先驱们最开始在设计互联网协议时主要考虑的是可用性,安全性是没有考虑在其中的,所以传输层的TCP.UDP协议本身都不具备安全性.SSL/TLS协议是基于T ...

  5. java ldap 工具_通过定义LdapUtil工具类实现JAVA关于LDAP协议目录数据保存和删除等操作代码示例...

    一.前言 关于基于jdk开发包javax.naming.directory.DirContext.javax.naming.directory.InitialDirContext目录协议定义LdapU ...

  6. php 解析 saml协议,解出SAMLRequest的代码示例

    "解url > 解base64 > 解压缩(zip.inflate)"的过程请参考如下代码示例:package org.apache; import java.io.* ...

  7. 2019-nCoV肺炎疫情同程查询-完整提供 Demo 代码示例及数据专业且全面的 API 查询接口

    更多资料请参考:www.woyaocha.net/product/trip2019ncov 手机查询链接(已开发好的):www.woyaocha.net/trip2019ncov 接口使用 我要查询 ...

  8. Java网络编程 Socket、ServerSocket 详解,方法介绍及完整代码示例

    Java网络编程 Socket.ServerSocket 详解,方法介绍及完整代码示例 概念 什么是网络编程? 网络编程是指编写运行在多个设备(计算机)的程序,这些设备通过网络连接起来.当这些通过网络 ...

  9. 快递查询(快递单号智能识别/快递公司+快递单号)-完整提供 Demo 代码示例及数据专业且全面的 API 查询接口

    更多资料请参考:www.woyaocha.net/product/express 查询说明 接口一:快递单号智能识别 快递单号智能识别,是根据查询的快递单号自动智能识别出该运单所属的快递公司,再获取快 ...

最新文章

  1. 【目标检测系列】CNN中的目标多尺度处理方法
  2. RPA和AI:加速数字化转型的步伐
  3. 从人类交互通信发展简史看元宇宙发展趋势及商业价值
  4. 多线程写文件_Shell简单实现多线程
  5. 给未来元素添加事件 jquery 1.10.2 版本
  6. 说说重试的那些实现方式
  7. 学习笔记(1)centos7 下安装nginx
  8. 性能优化:Sql语句中HINT不起作用
  9. 简单易懂的多线程(通过实现Runnable接口实现多线程)
  10. Git:常用命令(二)
  11. 面向对象的程序设计在游戏开发中使用(一):类
  12. Android笔记 fragment的生命周期
  13. layui select监听选中的值 二级联动
  14. 基于springboot的物资管理系统
  15. Android Binder -- AIDL 原理
  16. 在Ubuntu上安装D-link DWA-131驱动
  17. 修改+首选+dns服务器地址,首选dns服务器地址怎么设置
  18. 暴躁蒟蒻在线水题er日记
  19. 阿基米德螺旋线lisp_CAD画阿基米德螺旋线程序
  20. STM32CubeMX新建工程+基本IO配置过程

热门文章

  1. Solr 使用Facet分组过程中与分词的矛盾解决办法
  2. 2022-2028年中国塑料管的制造行业市场需求预测及投资策略研究报告
  3. Redis 高级特性(3)—— 持久化及数据恢复
  4. 判别模型和生成模型的区别
  5. Python ljust()方法
  6. 深度学习原理与框架-CNN在文本分类的应用 1.tf.nn.embedding_lookup(根据索引数据从数据中取出数据) 2.saver.restore(加载sess参数)...
  7. 英伟达TensorRT 8-bit Inference推理
  8. 全文翻译(一):TVM: An Automated End-to-End Optimizing Compiler for Deep Learning
  9. A100 GPU硬件架构
  10. CodeGen CreateFile实用程序