Peach是基于DotNetty的Socket网络通讯帮助类库,可以帮助开发者简化使用DotNetty,关于DotNetty可参考我之前的这篇文章

Peach内置实现了一个基于文本协议的CommandLineProtocol,下面的实例以这个协议展开,最后以DotBPE中Amp协议来介绍下如何扩展自定义协议。

Github地址: https://github.com/xuanye/Peach

QuickStart 使用

添加引用

dotnet nuget add Peach

要使用Peach编写网络程序,一般只需要三个步骤

  1. 实现协议传输消息IMessage
  2. 实现协议打包和解包逻辑IProtocol
  3. 实现ISocketService完成服务端逻辑编写

在快速开始的实例中,我们使用内置的CommandLineProtocol,所以省去了步骤1,2让我们开始吧!

1 服务端

1.1 实现MyService

可分别重写

  1. OnConnected 有客户端连接上的事件
  2. OnDisConnected 客户端断开连接时的事件
  3. OnReceive 收到客户端消息的事件
  4. OnException 发生异常时的事件,如异常断开
public class MyService : Peach.AbsSocketService<Peach.Messaging.CommandLineMessage>
{
private readonly ILogger<MyService> _logger; public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public override void OnConnected(ISocketContext<CommandLineMessage> context)
{
_logger.LogInformation("client connected from {0}", context.RemoteEndPoint);
base.OnConnected(context);
} public override void OnDisconnected(ISocketContext<CommandLineMessage> context)
{
_logger.LogInformation("client disconnected from {0}", context.RemoteEndPoint);
base.OnDisconnected(context);
} public override void OnException(ISocketContext<CommandLineMessage> context, Exception ex)
{
_logger.LogError(ex,"client from {0}, occ error {1}", context.RemoteEndPoint,ex.Message);
base.OnException(context, ex);
} public override void OnReceive(ISocketContext<CommandLineMessage> context, CommandLineMessage msg)
{
string replyMessage = string.Empty;
string replyCmd = string.Empty;
switch (msg.Command)
{
case "echo":
replyMessage = msg.Parameters[0];
replyCmd = "echo";
break;
case "init":
replyMessage = "ok";
replyCmd = "init_reply"; break;
default:
replyMessage = "error unknow command";
break;
} Task.Run(async () =>
{
await context.SendAsync(new CommandLineMessage(replyCmd, replyMessage));
});
} }

2. 挂载服务

服务默认挂载在5566端口

static void Main(string[] args)
{
var builder = new HostBuilder()
.ConfigureServices((context,services) =>
{
//协议
services.AddSingleton<IProtocol<CommandLineMessage>, CommandLineProtocol>();
//挂载服务逻辑
services.AddSingleton<ISocketService<CommandLineMessage>, MyService>();
//添加挂载的宿主服务
services.AddTcpServer<CommandLineMessage>();
})
.ConfigureLogging(
logger =>
{
logger.AddConsole();
}
);
builder.RunConsoleAsync().Wait();
}

2. 客户端

2.1 使用内置的TcpClient

监听接收消息和链接的消息,如下所示:

TcpClient<CommandLineMessage> client = new TcpClient<CommandLineMessage>(new CommandLineProtocol());
client.OnReceived += Client_OnReceived;
client.OnConnected += Client_OnConnected; Task.Run(async () =>
{
//连接服务器
var socketContext = await client.ConnectAsync(new IPEndPoint(Hey.IPUtility.GetLocalIntranetIP(), 5566));
//发送消息
var initCmd = new Hey.Messaging.CommandLineMessage("init");
await socketContext.SendAsync(initCmd);
}).Wait();

可用的事件:

  • OnReceived 当收到服务端消息时
  • OnError 当通讯发生异常时
  • OnConnected 当连接上服务器时
  • OnDisconnected 当与服务端断开链接时
  • OnIdleState 链接闲置时触发,一般在此事件中发送心跳包

3. 自定义协议

Peach支持使用自定义协议,扩展协议需要自行实现两个接口:

3.1. IMessage 接口

实现类具体实现通讯消息的内容载体,只需实现如何获取消息长度的属性

public interface IMessage
{
int Length { get; }
}

3.2. IProtocol 接口

实现类需要描述消息头信息和具体打包解包逻辑,头信息描述参见ProtocolMeta字段描述

/// <summary>
/// 协议接口
/// </summary>
/// <typeparam name="TMessage"></typeparam>
public interface IProtocol<TMessage>
where TMessage : Messaging.IMessage
{
ProtocolMeta GetProtocolMeta(); TMessage Parse(Buffer.IBufferReader reader); void Pack(Buffer.IBufferWriter writer, TMessage message); }

3.3 Amp协议

为了更好让读者理解自定义协议的操作,这里以DotBPE中的Amp协议为例,来具体讲解一下,先来看下Amp协议的说明:

      0        1 2 3 4   5 6 7 8     9     10 11 12 13   1415      16171819    20    <length>-21
+------------+----------+---------+------+-------------+---------+---------+--------+------------+
| <ver/argc> | <length> | <seq> |<type>| <serviceId> | <msgId> | <code> | <codec>| <data> |
+------------+----------+---------+------+-------------+---------+---------+--------+------------+

Amp协议固定包头上21个字节,说明如下:

  • ver/argc = 版本 固定填1
  • length = 为总包长
  • seq = 请求序列号
  • type = 消息类型
    • 1 = Request 请求消息
    • 2 = Response 响应消息
    • 3 = Notify 通知消息
    • 4 = OneWayRequest 调用不关心返回值
  • serId = serviceId 服务号
  • msgId = msgId 消息ID
  • code = 当 type = 0 (请求时)固定传0 ,其他即为响应码,如果响应码不为0 则认为请求失败,具体错误码再定义
  • codecType = 编码方式 0=默认 Protobuf 1=MessagePack 2=JSON
  • data = 实际的业务数据

3.3.1 AmpMessage实现

为了避免干扰因素,这里的代码去除了一些,辅助行的字段和方法,AmpMessage其实是主要用于描述头信息的,并且包含body的buffer数据 Data字段,并实现获取消息体Length的方法(用于发送消息时,计算缓冲区)

 public class AmpMessage :  Peach.Messaging.IMessage
{
/// <summary>
/// 第一个版本为18个字节头固定长度
/// </summary>
public const int VERSION_0_HEAD_LENGTH = 18;
/// <summary>
/// 现有版本21个字节头固定长度
/// </summary>
public const int VERSION_1_HEAD_LENGTH = 21;
/// <summary>
/// 状态码
/// </summary>
public int Code { get; set; } //0 默认为Protobuf 1 MessagePack 2 = JSON
public CodecType CodecType { get; set; } /// <summary>
/// 实际的请求数据
/// </summary>
public byte[] Data { get; set; } public int Length {
get
{
var hl = Version == 0 ? VERSION_0_HEAD_LENGTH : VERSION_1_HEAD_LENGTH;
if(Data == null)
{
return hl;
} return hl + this.Data.Length;
}
} /// <summary>
/// 消息标识
/// </summary>
public string Id => $"{ServiceId}|{MessageId}|{Sequence}";
/// <summary>
/// 调用服务的唯一消息号 确定哪个方法
/// </summary>
public ushort MessageId { get; set; }
/// <summary>
/// 请求的序列号
/// </summary>
public int Sequence { get; set; }
/// <summary>
/// 调用服务的唯一服务号 确定哪个服务
/// </summary>
public int ServiceId { get; set; } /// <summary>
/// 协议版本0/1
/// </summary>
public byte Version { get; set; } public InvokeMessageType InvokeMessageType { get; set; }
} public enum InvokeMessageType : byte
{
Request = 1,
Response = 2,
Notify = 3,
OnewayRequest=4 //请求且不等待回复
}

3.3.2 AmpProtocol的实现

AmpProtocol中的实现主要是对ProtocolMeta描述,代码中已有详细注释,至于打包和解包,就是根据协议Write或者Read对应的数据类型即可

/// <summary>
/// Amp Protocol
/// </summary>
public class AmpProtocol : IProtocol<AmpMessage>
{
private readonly ISerializer _serializer; public AmpProtocol(ISerializer serializer)
{
this._serializer = serializer;
} static readonly ProtocolMeta AMP_PROTOCOL_META = new ProtocolMeta
{
InitialBytesToStrip = 0, //读取时需要跳过的字节数
LengthAdjustment = -5, //包实际长度的纠正,如果包长包括包头和包体,则要减去Length之前的部分
LengthFieldLength = 4, //长度字段的字节数 整型为4个字节
LengthFieldOffset = 1, //长度属性的起始(偏移)位
MaxFrameLength = int.MaxValue, //最大的数据包字节数
HeartbeatInterval = 30 * 1000 // 30秒没消息发一个心跳包
}; public ProtocolMeta GetProtocolMeta()
{
return AMP_PROTOCOL_META;
} public void Pack(IBufferWriter writer, AmpMessage message)
{
writer.WriteByte(message.Version);
writer.WriteInt(message.Length);
writer.WriteInt(message.Sequence);
writer.WriteByte((byte)message.InvokeMessageType); if (message.Version == 0)
{
writer.WriteUShort((ushort)message.ServiceId);
}
else
{
writer.WriteInt(message.ServiceId);
}
writer.WriteUShort(message.MessageId);
writer.WriteInt(message.Code);
if(message.Version == 1)
{
writer.WriteByte(_serializer.CodecType);
} if (message.Data != null)
{
writer.WriteBytes(message.Data);
}
} public AmpMessage Parse(IBufferReader reader)
{
if (reader.ReadableBytes == 0)
{
return null;
} var msg = new AmpMessage {Version = reader.ReadByte()}; int headLength;
if (msg.Version == 0 )
{
headLength = AmpMessage.VERSION_0_HEAD_LENGTH;
if (reader.ReadableBytes < AmpMessage.VERSION_0_HEAD_LENGTH - 1)
{
throw new RpcCodecException($"decode error ,ReadableBytes={reader.ReadableBytes+1},HEAD_LENGTH={AmpMessage.VERSION_0_HEAD_LENGTH}");
}
}
else if (msg.Version == 1 )
{
headLength = AmpMessage.VERSION_1_HEAD_LENGTH;
if (reader.ReadableBytes < AmpMessage.VERSION_1_HEAD_LENGTH - 1)
{
throw new RpcCodecException($"decode error ,ReadableBytes={reader.ReadableBytes+1},HEAD_LENGTH={AmpMessage.VERSION_1_HEAD_LENGTH}");
}
}
else
{
throw new RpcCodecException($"decode error ,{msg.Version} is not support");
} var length = reader.ReadInt();
msg.Sequence = reader.ReadInt();
var type = reader.ReadByte();
msg.InvokeMessageType = (InvokeMessageType)Enum.ToObject(typeof(InvokeMessageType), type); msg.ServiceId = msg.Version == 0 ? reader.ReadUShort() : reader.ReadInt(); msg.MessageId = reader.ReadUShort();
msg.Code = reader.ReadInt(); if (msg.Version == 1)
{
byte codeType = reader.ReadByte();
if (codeType != this._serializer.CodecType)
{
throw new RpcCodecException($"CodecType:{codeType} is not Match {this._serializer.CodecType}");
}
msg.CodecType = (CodecType)Enum.ToObject(typeof(CodecType), codeType);
}
else
{
msg.CodecType = CodecType.Protobuf;
} int left = length - headLength;
if (left > 0)
{
if (left > reader.ReadableBytes)
{
throw new RpcCodecException("message not long enough!");
}
msg.Data = new byte[left];
reader.ReadBytes(msg.Data);
}
return msg;
}
}
}

4. 后记

Peach的产生主要是源于对DotBPE的重构,因为在其他项目中有关于通讯的其他需求,所以这块虽然比较简单,也可比较独立,所以单独开了一个库来实现对DotNetty的封装。另外欢迎各位dotnet core的同学一起学习交流 QQ群号:699044833

[dotnet core]使用Peach简化Socket网络通讯协议开发的更多相关文章

  1. Socket网络通讯开发总结之:Java 与 C进行Socket通讯 + [备忘] Java和C之间的通讯

    Socket网络通讯开发总结之:Java 与 C进行Socket通讯 http://blog.sina.com.cn/s/blog_55934df80100i55l.html (2010-04-08 ...

  2. TCP/IP 中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议

    原文地址:http://hi.baidu.com/albyuyrgqgbbhoq/item/65006d2d002ab33195f62ba1 TCP/IP(Transmission Control P ...

  3. tcp/ip (网络通讯协议)

    介绍 TCP: 传输控制协议, IP: 网际协议, TCP/IP: 供已连接互联网的计算机之间进行通信的通信协议 在tcp/ip内部 , 包含一系列处理数据通信的协议: tcp.udp.icmp.dh ...

  4. 串口屏Modbus协议,串口屏的modbus协议资料,串口屏modbus通讯协议开发,串口屏之modbus协议使用技巧

    串口屏Modbus协议,串口屏的modbus协议资料,串口屏modbus通讯协议开发,串口屏之modbus协议使用技巧 本例程中用51单片机作为Modbus从机,从机的设备地址为2,从机有4个寄存器, ...

  5. 网络编程介绍,C/S 架构,网络通讯协议,osi七层

    网络编程: 什么是网络编程: 网络通常指的是计算机中的互联网,是由多台计算机通过网线或其他媒介相互链接组成的 编写基于网络的应用程序的过程序称之为网络编程 为什么要学习网络编程: 我们已经知道计算机, ...

  6. HslCommunication库的二次协议扩展,适配第三方通讯协议开发,基础框架支持长短连接模式

    本文将使用一个gitHub开源的项目来扩展实现二次协议的开发,该项目已经搭建好了基础层架构,并实现了三菱,西门子,欧姆龙,MODBUS-TCP的通讯示例,也可以参照这些示例开发其他的通讯协议,并Pul ...

  7. Socket网络通讯

    网络编程 使用C#进行网络编程时,通常都需要用到System.Net命名空间.System.Net.Sockets命名空间和System.Net.Mail命名空间: 1. System.Net命名空间 ...

  8. Socket网络通讯开发总结之:Java 与 C进行Socket通讯(转)

    先交待一下业务应用背景:服务端:移动交费系统:基于C语言的Unix系统客户端:增值服务系统:基于Java的软件系统通迅协议:采用TCP/IP协议,使用TCP以异步方式接入数据传输:基于Socket流的 ...

  9. [转] C#.Net Socket网络通讯编程总结

    1.理解socket1).Socket接口是TCP/IP网络的应用程序接口(API).Socket接口定义了许多函数和例程,程序员可以用它们来开发TCP/IP网络应用程序.Socket可以看成是网络通 ...

随机推荐

  1. NSLayoutConstraint 使用详解 VFL使用介绍

    注意 使用前必须先取消所有的你想设置View 的 Autoresizing 属性 因为 Autoresizing  Layout不能共存  系统默认是 Autoresizing for v in su ...

  2. input,select默认颜色修改

    input::-webkit-input-placeholder{color: #7f7f7f;} select{color: #7f7f7f} option{color: #7f7f7f;}

  3. NIO(一)

    1.NIO是什么? 是JDK1.4之后推出的一个新的IO操作(netty.mina通讯框架的底层都是NIO实现的连接) 2.NIO和IO的区别(阻塞只会出现在网络通讯中,都是同步) NIO:非阻塞类型 ...

  4. linux学习笔记:第二单元 UNIX和Linux操作系统概述

    第二单元 UNIX和Linux操作系统概述 UNIX是什么 UNIX操作系统的特点 UNIX 与Linux的关系 GNU项目与自由软件 GUN计划 自由软件意味着什么 Linux简介 Linux是什么 ...

  5. Cpython支持的进程与线程

    一.multiprocessing模块介绍 python中的多线程无法利用CPU资源,在python中大部分情况使用多进程.python中提供了非常好的多进程包multiprocessing. mul ...

  6. CentOS 7 部署 Spring Boot

    Spring Boot 内嵌了tomcat .我们可以将Boot打成 jar 包丢到服务器上运行才行. Spring Boot已经帮我们打理好了这一切,如果项目是继承自 spring-boot-sta ...

  7. java web----URL

     简单使用 import java.io.*; import java.net.HttpURLConnection; import java.net.MalformedURLException; im ...

  8. cf1110E 思维

    /* Ci'=Ci+1 + Ci-1 -Ci Ci+1 - Ci' = Ci - Ci-1 Ci' - Ci-1 = Ci+1 - Ci; 即求一次Ci’等价于交换Ci和Ci-1 与 Ci+1和Ci的 ...

  9. 饮冰三年-人工智能-Python-14Python基础之变量与函数

    1:函数:函数是逻辑结构化和过程化的一种编程方法.函数即变量 #参数组:**字典 *列表 def test(x,*args): print(args); print(args[0]); print(& ...

  10. 20165206 2017-2018-2 《Java程序设计》第6周学习总结

    20165206 2017-2018-2 <Java程序设计>第6周学习总结 教材学习内容总结 String类:可以被直接使用,不可以有子类. String对象:可以使用String类声明 ...