java实现akka协议对接c#版本实现

Akka

Akka是一个用于构建高性能、分布式和容错系统的工具包及运行时环境,特别适合于Java和Scala开发者在JVM平台上开发高并发应用。

它采用Actor模型来简化并发编程,Actor作为最小的计算单元,通过异步消息传递进行通信,从而有效地解决了传统并发问题。

Akka还提供了丰富的容错机制和位置透明性,使得构建可伸缩的分布式系统更为便捷。此外,Akka还包括其他模块,如Akka Streams用于处理数据流,并支持反压机制,增强了系统在处理大量数据时的稳定性和效率。

akka本身是用scala编写的库,可以运行在JVM上。在c#中,用的是c#版本的akka,github地址如下:

背景

由于现实情况限制,公司现有某项目的部分模块采用了C#开发,并通过Akka进行了进程间通信。现计划对选定模块进行重构,以Java语言替代C#实现。

为了保持重构后的模块与现有系统兼容,新Java项目需作为被调用的服务端,遵循Akka协议来确保与原有C#调用方的接口无缝对接。尽管全面转向Java是一种理想方案,但由于多种复杂的现实因素,暂时无法对此整个项目进行彻底的语言迁移和技术栈统一。

因此,当前任务的核心在于,在Java环境中建立一个遵循Akka规范的服务端,以便于与遗留的C#客户端进行有效的通信交互。

初探

我把c#的akka源码下载下来,查阅相关文档并基于这个demo进行调试.

在c#版本akka的文档,关于序列化部分,描述了用到的所有序列化器:

我使用demo调试中,发现请求与响应对象的序列化/反序列化只用到了NewtonSoftJsonSerializer ,这让我一开始以为事情的解决如此简单,我只需要在服务端以字符串接收并解析json即可。

事实并非如此简单,Akka Remote类的序列化器都可能用到,我们的项目中,框架把几种序列化器都用了,除了json这个。

再探

我开始深入debug c#版本的源码,同时把scala版本源码也下载下来进行调试分析。

scala版本必须是最远古的那几个版本。

两个版本的实现非常接近,c#的通信框架基于dot.netty实现,scala版本基于netty 3.x的版本实现。

通过源码分析整个实现流程之后,我对协议的实现有了大致思路。

基于netty实现,流的编码采用LengthFieldPrepender,解码采用LengthFieldBasedFrameDecoder,但是需要注意,c#的实现使用小端网络字节序,而Java默认实现是大端网络字节序,因此这个地方实现的时候必须指定小端。

难解

我基于java实现一个简易版的服务端,尝试接收demo中c#客户端的请求并在打印日志后给予响应。

结果,生活总是没有想象那么美好。

akka协议的请求分为两种:

  • 指令,控制类消息
  • 消息

消息又分为下面两种:

  • 系统消息(akka自身)
  • 业务消息(我们发的)

所以在编写解码器的时候,必须考虑指令类消息的解码:

java 复制代码
private AkkaPduCodec.AkkaPdu decodeInstruction(final WireFormats.AkkaControlMessage message) {

        switch (message.getCommandType()) {
            case ASSOCIATE -> {
                if (message.hasHandshakeInfo()) {
                    WireFormats.AkkaHandshakeInfo handshakeInfo = message.getHandshakeInfo();

                    String cookie = null;
                    if (handshakeInfo.hasCookie()) {
                        cookie = handshakeInfo.getCookie();
                    }
                    WireFormats.AddressData origin = handshakeInfo.getOrigin();
                    Address address = new Address(origin.getProtocol(), origin.getSystem(), origin.getHostname(), origin.getPort());
                    HandshakeInfo info = new HandshakeInfo(address, (int) handshakeInfo.getUid(), Option.apply(cookie));
                    return new AkkaPduCodec.Associate(info);
                }
            }
            case DISASSOCIATE -> {
                return new AkkaPduCodec.Disassociate(AssociationHandle.Unknown$.MODULE$);
            }

            case DISASSOCIATE_SHUTTING_DOWN -> {
                return new AkkaPduCodec.Disassociate(AssociationHandle.Shutdown$.MODULE$);
            }
            case DISASSOCIATE_QUARANTINED -> {
                return new AkkaPduCodec.Disassociate(AssociationHandle.Quarantined$.MODULE$);
            }
            case HEARTBEAT -> {
                return AkkaPduCodec.Heartbeat$.MODULE$;
            }
            default -> log.error("Unknown:{}", message);
        }
        return null;
}

这里面用到的一些类,实际是我引用了scala版本的akka依赖。

注意,akka通信也采用了protobuf的序列化,而如何判断是否是指令类型还是其它消息类型,我的实现也很简单:

java 复制代码
        WireFormats.AkkaProtocolMessage message = WireFormats.AkkaProtocolMessage.parseFrom(bytes);

        if (message.hasPayload()) {
            AkkaPduCodec.Payload payload = new AkkaPduCodec.Payload(ByteString.
                    fromByteBuffer(message.getPayload().asReadOnlyByteBuffer()));
            out.add(payload);
        } else if (message.hasInstruction()) {
            AkkaPduCodec.AkkaPdu akkaPdu = decodeInstruction(message.getInstruction());
            out.add(akkaPdu);
        } else {
            log.error("Unknown:{}", message);
        }

我以为问题如此简单,便兴致勃勃的进行了实现,和本地的c#的客户端联调也通过了。

当我和实际业务系统进行联调的时候,才发现并不简单,业务系统也并不是使用的如此简单,消息的序列化也并不是json,并且出现了其它我无法解析的消息格式。

把我之前的认知和推断推翻了。好难。

好难解

我对业务系统的配置和源码进行了分析,发现还使用了Remote DeathWatch:

同时进行了抓包分析并结合源码,查询这个命令的请求情况,发现根据配置的间隔定时请求(还是有不少额外开销的)。

通过源码分析,这个请求客户端发送到服务端,服务端本身的实现是进行相关监听器的注册处理,但是不需要给予客户端相关响应。这样我实现起来就方便多了,只管接收这个请求,不需要响应。

但是比较麻烦的是,不同的请求用的反序列化器,不一样。我本身以为只是固定的一两个,结果发现不是。我差点懵了,实现这一两个已经耗费不少精力,如果要考虑所有情况把所有的都实现一遍,成本太高。

完美解决

仔细核对c#和scala版本akka的序列化及网络传输部分的实现,包括使用的protobuf定义。

然后把c#版本的定义拿过来在java里调整过后,重新实现,在不懈努力下,重新写出了一个通用的协议的序列化与反序列化版本,下面是一些debug看到的格式示例:

swift 复制代码
envelope {
  recipient {
    path: "akka.tcp://server@localhost:4567/"
  }
  message {
    message: "\n\203\001\nM\b\002\020\b\032Gakka.tcp://client@localhost:53151/system/inbox-1#149231235\020\002\0320AkkaDemo.Proto.Request, AkkaDemo\022\b\b\002\022\004user\022\022\b\002\022\016DispatchServer"
    serializerId: 6
  }
  sender {
    path: "akka.tcp://client@localhost:53151/user/dispatch-client-8-0#1086482885"
  }
  seq: 18446744073709551615
}


ack {
  cumulativeAck: 1
}
envelope {
  recipient {
    path: "akka.tcp://server@localhost:4567/"
  }
  message {
    message: "\n\b\020\020\032\004RWHB\022\n\b\002\022\006system\022\022\b\002\022\016remote-watcher"
    serializerId: 6
  }
  sender {
    path: "akka.tcp://client@localhost:58542/system/remote-watcher#2129229977"
  }
  seq: 18446744073709551615
}

最终,终于成功实现了这个服务端

其它方案

实现的时候,也考虑过是否能直接用scala版本的实现直接对接c#版本,最终发现两个问题,让我放弃了这个方案:

  1. 上面示例的包格式里c#版本的带有一个命名空间,是自动生成的,scala实现应该是包路径,两边完全对不上。

  2. 远古版本用的是3.x的netty,项目里有4.x的netty,版本冲突,如果想用4.x的,我需要把scala实现的通信协议改成4.x的netty重新打包,不了解sbt这个工具(scala用的这个),我感觉了解这个的时间我已经写完了。新版本的实现已经不用netty了,用的另一套,基于udp协议的,所以和c#版本的更难对接了。

总之,直接对接的方案,改动scala实现并重新打包的成本,大于我直接实现的成本。所以考虑之后就放弃了。

相关推荐
向前看-1 小时前
验证码机制
前端·后端
超爱吃士力架2 小时前
邀请逻辑
java·linux·后端
AskHarries4 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion5 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp6 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder6 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚7 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心8 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴8 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲8 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端