SuperSocket与Netty之实现protobuf协议,包括服务端和客户端

C#与Java跨平台通信

今天准备给大家介绍一个c#服务器框架(SuperSocket)和一个c#客户端框架(SuperSocket.ClientEngine)。这两个框架的作者是园区里面的江大渔。 首先感谢他的无私开源贡献。之所以要写这个文章是因为群里经常有人问这个客户端框架要如何使用。原因在于服务端框架的文档比较多,客户端的文档比较少,所以很多c#基础比较差的人就不懂怎么玩起来。今天就这里写一个例子希望能给部分人抛砖引玉吧。

参考资料:

SuperSocket文档 http://docs.supersocket.net/

我以前在开源中国的一部分文章:http://my.oschina.net/caipeiyu/blog

这篇文章选择 protobuf 来实现,选择protobuf是因为服务器有可能用的是java的netty,客户端想用SuperSocket.ClientEngine,而netty我看很多人经常用protobuf。


一、SuperSocket服务器

新建一个项目 ProtobufServer 然后添加 SuperSocket 和 protobuf 的依赖包。

添加protobuf依赖包 输入的搜索词是 Google.ProtocolBuffers
添加SuperSocket依赖包 输入搜索词是 SuperSocket,要添加两个SuperSocket.Engine 和 SuperSocket

上面的工作完成后,我们就应该来实现我们的传输协议了。传输协议打算参考netty的ProtobufVarint32FrameDecoder.java

* BEFORE DECODE (302 bytes)       AFTER DECODE (300 bytes)
* +--------+---------------+      +---------------+
* | Length | Protobuf Data |----->| Protobuf Data |
* | 0xAC02 |  (300 bytes)  |      |  (300 bytes)  |
* +--------+---------------+ +---------------+

Protobuf Data是protobuf的序列化结果。Length(Base 128 Varints)是表示Protobuf Data的长度。protobuf本身的序列号协议可以参考:https://developers.google.com/protocol-buffers/docs/encoding

我们先看一下SuperSocket的内置的常用协议实现模版看看有没有合适我们可以直接拿来用的。因为Length使用的是Base 128 Varints一种处理整数的变长二进制编码算法,所以呢内置的协议实现模板并不能直接拿来使用,所以我们只能自己来实现接口IRequestInfo和IReceiveFilter了,参考:使用 IRequestInfo 和 IReceiveFilter 等等其他对象来实现自定义协议

这里说明一下:为什么protobuf明明序列化成Protobuf Data 了为什么还要再加一个Length来打包,因为tcp这个流发送会参数粘包、分包,如果不加个协议来解析会读取错误的数据而导致无法反序列化 Protobuf Data (自行谷歌 tcp 粘包、分包)

ProtobufRequestInfo的实现

在实现ProtobufRequestInfo之前要先来考虑一个问题,那就是我们的传输协议是长度+protobuf数据,那么我们根本就无法知道获取到的protobuf数据该如何反序列化。在官方网站提供了一种解决思路:https://developers.google.com/protocol-buffers/docs/techniques#union
就是我们可以弄唯一个数据包,然后这个数据包里面必须包含一个枚举值,然后还包含了其他类型的数据包,每一个枚举值对应一个数据包,然后传送过来后,可以用分支判断来获取值。

那我们先设计一个 DefeatMessage.proto包含内容:

import "BackMessage.proto";
import "CallMessage.proto";

message DefeatMessage { enum Type { CallMessage = 1; BackMessage = 2; } required Type type = 1; optional CallMessage callMessage = 2; optional BackMessage backMessage = 3; }

然后再把CallMessage和BackMessage补全

message BackMessage {

    optional string content = 1;

}

message CallMessage {

    optional string content = 1; }

然后在我们的路径packages\Google.ProtocolBuffers.2.4.1.555\tools里面有两个工具protoc.exe 和 protogen.exe,我们可以执行下面的命令来生成我们的c#代码

protoc --descriptor_set_out=DefeatMessage.protobin --proto_path=pack --include_imports pack\DefeatMessage.proto

protogen DefeatMessage.protobin

注意路径要自己修改

如果有报Expected top-level statement (e.g. "message").这么一个错误,那就是你cmd的编码和proto的编码不一致,要改成一致。

相关文件:https://github.com/kotcmm/SuperSocket.ClientEngine.QuickStart/tree/master/ProtobufServer/Pack

生成完c#代码后,我们就要设计ProtobufRequestInfo了。这个比较简单,只要实现IRequestInfo接口。我们这里在实现接口带的属性外另加一个 DefeatMessage 和 DefeatMessage.Types.Type,其中DefeatMessage是为了存储我们解包完数据后反序列化出来的对象,Type是为了方便区分我们应该取出DefeatMessage里面的哪个值。

using System;
using SuperSocket.SocketBase.Protocol;

namespace ProtobufServer
{
    public class ProtobufRequestInfo : IRequestInfo { public string Key { get; private set; } public DefeatMessage.Types.Type Type { get; private set; } public DefeatMessage Body { get; private set; } public ProtobufRequestInfo (DefeatMessage.Types.Type type, DefeatMessage body) { Type = type; Key = type.ToString(); Body = body; } } }
ProtobufReceiveFilter的实现

代码比较长,直接看在github上的代码ProtobufReceiveFilter的实现
实现的注意点参考:使用 IRequestInfo 和 IReceiveFilter 等等其他对象来实现自定义协议。主要是对ss里面给我们缓存的数据流进行协议解析。

  1. readBuffer: 接收缓冲区, 接收到的数据存放在此数组里
  2. offset: 接收到的数据在接收缓冲区的起始位置
  3. length: 本轮接收到的数据的长度
  4. toBeCopied: 表示当你想缓存接收到的数据时,是否需要为接收到的数据重新创建一个备份而不是直接使用接收缓冲区
  5. rest: 这是一个输出参数, 它应该被设置为当解析到一个为正的请求后,接收缓冲区还剩余多少数据未被解析

    public  ProtobufRequestInfo Filter (byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest) { rest = 0; var readOffset = offset - m_OffsetDelta;//我们重新计算缓存区的起始位置,这里要说明的是如果前一次解析还有剩下没有解析到的数据,那么就需要把起始位置移到之前最后要解析的那个位置 CodedInputStream stream = CodedInputStream.CreateInstance (readBuffer, readOffset, length);//这个类是Google.ProtocolBuffers提供的 var varint32 = (int)stream.ReadRawVarint32 ();//这里是计算我们这个数据包是有多长(不包含length本身) if(varint32 <= 0) return null; var headLen = (int) stream.Position - readOffset;//计算协议里面length占用几位 rest = length - varint32 - headLen + m_ParsedLength;//本次解析完后缓存区还剩下多少数据没有解析 if (rest >= 0)//缓存区里面的数据足够本次解析 { byte[] body = stream.ReadRawBytes(varint32); DefeatMessage message = DefeatMessage.ParseFrom(body); var requestInfo = new ProtobufRequestInfo(message.Type,message); InternalReset(); return requestInfo; } else//缓存区里面的数据不够本次解析,(名词为分包) { m_ParsedLength += length; m_OffsetDelta = m_ParsedLength; rest = 0; var expectedOffset = offset + length; var newOffset = m_OrigOffset + m_OffsetDelta; if (newOffset < expectedOffset) { Buffer.BlockCopy(readBuffer, offset - m_ParsedLength + length, readBuffer, m_OrigOffset, m_ParsedLength); } return null; } }
ProtobufAppSession 的实现
using System;
using SuperSocket.SocketBase;

namespace ProtobufServer
{
    public class ProtobufAppSession : AppSession<ProtobufAppSession,ProtobufRequestInfo> { public ProtobufAppSession () { } } }
ProtobufAppServer 的实现
using System;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Protocol;

namespace ProtobufServer
{
    public class ProtobufAppServer : AppServer<ProtobufAppSession,ProtobufRequestInfo>
    {
        public ProtobufAppServer ()
            :base(new DefaultReceiveFilterFactory< ProtobufReceiveFilter, ProtobufRequestInfo >()) { } } }
服务器的实例启动实现

参考:http://docs.supersocket.net/v1-6/zh-CN/A-Telnet-Example

代码:https://github.com/kotcmm/SuperSocket.ClientEngine.QuickStart/blob/master/ProtobufServer/Program.cs

主要是接收到数据的一个方法实现,当然ss里面还带了命令模式的实现,不过这个不在本文章里面说。这里的实现了接收到不同的数据给打印出来,然后接收到CallMessage数据的话就给客户端回发一条信息

private static void AppServerOnNewRequestReceived(ProtobufAppSession session, ProtobufRequestInfo requestInfo) { switch (requestInfo.Type) { case DefeatMessage.Types.Type.BackMessage: Console.WriteLine("BackMessage:{0}", requestInfo.Body.BackMessage.Content); break; case DefeatMessage.Types.Type.CallMessage: Console.WriteLine("CallMessage:{0}", requestInfo.Body.CallMessage.Content); var backMessage = BackMessage.CreateBuilder() .SetContent("Hello I am form C# server by SuperSocket").Build(); var message = DefeatMessage.CreateBuilder() .SetType(DefeatMessage.Types.Type.BackMessage) .SetBackMessage(backMessage).Build(); using (var stream = new MemoryStream()) { CodedOutputStream os = CodedOutputStream.CreateInstance(stream); os.WriteMessageNoTag(message); os.Flush(); byte[] data = stream.ToArray(); session.Send(new ArraySegment<byte>(data)); } break; } }

服务器的代码就到这里,可以编译运行起来看看有无错误。

二、SuperSocket.ClientEngine客户端

与服务器实现相同,先通过NuGet添加 SuperSocket.ClientEngine 和 protobuf 的依赖包。
有三个实现:

ProtobufPackageInfo的实现

把前面实现服务器时候生成的通讯数据包拷贝过来,然后和实现服务器的ProtobufRequestInfo一样,只不过这里只是实现接口IPackageInfo而已

using SuperSocket.SocketBase.Protocol;using SuperSocket.ProtoBase;

namespace ProtobufClient
{
    public class ProtobufPackageInfo : IPackageInfo { public ProtobufPackageInfo(DefeatMessage.Types.Type type, DefeatMessage body) { Type = type; Key = type.ToString(); Body = body; } public string Key { get; private set; } public DefeatMessage Body { get; private set; } public DefeatMessage.Types.Type Type { get; private set; } } }
ProtobufReceiveFilter的实现

代码:https://github.com/kotcmm/SuperSocket.ClientEngine.QuickStart/blob/master/ProtobufClient/ProtobufReceiveFilter.cs
这里的数据解析的实现与服务器的实现有点不同,不过下一个版本可能会统一,如果统一起来的话,那么以后数据解析就可以做成和插件一样,同时可以给服务器和客户端使用。

  1. data:也是数据缓存区
  2. rest:缓存区还剩下多少

这个实现与服务器的不同就在BufferList本身就已经有处理分包,就不需要我们自己再做处理。

public ProtobufPackageInfo Filter(BufferList data, out int rest) { rest = 0; var buffStream = new BufferStream(); buffStream.Initialize(data); var stream = CodedInputStream.CreateInstance(buffStream); var varint32 = (int) stream.ReadRawVarint32(); if (varint32 <= 0) return default(ProtobufPackageInfo); var total = data.Total; var packageLen = varint32 + (int) stream.Position; if (total >= packageLen) { rest = total - packageLen; var body = stream.ReadRawBytes(varint32); var message = DefeatMessage.ParseFrom(body); var requestInfo = new ProtobufPackageInfo(message.Type, message); return requestInfo; } return default(ProtobufPackageInfo); }
运行主程序实现

具体实现看:https://github.com/kotcmm/SuperSocket.ClientEngine.QuickStart/blob/master/ProtobufClient/Program.cs

这个真的没有什么好说了。运行效果如下:

这里的打印信息是相对比较简单,大家可以自己下载源码来加些打印数据,让看起来更好看点。

三、java的Netty实现

既然前面提到了Netty,那就顺便实现一个简单的服务器来通讯看看。
使用的是Netty 4.x 参考资料:http://netty.io/wiki/user-guide-for-4.x.html

Netty的实现在网络上有超级多的例子,这里就简单的介绍一下就可以,首先先生成java的通讯包代码

protoc --proto_path=pack --java_out=./ pack/DefeatMessage.proto

protoc --proto_path=pack --java_out=./ pack/BackMessage.proto

protoc --proto_path=pack --java_out=./ pack/CallMessage.proto

这几个命令要自己灵活改变们不要死死的硬搬。

生成的代码:https://github.com/kotcmm/SuperSocket.ClientEngine.QuickStart/tree/master/java/NettyProtobufServer/src/main/java

然后创建一个maven项目,添加Netty 和 protobuf 依赖:https://github.com/kotcmm/SuperSocket.ClientEngine.QuickStart/blob/master/java/NettyProtobufServer/pom.xml

<dependencies>
        <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-microbench</artifactId> <version>4.1.0.Final</version> </dependency> </dependencies>
ProtobufServerHandler的实现

因为Netty里面已经有帮我们实现了protobuf的解析,所以我们不需要自己实现。我们只要继承ChannelInboundHandlerAdapter然后通过channelRead就可以拿到解析好的对象,然后转换成我们自己的类型,就可以直接使用。这里同样是实现不同类型的消息打印和CallMessage消息就回复信息给客户端。

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

/**
* Created by caipeiyu on 16/6/4.
*/
public class ProtobufServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2) try { DefeatMessageOuterClass.DefeatMessage in = (DefeatMessageOuterClass.DefeatMessage) msg; if(in.getType() == DefeatMessageOuterClass.DefeatMessage.Type.BackMessage){ System.out.print("BackMessage:"); System.out.print(in.getBackMessage()); System.out.flush(); }else if(in.getType() == DefeatMessageOuterClass.DefeatMessage.Type.CallMessage){ System.out.print("CallMessage:"); System.out.print(in.getCallMessage()); System.out.flush(); DefeatMessageOuterClass.DefeatMessage out = DefeatMessageOuterClass.DefeatMessage.newBuilder() .setType(DefeatMessageOuterClass.DefeatMessage.Type.BackMessage) .setBackMessage(BackMessageOuterClass.BackMessage .newBuilder().setContent("Hello I from server by Java Netty").build()) .build(); ctx.write(out); ctx.flush(); } } finally { ReferenceCountUtil.release(msg); // (2) } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4) // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); } }
ProtobufServer的实现

主要是添加已经有的编码解码和消息接收的类就可以了。

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.protobuf.ProtobufDecoder; import io.netty.handler.codec.protobuf.ProtobufEncoder; import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; /** * Created by caipeiyu on 16/6/4. */ public class ProtobufServer { public static void main(String[] args) { EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // (2) b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // (3) .childHandler(new ChannelInitializer<SocketChannel>() { // (4) @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); //解码 p.addLast("frameDecoder", new ProtobufVarint32FrameDecoder()); //构造函数传递要解码成的类型 p.addLast("protobufDecoder", new ProtobufDecoder(DefeatMessageOuterClass.DefeatMessage.getDefaultInstance())); //编码 p.addLast("frameEncoder", new ProtobufVarint32LengthFieldPrepender()); p.addLast("protobufEncoder", new ProtobufEncoder()); //业务逻辑处理 p.addLast("handler", new ProtobufServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) // (5) .childOption(ChannelOption.SO_KEEPALIVE, true); // (6) // Bind and start to accept incoming connections. ChannelFuture f = b.bind(2012).sync(); // (7) // Wait until the server socket is closed. // In this example, this does not happen, but you can do that to gracefully // shut down your server. f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }

整个代码编写完成后,直接运行并打开我们前面的客户端进通讯发数据。运行结果如下:

当然这三个例子直接简单的说明如何使用框架来解决我们的问题,实际开发过程中肯定不只是我们例子的这么点东西,需要考虑的东西还很多,这里只是写一些可以运行起来的例子作为抛砖引玉。希望能给不懂的同学有点启发作用。谢谢您百忙中抽出时间来观看我的分享。


由于本人水平有限,知识有限,文章难免会有错误,欢迎大家指正。如果有什么问题也欢迎大家回复交流。要是你觉得本文还可以,那么点击一下推荐。

<think>嗯,用户想了解CRC32校验算法的实现和使用方法。我需要先回顾一下CRC32的基本原理,然后考虑如何用简单易懂的中文解释清楚。首先,CRC32是基于循环冗余校验的一种算法,主要用于检测数据传输或存储中的错误。用户可能希望知道它的具体步骤,比如如何初始化、处理数据、异或操作以及最终得到校验值。 根据提供的引用资料,CRC32的核心是多项式除法,但实际中通过查表法优化速度。需要解释生成多项式,比如常见的0xEDB88320,以及初始值、输入输出是否反转这些参数。用户提供的代码片段中有两个版本,一个是逐位计算,另一个是查表法,这可能需要在回答中对比说明,突出查表法的效率优势。 接下来,用户可能关心具体怎么使用这个算法,比如在C语言中的实现步骤,或者如何应用到实际项目中,比如文件校验或网络传输。要分步骤讲解,从初始化到处理每个字节,再到最终计算,可能需要示例代码来辅助说明。同时,注意用户要求行内数学表达式用$...$,比如多项式应该写成$0xEDB88320$这样的格式。 另外,用户的问题里提到生成相关问题,我需要考虑相关的问题可能涉及性能优化、不同生成多项式的影响,或者CRC32与其他校验算法的比较。这些可以作为相关问题放在最后,帮助用户进一步探索。 需要确保回答结构清晰,先讲原理,再讲实现步骤,然后使用方法,最后附上相关问题。同时,引用提供的资料,比如引用[^1]和[^2]来支持解释。要避免技术术语过于复杂,保持语言通俗易懂,同时满足LaTeX格式要求,比如多项式用$符号包裹。检查是否有遗漏的步骤,比如输入反转和输出反转的处理,这在代码示例中有所体现,需要解释清楚这些参数的作用。</think>### CRC32校验算法实现及使用方法 #### 一、CRC32基本原理 CRC32通过多项式除法生成32位校验值,用于检测数据传输或存储中的错误。其核心是生成多项式$G(x)$,常用标准为$0xEDB88320$(反向多项式形式)[^1]。算法流程如下: 1. **初始化**:设置32位寄存器初值为$0xFFFFFFFF$ 2. **数据输入**:逐字节与寄存器异或运算 3. **位移处理**:根据多项式进行循环移位和异或操作 4. **结果取反**:最终值与$0xFFFFFFFF$异或 #### 二、算法实现(C语言) ```c // 查表法实现(高效) uint32_t crc32_table[256]; void init_crc32_table() { for (int i = 0; i < 256; i++) { uint32_t crc = i; for (int j = 0; j < 8; j++) { crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320 : 0); } crc32_table[i] = crc; } } uint32_t calc_crc32(uint8_t *data, size_t len) { uint32_t crc = 0xFFFFFFFF; while (len--) { crc = (crc >> 8) ^ crc32_table[(crc ^ *data++) & 0xFF]; } return crc ^ 0xFFFFFFFF; } ``` 此实现通过预先生成的256元素查找表,将时间复杂度从$O(n \cdot 8)$优化到$O(n)$。 #### 三、使用场景及方法 1. **文件校验**:计算文件CRC32值验证完整性 ```bash $ crc32 filename.txt ``` 2. **网络传输**:在数据帧尾部附加4字节校验码 3. **存储系统**:RAID、ZFS等文件系统使用CRC32校验数据块 #### 四、参数配置 | 参数 | 说明 | 标准值 | |---------------|---------------------------|-----------------| | 初始值 | 寄存器初始状态 | 0xFFFFFFFF | | 多项式 | 生成多项式 | 0xEDB88320 | | 输入/输出反转 | 字节处理顺序 | 通常需要反转 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值