用户自定义协议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了。 

serverclient的定义

有了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. DTLS协议中client/server的认证过程和密钥协商过程

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

  2. JAVA NIO工作原理及代码示例

    简介:本文主要介绍了JAVA NIO中的Buffer, Channel, Selector的工作原理以及使用它们的若干注意事项,最后是利用它们实现服务器和客户端通信的代码实例. 欢迎探讨,如有错误敬请 ...

  3. DotNetty 实现 Modbus TCP 系列 (四) Client & Server

    本文已收录至:开源 DotNetty 实现的 Modbus TCP/IP 协议 Client public class ModbusClient { public string Ip { get; } ...

  4. socket模块实现基于UDP聊天模拟程序;socketserver模块实现服务端 socket客户端代码示例

    socket模块 serSocket.setblocking(False) 设置为非阻塞: #coding=utf-8 from socket import * import time # 用来存储所 ...

  5. Java基础知识强化之IO流笔记72:NIO之 NIO核心组件(NIO使用代码示例)

    1.Java NIO 由以下几个核心部分组成: Channels(通道) Buffers(缓冲区) Selectors(选择器) 虽然Java NIO 中除此之外还有很多类和组件,Channel,Bu ...

  6. SFTP客户端代码示例

    参考链接:SFTP客户端代码示例 操作系统:Windows7/8,VS2013 环境:libssh2 1.4.3.zlib-1.2.8.openssl-1.0.1g 原文: “从http://www. ...

  7. Ice简介+Qt代码示例

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

  8. socket 建立网络连接,client && server

    client代码: package socket; import java.io.IOException; import java.net.Socket; /** * 客户端_聊天室 * * @aut ...

  9. 深入浅出 Redis client/server交互流程

    综述 最近笔者阅读并研究redis源码,在redis客户端与服务器端交互这个内容点上,需要参考网上一些文章,但是遗憾的是发现大部分文章都断断续续的非系统性的,不能给读者此交互流程的整体把握.所以这里我 ...

随机推荐

  1. UVA10020(最小区间覆盖)

    题意:       给你一个区间[0,m]和一些小的区间[l,r]让你选择最少的小区间个数去把整个区间覆盖起来. 思路:       算是比较经典的贪心题目吧(经典于难度没什么对应关系),大体思路可以 ...

  2. hdu5251最小矩形覆盖

    题意(中问题直接粘吧)矩形面积 Problem Description 小度熊有一个桌面,小度熊剪了很多矩形放在桌面上,小度熊想知道能把这些矩形包围起来的面积最小的矩形的面积是多少.   Input ...

  3. POJ2771最大独立集元素个数

    题意:       女生和男生之间只要满足四个条件中的一个,那么两个人就不会在一起!然后给出一些男生和女生,问最多多少人一起做活动彼此不会产生暧昧关系. 思路:       这样的问题还是比较裸的问法 ...

  4. .NET并发编程-TPL Dataflow并行工作流

    本系列学习在.NET中的并发并行编程模式,实战技巧 本小节了解TPL Dataflow并行工作流,在工作中如何利用现成的类库处理数据.旨在通过TDF实现数据流的并行处理. TDF Block 数据流由 ...

  5. php 获取某年后的日期

    比如两年后:date('Y-m-d',strtotime('+2 year')) 月份year改成month

  6. 在Visual Studio 中使用git——文件管理-上(四)

    在Visual Studio 中使用git--什么是Git(一) 在Visual Studio 中使用git--给Visual Studio安装 git插件(二) 在Visual Studio 中使用 ...

  7. Packing data with Python

    Defining how a sequence of bytes sits in a memory buffer or on disk can be challenging from time to ...

  8. mysql枚举和集合

    create table consumer( id int, name char(16), sex enum('male','female','other'), level enum('vip1',' ...

  9. str.isdigit()可以判断变量是否为数字

    字符串.isdigit()可以判断变量是否为数字 是则输出True 不是则输出False 好像只能字符串

  10. 02、SpringBoot2入门

    1.系统要求 Java 8 & 兼容java14 . Maven 3.3+ idea 2019.1.2 1.1.maven设置 <mirrors> <mirror> & ...