Akka RPC通信案例代码

1 篇文章 0 订阅

Akka RPC通信案例代码

1. 背景

  1. akka作为一个分布式异步通信框架,可以很好解决分布式集群中的异步信息同步问题
  2. akka是一个针对java和scala程序的框架,所以可以使用java也可以使用scala的api。
  3. 本文使用scala接口来进行akka简单master和worker通信的示例代码
  4. 环境准备
  1. idea 2020版本
  2. maven3.6.3
  3. scala 2.12.12
  4. jdk 1.8

2. 代码一

2.1 创建maven项目

maven的pom依赖文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.doit</groupId>
    <artifactId>akka-rpc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 常量 -->
    <properties>
        <!-- jdk 1.8版本-->
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <!-- 字符编码是utf8       -->
        <encoding>UTF-8</encoding>
        <!-- scala版本是2.12.12,最新的2.13.3的版本了        -->
        <scala.version>2.12.12</scala.version>
        <scala.compat.version>2.12</scala.compat.version>
        <!-- akka的版本是2.4.17,最新的现在是2.6.9       -->
        <akka.version>2.4.17</akka.version>
    </properties>


    <dependencies>
        <!-- scala的依赖 -->
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <!--  这里取得是变量 -->
            <version>${scala.version}</version>
        </dependency>

        <!-- akka actor依赖 -->
        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-actor_${scala.compat.version}</artifactId>
            <version>${akka.version}</version>
        </dependency>

        <!-- akka远程通信依赖 -->
        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-remote_${scala.compat.version}</artifactId>
            <version>${akka.version}</version>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <!-- 编译scala的插件 -->
                <plugin>
                    <groupId>net.alchim31.maven</groupId>
                    <artifactId>scala-maven-plugin</artifactId>
                    <version>3.2.2</version>
                </plugin>
                <!-- 编译java的插件 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.5.1</version>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>scala-compile-first</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>add-source</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>scala-test-compile</id>
                        <phase>process-test-resources</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>


            <!-- 打包包含依赖的jar包插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>

                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>reference.conf</resource>
                                </transformer>

                                <!-- 指定maven-main方法 -->
                                <!--                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">-->
                                <!--                                    <mainClass>cn._51doit.rpc.Master</mainClass>-->
                                <!--                                </transformer>-->
                            </transformers>

                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

2.2 scala代码思路

在这里插入图片描述
整体代码思路,就是这个图里面的

  1. 先启动主节点,然后启动从节点
  2. 从节点连接主节点,并发送注册信息
  3. 主节点接收并保存从节点的注册信息
  4. 主节点返回一个注册成功信息给从节点
  5. 从节点开始发送心跳信息给主节点
  6. 主节点定时检查心跳,如果出现超时连接补上的从节点就剔除
    注意,akka遵循actor模型,这里面不管是master主节点还是worker从节点,都会有一个actorsystem单例对象,这个对象负责分发和创建actor对象。实际信息传输是发生在各个actor之间。

2.3 scala代码

2.3.1 master节点代码

package com.doit.akka.rpc.model

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._
import scala.collection.mutable

class Master extends Actor {
  val map = new mutable.HashMap[String, WorkerInfo]()

  override def preStart(): Unit = {
    // 这里启动定时器,检查从节点的连接
    import context.dispatcher
    // 第一个参数是什么时候启动,第二个参数是间隔,第三个参数是发给哪个actor,第四个是发送的信息
    context.system.scheduler.schedule(0.millisecond, 15000.millisecond, self, CheckTimeOut)
  }

  // master接收到数据后的匹配处理
  override def receive: Receive = {
    // worker注册的消息
    case WorkerRegisterInfo(workerid, memory, cores) => {

      // 保存起来
      if (!map.contains(workerid)) {
        println("收到注册信息")

        // 因为master节点需要保存worker从节点发送的注册信息,使用case calss进行保存
        // case class自己实现了很多方法,如序列化,apply,getter,setter,equals,hashcode等等
        val workerInfo: WorkerInfo = WorkerInfo(workerid, memory, cores)

        // 使用map来保存注册信息
        map.put(workerid, workerInfo)

        // 告诉从节点,注册成功
        sender() ! RegisterSuccess
      }
    }

    // 检查超时
    case CheckTimeOut => {
      // 查看一下worker字典中信息,看一下时间是否超时了,超时就将其从字典中删除
      val workerInfoes: Iterable[WorkerInfo] = map.values.filter(w => System.currentTimeMillis() - w.lastHeartbeatTime > 10000)

      // 删除超时的
      workerInfoes.foreach(w => {
        map.remove(w.workerid)
      })

      println("存活worker个数:" + map.values.size)
    }

    // 心跳
    case Heartbeat(workid) => {
      // 收到心跳包之后,更新对应心跳的时间
      map(workid).lastHeartbeatTime = System.currentTimeMillis()

      println("心跳时间:" + map(workid).lastHeartbeatTime)
    }

  }

}

object Master {
  def main(args: Array[String]): Unit = {

    val masterHost = "localhost"
    val masterPort = 9999

    val configStr =
      s"""
         |akka.actor.provider = "akka.remote.RemoteActorRefProvider"
         |akka.remote.netty.tcp.hostname = $masterHost
         |akka.remote.netty.tcp.port = $masterPort
         |""".stripMargin

    val config: Config = ConfigFactory.parseString(configStr)

    // 创建master节点上的actorsystem对象,单例
    val actorSystem: ActorSystem = ActorSystem.apply("Master-Actor-System", config)

    // 使用actorsystem创建actor对象
    val actorRef: ActorRef = actorSystem.actorOf(Props[Master], "Master-Actor")
  }
}

2.3.2 worker节点代码

package com.doit.akka.rpc.model

import akka.actor.{Actor, ActorRef, ActorSelection, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._

class Worker extends Actor {

  var actorSelection: ActorSelection = null;

  override def preStart(): Unit = {
    // 建立连接
    actorSelection = context.actorSelection("akka.tcp://Master-Actor-System@localhost:9999/user/Master-Actor")

    // 发送注册的信息,workerid,内存,cpu核心数
    actorSelection ! WorkerRegisterInfo("3", 4096, 4)
  }

  // 这里接收收到的消息
  override def receive: Receive = {
    // 注册成功消息
    case RegisterSuccess => {
      // 发送一个给自己的消息,要发心跳了
      println("注册成功")

      import context.dispatcher
      context.system.scheduler.schedule(0.millisecond, 10000.millisecond, self, SendHeartBeat)
    }

    // 发心跳的信号
    case SendHeartBeat => {
      println("发送心跳信号")
      // 需要发送给主节点,注意不要使用sender(),因为SendHeartBeat其实是worker自己发给自己的
      actorSelection ! Heartbeat("3")
    }
  }
}

object Worker {
  def main(args: Array[String]): Unit = {

    // 这里进行worker的创建
    val workerHost = "localhost"
    val workerPort = 8888

    val confiStr =
      s"""
         |akka.actor.provider = "akka.remote.RemoteActorRefProvider"
         |akka.remote.netty.tcp.hostname = $workerHost
         |akka.remote.netty.tcp.port = $workerPort
         |""".stripMargin

    val config: Config = ConfigFactory.parseString(confiStr)

    val actorSystem: ActorSystem = ActorSystem.apply("Worker-Actor-System", config)

    val actorRef: ActorRef = actorSystem.actorOf(Props[Worker], "Worker-Actor")
  }
}

2.3.3 各个case class、case object等代码

  • 检查超时的actor信息,这是master节点发送给自己的信息。当master节点启动后,就会开启一个定时器,定时发送这个信息给自己,自己接收到这个信息后,再receive方法中进行处理。
package com.doit.akka.rpc.model

case object CheckTimeOut
  • 心跳actor信息,当worker节点建立起和master节点的连接,注册成功后,就会开始定时发送心跳信息。这个是worker节点发送给master节点的信息。
package com.doit.akka.rpc.model

case class Heartbeat(workid: String)
  • 这是worker建立起和master节点,注册成功后,master节点发送给worker节点的注册成功信息。
package com.doit.akka.rpc.model

case object RegisterSuccess
  • 这是worker节点接收到master节点返回的注册成功信息后,worker节点自己发给自己的actor信息,告诉自己可以开始往master节点发送心跳信息了。
package com.doit.akka.rpc.model

case object SendHeartBeat

  • 这是主节点接收到worker节点发送的注册信息后,因为master节点需要保存worker节点的各种信息,所以建立的一个case class,用来保存信息的。
package com.doit.akka.rpc.model

case class WorkerInfo(workerid:String, memory:Int, cpuCores:Int){

  var lastHeartbeatTime: Long = _
}
  • 这是worker节点建立起和master节点的连接后,worker节点发送给master节点的注册信息,里面会包含worker节点的id,内存,cpu等信息。
package com.doit.akka.rpc.model

case class WorkerRegisterInfo(workerid:String, memory:Int, cpuCores:Int){}

注意

  1. 这里面使用case class,case object来进行信息传递和数据存储,这是因为本身case class和case object已经由scala实现了很多方法,如序列化,equals、hashcode,getter,setter等等。但这并不是一定的,其实还可以使用更高效的数据传输方式,这一点akka其实有警告信息提示
[WARN] [SECURITY][09/14/2020 19:59:39.082] [Master-Actor-System-akka.remote.default-remote-dispatcher-17] [akka.serialization.Serialization(akka://Master-Actor-System)] Using the default Java serializer for class [com.doit.akka.rpc.model.RegisterSuccess$] which is not recommended because of performance implications. Use another serializer or disable this warning using the setting 'akka.actor.warn-about-java-serializer-usage'
  1. akka里使用的是actor通信模型,也会有发送者和接收者概念,还是用到了隐式参数,所以隐式context中其实会包含各种需要的信息和对象。包括定时器,发送者,接收者等等。
  2. akka其实也是有生命周期,所以这里的prestart、receive都是其中的生命周期函数之一。同样的也会有结束/关闭前 的方法
    如下图:在这里插入图片描述
  3. 如果跟hdfs的namenode和datanode节点之间的通信机制进行对比,会发现其实2者非常相近,非常非常像。
  4. 注意,本身akka是用于分布式的异步通信框架或者工具,这里演示是使用的本地模式来掩饰,可以将程序打成jar 包,发散到各个集群节点中进行通信,不过代码中写死的参数就需要做一定调整,改为从外部参数输入或者读取配置文件等方式来保证参数可修改。
  5. akka其实应用于spark、flink框架,但在国内,直接用于应用层的案例并不多,因为要做到分布式集群通信有更加通用并且验证有效的解决方案。但使用与底层通信会更多一些,这一点从spark、flink采用akka来通信就可知一二。

3. 代码二

这里是基于上述代码,对代码做改进,将写死的代码改为外部传入,冗余代码做合并。
在这里插入图片描述

  • 定义的case class、case object合并到一个文件中
package com.doit.akka.rpc2.model

// 超时检查信号,master-》自己
case object CheckTimeOut

// 心跳信号,worker-》master
case class Heartbeat(workid: String)

// 注册成功,master-》worker
case object RegisterSuccess

// 发送心跳,worker->自己
case object SendHeartBeat

// master节点保存worker信息的case class;
// 注意这里还有一个lastHeartbeatTime,属于变量,master定期收到心跳信息,收到时,就更新一下这个心跳时间。
case class WorkerInfo(workerid:String, memory:Int, cpuCores:Int){

  var lastHeartbeatTime: Long = _
}

// worker节点信息,worker->master的注册信息
case class WorkerRegisterInfo(workerid:String, memory:Int, cpuCores:Int){}

  • worker代码
package com.doit.akka.rpc2.model

import java.util.UUID

import akka.actor.{Actor, ActorRef, ActorSelection, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}

import scala.concurrent.duration._

class Worker(val masterDomain:String, val masterPort:String, val memory:Int, val cpuCores:Int) extends Actor {

  var actorSelection: ActorSelection = null;

  // workerid使用随机字符串,真实情况可能会按照需求从配置文件中读取
  val workerid = UUID.randomUUID().toString

  override def preStart(): Unit = {
    // 建立连接
    actorSelection = context.actorSelection(s"akka.tcp://${Worker.Worker_Actor_System}@${masterDomain}:${masterPort}/user/${Worker.Worker_Actor}")

    // 发送注册的信息,workerid,内存,cpu核心数
    actorSelection ! WorkerRegisterInfo(workerid, memory, cpuCores)
  }

  // 这里接收收到的消息
  override def receive: Receive = {
    // 注册成功消息
    case RegisterSuccess => {
      // 发送一个给自己的消息,要发心跳了
      println("注册成功")

      import context.dispatcher
      context.system.scheduler.schedule(0.millisecond, 10000.millisecond, self, SendHeartBeat)
    }

    // 发心跳的信号
    case SendHeartBeat => {
      println("发送心跳信号")
      // 需要发送给主节点,注意不要使用sender(),因为SendHeartBeat其实是worker自己发给自己的
      actorSelection ! Heartbeat(workerid)
    }
  }
}

object Worker {
  val Worker_Actor_System = "Worker-Actor-System"
  val Worker_Actor = "Worker-Actor"

  def main(args: Array[String]): Unit = {

    // main方法这里进行worker的创建

    // val masterDomain:String, val masterPort:String, val memory:Int, val cpuCores:Int
    val workerHost = args(0) // 如"localhost"
    val workerPort = args(1)// 如8888
    val masterDomain = args(2)
    val masterPort = args(3)
    val memory = args(4).toInt
    val cpuCores = args(5).toInt

    val confiStr =
      s"""
         |akka.actor.provider = "akka.remote.RemoteActorRefProvider"
         |akka.remote.netty.tcp.hostname = $workerHost
         |akka.remote.netty.tcp.port = $workerPort
         |""".stripMargin

    // 根据配置字符串,创建config对象
    val config: Config = ConfigFactory.parseString(confiStr)

    // 创建worker的actorSystem单例对象
    val actorSystem: ActorSystem = ActorSystem.apply(Worker_Actor_System, config)

    // 创建Props对象
    val props: Props = Props(new Worker(masterDomain, masterPort, memory, cpuCores))

    // 创建actor对象
    val actorRef: ActorRef = actorSystem.actorOf(props, Worker_Actor)
  }
}
  • master代码
package com.doit.akka.rpc2.model

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._
import scala.collection.mutable

class Master extends Actor {
  val map = new mutable.HashMap[String, WorkerInfo]()

  override def preStart(): Unit = {
    // 这里启动定时器,检查从节点的连接
    import context.dispatcher
    // 第一个参数是什么时候启动,第二个参数是间隔,第三个参数是发给哪个actor,第四个是发送的信息
    context.system.scheduler.schedule(0.millisecond, 15000.millisecond, self, CheckTimeOut)
  }

  // master接收到数据后的匹配处理
  override def receive: Receive = {
    // worker注册的消息
    case WorkerRegisterInfo(workerid, memory, cores) => {

      // 保存起来
      if (!map.contains(workerid)) {
        println("收到注册信息")

        // 因为master节点需要保存worker从节点发送的注册信息,使用case calss进行保存
        // case class自己实现了很多方法,如序列化,apply,getter,setter,equals,hashcode等等
        val workerInfo: WorkerInfo = WorkerInfo(workerid, memory, cores)

        // 使用map来保存注册信息
        map.put(workerid, workerInfo)

        // 告诉从节点,注册成功
        sender() ! RegisterSuccess
      }
    }

    // 检查超时
    case CheckTimeOut => {
      // 查看一下worker字典中信息,看一下时间是否超时了,超时就将其从字典中删除
      val workerInfoes: Iterable[WorkerInfo] = map.values.filter(w => System.currentTimeMillis() - w.lastHeartbeatTime > 10000)

      // 删除超时的
      workerInfoes.foreach(w => {
        map.remove(w.workerid)
      })

      println("存活worker个数:" + map.values.size)
    }

    // 心跳
    case Heartbeat(workid) => {
      // 收到心跳包之后,更新对应心跳的时间
      map(workid).lastHeartbeatTime = System.currentTimeMillis()

      println("心跳时间:" + map(workid).lastHeartbeatTime)
    }
  }
}

object Master {
  val Master_Actor_System = "Master-Actor-System"
  val Master_Actor = "Master-Actor"

  def main(args: Array[String]): Unit = {

    val masterHost = args(0) // 如 "localhost"
    val masterPort = args(1) // 如9999

    val configStr =
      s"""
         |akka.actor.provider = "akka.remote.RemoteActorRefProvider"
         |akka.remote.netty.tcp.hostname = $masterHost
         |akka.remote.netty.tcp.port = $masterPort
         |""".stripMargin

    val config: Config = ConfigFactory.parseString(configStr)

    // 创建master节点上的actorsystem对象,单例
    val actorSystem: ActorSystem = ActorSystem.apply(Master_Actor_System, config)

    // 使用actorsystem创建actor对象
    val actorRef: ActorRef = actorSystem.actorOf(Props[Master], Master_Actor)
  }
}
  • 打成jar包
    在这里插入图片描述
    original的是不带依赖的jar包
  • 调用未指定main方法jar包的方法
java -cp xxx.jar yy.MainClass arg0 arg1 arg2...
  • 实际运行效果
    在这里插入图片描述

master节点的全类名和参数

com.doit.akka.rpc2.model.Master
192.168.133.2
9999
在这里插入图片描述

worker节点的全类名和参数
在这里插入图片描述

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值