使用Akka的远程调用
概述
正如其它RPC或者RMI框架那样,Akka也提供了远程调用的能力。服务端在监听的端口上接收客户端的调用。本文将在《Spring与Akka的集成》一文的基础上介绍Akka的remote调用,本文很多代码和例子来源于Akka官网的代码示例,也包含了一些适用于Spring集成的改造,本文旨在介绍Akka的远程调用的开发过程。
服务端开发
配置
Akka的默认配置文件为application.conf,如果不特别指明,Akka System都会默认加载此配置。如果你想自定义符合你习惯的名字,可以使用如下代码:
1 | final ActorSystem system = ActorSystem.create( "YourSystem" , ConfigFactory.load( "yourconf" )); |
上述代码中的yourconf不包含文件后缀名,在你的资源路径下实际是yourconf.conf。
我不太想自定义加载的配置文件,而是继续使用application.conf,这里先列出其配置:
1 2 3 4 5 6 | include "common" akka { # LISTEN on tcp port 2552 remote.netty.tcp.port = 2552 } |
这里的remote.netty.tcp.port配置属性表示使用Netty框架在TCP层的监听端口是2552。include与java里的import或者jsp页面中的include标签的作用类似,表示引用其他配置文件中的配置。由于common.conf中包含了Akka的一些公共配置,所以可以这样引用,common.conf的配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | akka { actor { provider = "akka.remote.RemoteActorRefProvider" } remote { netty.tcp { hostname = "127.0.0.1" } } } |
common配置中的provider属性表示Actor的引用提供者是akka.remote.RemoteActorRefProvider,即远程ActorRef的提供者。这里的hostname属性表示服务器的主机名。从common配置我们还可以看出Akka的配置有点类似于json,也是一种嵌套结构。此外,Akka还可以采用一种扁平的配置方式,例如:
1 2 | akka.actor.provider = "..." akka.remote.netty.tcp.hostname = "127.0.0.1" |
它们所代表的作用是一样的。至于选择扁平还是嵌套的,一方面依据你的个人习惯,一方面依据配置的多寡——随着配置项的增多,你会发现嵌套会让你的配置文件更加清晰。
服务端
由于官网的例子比较简洁并能说明问题,所以本文对Akka官网的例子进行了一些改造来介绍服务端与客户端之间的远程调用。服务端的配置已在上一小节列出,本小节着重介绍服务端的实现。
我们的服务端是一个简单的提供基本的加、减、乘、除的服务的CalculatorActor,这些运算都直接封装在CalculatorActor的实现中(在实际的业务场景中,Actor应该只接收、回复及调用具体的业务接口,这里的加减乘除运算应当由指定的Service接口实现,特别是在J2EE或者与Spring集成后),CalculatorActor的实现见代码清单1。
代码清单1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | @Named ( "CalculatorActor" ) @Scope ( "prototype" ) public class CalculatorActor extends UntypedActor { private static Logger logger = LoggerFactory.getLogger(CalculatorActor. class ); @Override public void onReceive(Object message) { if (message instanceof Op.Add) { Op.Add add = (Op.Add) message; logger.info( "Calculating " + add.getN1() + " + " + add.getN2()); Op.AddResult result = new Op.AddResult(add.getN1(), add.getN2(), add.getN1() + add.getN2()); getSender().tell(result, getSelf()); } else if (message instanceof Op.Subtract) { Op.Subtract subtract = (Op.Subtract) message; logger.info( "Calculating " + subtract.getN1() + " - " + subtract.getN2()); Op.SubtractResult result = new Op.SubtractResult(subtract.getN1(), subtract.getN2(), subtract.getN1() - subtract.getN2()); getSender().tell(result, getSelf()); } else if (message instanceof Op.Multiply) { Op.Multiply multiply = (Op.Multiply) message; logger.info( "Calculating " + multiply.getN1() + " * " + multiply.getN2()); Op.MultiplicationResult result = new Op.MultiplicationResult(multiply.getN1(), multiply.getN2(), multiply.getN1() * multiply.getN2()); getSender().tell(result, getSelf()); } else if (message instanceof Op.Divide) { Op.Divide divide = (Op.Divide) message; logger.info( "Calculating " + divide.getN1() + " / " + divide.getN2()); Op.DivisionResult result = new Op.DivisionResult(divide.getN1(), divide.getN2(), divide.getN1() / divide.getN2()); getSender().tell(result, getSelf()); } else { unhandled(message); } } } |
Add、Subtract、Multiply、Divide都继承自MathOp,这里只列出MathOp和Add的实现,见代码清单2所示。
代码清单2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public interface MathOp extends Serializable { } public static class Add implements MathOp { private static final long serialVersionUID = 1L; private final int n1; private final int n2; public Add( int n1, int n2) { this .n1 = n1; this .n2 = n2; } public int getN1() { return n1; } public int getN2() { return n2; } } |
服务端应当启动CalculatorActor实例,以提供服务,代码如下:
1 2 3 4 | logger.info( "Start calculator" ); final ActorRef calculator = actorSystem.actorOf(springExt.props( "CalculatorActor" ), "calculator" ); actorMap.put( "calculator" , calculator); logger.info( "Started calculator" ); |
客户端
客户端调用远程CalculatorActor提供的服务后,还要接收其回复信息,因此也需要监听端口。客户端和服务端如果在同一台机器节点上,那么客户端的监听端口不能与服务端冲突,我给出的配置示例如下:
1 2 3 4 5 | include "common" akka { remote.netty.tcp.port = 2553 } |
客户端通过远程Actor的路径获得ActorSelection,然后向远程的Akka System获取远程CalculatorActor的ActorRef,进而通过此引用使用远端CalculatorActor提供的服务。在详细的说明实现细节之前,先来看看LookupActor的实现,见代码清单3所示。
代码清单3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | @Named ( "LookupActor" ) @Scope ( "prototype" ) public class LookupActor extends UntypedActor { private static Logger logger = LoggerFactory.getLogger(LookupActor. class ); private final String path; private ActorRef calculator = null ; public LookupActor(String path) { this .path = path; sendIdentifyRequest(); } private void sendIdentifyRequest() { getContext().actorSelection(path).tell( new Identify(path), getSelf()); getContext().system().scheduler().scheduleOnce(Duration.create( 3 , SECONDS), getSelf(), ReceiveTimeout.getInstance(), getContext().dispatcher(), getSelf()); } @Override public void onReceive(Object message) throws Exception { if (message instanceof ActorIdentity) { calculator = ((ActorIdentity) message).getRef(); if (calculator == null ) { logger.info( "Remote actor not available: " + path); } else { getContext().watch(calculator); getContext().become(active, true ); } } else if (message instanceof ReceiveTimeout) { sendIdentifyRequest(); } else { logger.info( "Not ready yet" ); } } Procedure<Object> active = new Procedure<Object>() { @Override public void apply(Object message) { if (message instanceof Op.MathOp) { // send message to server actor calculator.tell(message, getSelf()); } else if (message instanceof Op.AddResult) { Op.AddResult result = (Op.AddResult) message; logger.info(String.format( "Add result: %d + %d = %d\n" , result.getN1(), result.getN2(), result.getResult())); ActorRef sender = getSender(); logger.info( "Sender is: " + sender); } else if (message instanceof Op.SubtractResult) { Op.SubtractResult result = (Op.SubtractResult) message; logger.info(String.format( "Sub result: %d - %d = %d\n" , result.getN1(), result.getN2(), result.getResult())); ActorRef sender = getSender(); logger.info( "Sender is: " + sender); } else if (message instanceof Terminated) { logger.info( "Calculator terminated" ); sendIdentifyRequest(); getContext().unbecome(); } else if (message instanceof ReceiveTimeout) { // ignore } else { unhandled(message); } } }; } |
LookupActor的构造器需要传递远端CalculatorActor的路径,并且调用了sendIdentifyRequest方法,sendIdentifyRequest的作用有两个:
- 通过向ActorSelection向远端的Akka System发送Identify消息,并获取远程CalculatorActor的ActorRef;
- 启动定时调度,3秒后向CalculatorActor的执行上下文发送ReceiveTimeout消息,而LookupActor处理ReceiveTimeout消息时,再次调用了sendIdentifyRequest方法。
- 如果收到MathOp的消息,说明是加减乘除的消息,则将消息进一步告知远端的CalculatorActor并由其进行处理;
- 如果收到AddResult或者SubtractResult,这说明CalculatorActor已经处理完了加或者减的处理,并回复了处理结果,因此对计算结果进行使用(本例只是简单的打印);
- 如果收到了Terminated消息,说明远端的CalculatorActor停止或者重启了,因此需要重新调用sendIdentifyRequest获取最新的CalculatorActor的ActorRef。最后还需要取消active,恢复为默认接收消息的状态;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | logger.info( "start lookup" ); final String path = "akka.tcp://metadataAkkaSystem@127.0.0.1:2552/user/calculator" ; final ActorRef lookup = actorSystem.actorOf(springExt.props( "LookupActor" , path), "lookup" ); final Random r = new Random(); actorSystem.scheduler().schedule(Duration.create( 1 , SECONDS), Duration.create( 1 , SECONDS), new Runnable() { @Override public void run() { if (r.nextInt( 100 ) % 2 == 0 ) { lookup.tell( new Op.Add(r.nextInt( 100 ), r.nextInt( 100 )), null ); } else { lookup.tell( new Op.Subtract(r.nextInt( 100 ), r.nextInt( 100 )), null ); } } }, actorSystem.dispatcher()); |
Actor远端调用模型

运行结果
我的客户端和服务端都运行于本地,客户端tcp监听端口是2553,服务端监听端口是2552,由于本例子的代码较为健壮,所以客户端、服务端可以以任意顺序启动。客户端运行后的日志如下图所示:
服务端的运行日志如下图所示:
总结
Akka的远端调用是大家在使用时最常用的特性之一,掌握起来不是什么难事,如何实现处理多种消息,并考虑其稳定性、健壮性是需要详细考虑的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?