本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。
我们使用 Netty 来接收客户端连接,然后再转发给 Akka 处理。
Netty 简介
Netty 是一个高性能、异步事件驱动的 网络框架,基于 Java 实现,广泛用于开发高并发、高性能的网络应用程序(如服务器、客户端)。它封装了 Java 的原生网络 API(如 NIO 和 Epoll),简化了网络编程的复杂性。
Netty 被用于实现通信协议(如 HTTP、WebSocket)、RPC 框架(如 gRPC、Dubbo)等。
Netty 的核心特点
-
高性能:
- 基于 Java NIO(非阻塞 IO)。
- 充分利用多核 CPU,提供良好的并发性能。
- 零拷贝支持(如直接内存、堆外内存等)。
-
易用性:
- 提供了丰富的抽象和封装,隐藏了底层的复杂性(如缓冲区管理、选择器)。
- 简化了网络应用的开发流程,减少了样板代码。
-
事件驱动模型:
- 基于 Reactor 模式,通过事件循环处理 I/O 事件,实现异步和高效的处理。
-
可扩展性:
- 通过 ChannelHandler 链式设计,轻松实现协议解析、数据处理等功能。
-
跨平台支持:
- 支持 Epoll(Linux)和 KQueue(macOS)等操作系统的优化。
-
多种协议支持:
- 内置支持多种协议,如 HTTP、WebSocket、FTP、TLS 等,也可自定义协议。
将网络连接关联到 Actor
我们会建立一个 ChannelActor
,用来作为 Netty 和 Akka 的桥梁,当有新的连接进来时,我们就会启动一个 ChannelActor
,然后让它持有 ChannelHandlerContext
,之后就可以在 Actor 中向客户端写入消息了。
class ChannelActor(node: GateNode, private val handlerContext: ChannelHandlerContext) : StatefulActor<GateNode>(node) {
companion object {
val MaxIdleDuration = 1.minutes
fun props(node: GateNode, handlerContext: ChannelHandlerContext): Props =
Props.create(ChannelActor::class.java, node, handlerContext)
}
}
添加一个 ChannelHandler
,继承 ChannelInboundHandlerAdapter
,然后覆写 channelActive
方法启动 Actor
override fun channelActive(ctx: ChannelHandlerContext) {
val state = node.state
if (node.state == State.Started) {
val channelActor = node.system.actorOf(ChannelActor.props(node, ctx))
ctx.channel().attr(actorKey).set(channelActor)
} else {
logger.warn("gate is not running, current state:{}, channel will close", state)
ctx.close()
}
}
转发消息
同样在 ChannelHandler
中覆写 channelRead
override fun channelRead(ctx: ChannelHandlerContext, message: Any) {
if (message is ClientProtobuf) {
tell(ctx, message)
} else {
logger.error("unsupported message:{}", message)
}
}
private fun tell(ctx: ChannelHandlerContext, message: ChannelMessage) {
val channelActorRef: ActorRef? = ctx.channel().attr(actorKey).get()
if (channelActorRef == null) {
logger.warn("failed to send message:{} to channel actor because of channel actor not found", message)
} else {
channelActorRef.tell(message)
}
}
这样就完成了 Netty 和 Akka 之间的关联了,如果在 Actor 中收到服务端推送给客户端的消息,只需要调用 ChannelHandlerContext
中的方法就好了。
fun write(message: GeneratedMessage, listener: ChannelFutureListener? = null) {
val future = handlerContext.writeAndFlush(message)
listener?.let { future.addListener(it) }
}
消息路由
现在 ChannelActor
已经收到消息了,但是距离消息的目的地还远,例如接受客户端连接的是 node1,但是消息的目的地 Actor 可能在 node2 或者 node3 上面,我们还需要在内部做一次消息转发。在设计中,PlayerActor
和 WorldActor
都是基于集群分片创建的,只要我们知道这些 Actor 的 id,那么 Akka 就会正确的将消息转发给对应的 Actor。对于集群单例的 Actor,也可以启动 ClusterSingletonProxy
将消息转发到对应的 Actor。
好了,现在我们又遇到下一个问题,哪些协议应该发往哪些 Actor?,例如消息 A 的处理函数是在 WorldActor
上的,消息 B 的处理函数是在 PlayerActor
上的。为此我们需要做一个消息转发的 Map,哪些消息应该转发到哪个 Actor 上。我们会在 Gradle 中定义如下的代码,用于生成转发 Map:
val actor = Forward[project.name]
if (actor != null) {
tasks.register<JavaExec>("generateProtoForwardMap") {
group = "other"
description = "Generates protobuf forward map for gate"
mainClass = "com.mikai233.common.message.MessageForwardGeneratorKt"
val sourceSetsMain = sourceSets.main.get()
classpath = sourceSetsMain.runtimeClasspath
val gateResourcesPath =
project(":gate").extensions.getByType<SourceSetContainer>().main.get().resources.srcDirs.first().path
args =
listOf(
"-p",
"com.mikai233.${project.name}.handler",
"-o",
gateResourcesPath,
"-f",
actor
)
}
tasks.named("compileKotlin") {
finalizedBy("generateProtoForwardMap")
}
}
执行的程序也很简单,就是通过反射扫描 MessageHandler
,查看这个包下面处理了哪些消息,然后再转成消息 id 写入 gate 的 resources 目录下, gate 启动的时候,通过读取这些文件,就知道怎么转发消息了。
resources
├─ ChannelActor.json
├─ PlayerActor.json
├─ WorldActor.json
├─ gate.conf
└─ logback.xml
文件里面就单纯写入了消息的 id。
[ 3, 2 ]
单例初始化的时候就直接扫 resources 目录,然后生成转发的 Map 就可以了。
object MessageForward {
private val ForwardMap = mutableMapOf<Int, Forward>()
fun whichActor(id: Int): Forward? {
return ForwardMap[id]
}
init {
Forward.entries.forEach { actor ->
val url = Resources.getResource("${actor.name}.json")
Json.fromBytes<List<Int>>(url.openStream().readBytes()).forEach { id ->
ForwardMap[id] = actor
}
}
}
}
在 ChannelActor
收到消息之后,就可以这么处理了:
private fun forwardClientMessage(clientProtobuf: ClientProtobuf) {
val (id, message) = clientProtobuf
val actor = MessageForward.whichActor(id)
logger.debug("forward message:{} to target:{}", formatMessage(message), actor)
if (actor == null) {
logger.warning("proto: {} has no target to forward", clientProtobuf.id)
} else {
when (actor) {
Forward.PlayerActor -> {
val playerId = playerId
if (playerId != null) {
node.playerSharding.tell(ProtobufEnvelope(playerId, message), self)
} else {
logger.warning("try to send message to uninitialized playerId, this message will be dropped")
}
}
Forward.WorldActor -> {
val worldId = worldId
if (worldId != null) {
node.worldSharding.tell(ProtobufEnvelope(worldId, message), self)
} else {
logger.warning("try to send message to uninitialized worldId, this message will be dropped")
}
}
Forward.ChannelActor -> {
handleProtobuf(message)
}
}
}
}