第 16 章 并发编程模型 Akka(上)

一、基本介绍

1. Akka是JAVA虚拟机JVM平台上构建高并发、分布式和容错应用的工具包和运行时,你可以理解成Akka是
编写并发程序的框架;
2. Akka用Scala语言写成,同时提供了Scala和JAVA的开发接口;
3. Akka主要解决的问题是:可以轻松的写出高效稳定的并发程序,程序员不再过多的考虑线程、锁和资源竞
争等细节。

示意图:
Akka示意图

二、Actor模型用于解决什么问题

1. 处理并发问题关键是要保证共享数据的一致性和正确性,因为程序是多线程时,多个线程对同一个数据
进行修改,若不加同步条件,势必会造成数据污染。但是当我们对关键代码加入同步条件synchronized 后,
实际上大并发就会阻塞在这段代码,对程序效率有很大影响;
2. 若是用单线程处理,不会有数据一致性的问题,但是系统的性能又不能保证;
3. Actor 模型的出现解决了这个问题,简化并发编程,提升程序性能。 可以这里理解:Actor 模型是一种处理
并发问题的解决方案,很牛!

三、Akka中Actor模型

示意图:
在这里插入图片描述
Actor模型及其说明

1. Akka处理并发的方法基于Actor模型(示意图);
2. 在基于Actor的系统里,所有的事物都是Actor,就好像在面向对象设计里面所有的事物都是对象一样;
3. Actor模型是作为一个并发模型设计和架构的。其中Actor与Actor之间只能通过消息通信,如图中的信封;
4. Actor与Actor之间只能用消息进行通信,当一个Actor给另外一个Actor发消息,消息是有顺序的(消息队列),
只需要将消息投寄的相应的邮箱即可;
5. 怎么处理消息是由接收消息的Actor决定的,发送消息Actor可以等待回复,也可以异步处理【ajax】;
6. ActorSystem的职责是负责创建并管理其创建的Actor, ActorSystem是单例的(可以ActorSystem是一个工厂,
专门创建Actor),一个JVM进程中有一个即可,而Acotr是可以有多个的;
7. Actor模型是对并发模型进行了更高的抽象;
8. Actor模型是异步、非阻塞、高性能的事件驱动编程模型。[案例:说明 什么是异步、非阻塞,最经典的案例就是
ajax异步请求处理 ];
9. Actor模型是轻量级事件处理(1GB 内存可容纳百万级别个 Actor),因此处理大并发性能高。

四、Actor模型工作机制说明

示意图:
在这里插入图片描述
Actor模型工作机制说明(对照上面示意图)

1. ActorySystem:创建Actor;
2. ActorRef:可以理解成是Actor的代理或者引用。消息是通过ActorRef来发送,而不能通过Actor发送消息,
通过哪个ActorRef 发消息,就表示把该消息发给哪个Actor;
3. 消息发送到Dispatcher Message (消息分发器),它得到消息后,会将消息进行分发到对应的MailBox。(注: 
Dispatcher Message可以理解成是一个线程池, MailBox 可以理解成是消息队列,可以缓冲多个消息,遵守
FIFO);
4. Actor可以通过receive方法来获取消息,然后进行处理。

Actor间传递消息机制(对照上面示意图)

1. 每一个消息就是一个Message对象。Message 继承了Runable, 因为Message就是线程类;
2. 从Actor模型工作机制看上去很麻烦,但是程序员编程时只需要编写Actor就可以了,其它的交给Actor模型完
成即可;
3. A Actor要给B Actor 发送消息,那么A Actor 要先拿到(也称为持有) B Actor的代理对象ActorRef才能发送消息。

五、Actor模型快速入门

应用实例需求

1. 编写一个Actor, 比如SayHelloActor;
2. SayHelloActor可以给自己发送消息;
3. 要求使用Maven的方式来构建项目,这样可以很好的解决项目开发包的依赖关系。[scala 和 akka]。

效果如下图
在这里插入图片描述
Actor自我通讯机制原理图
在这里插入图片描述
示例代码:

package com.lj.akka

import akka.actor.{Actor, ActorRef, ActorSystem, Props}

/**
  * @author Administrator
  * @create 2020-03-19
  * 说明:当我们继承Actor之后,就是一个Actor,核心方法是receive,进行重写
  */

class SayHelloActor extends Actor{

    /**
      * 说明:
      *   1. receive方法会被该Actor的MailBox(实现了Runable接口)调用;
      *   2. 当该Actor的MailBox接收到消息,就会调用receive方法;
      *   3. type Receive = PartialFunction[Any, Unit](Receive是一个偏函数);
      *   4. receive方法循环接收消息。
      */
	// 偏函数的简写形式
    override def receive: Receive = {
        case "Hello" => println("收到Hello,回应Hello too.")
        case "Ok" => println("收到OK,回应OK too.")
        case "EXIT" => {

            println("收到EXIT指令,退出系统!")
            context.stop(self)    // 停止 actorRef
            context.system.terminate()    // 退出actor system

        }
        case _ => println("接收指令有误!")
    }
}

object SayHelloActorMain {

    // 1. 先创建一个ActorSystem,专门用于创建Actor
    private val actorFactory = ActorSystem("actorFactory")
    // 2. 创建一个Actor的同时,返回Actor的ActorRef
    /**
      * 说明:
      *   1. Props[SayHelloActor]:创建一个SayHelloActor实例,使用反射创建,
      * 	 也可以使用new创建Props(new SayHelloActor)
      *   2. "sayHelloActor"是给Actor取名
      *   3. sayHelloActorRef: ActorRef就是Props[SayHelloActor]的ActorRef
      *   4. 创建SayHelloActor实例被ActorSystem接管
      */
    private val sayHelloActorRef: ActorRef = actorFactory.actorOf(Props[SayHelloActor], "sayHelloActor")

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

		// 给sayHelloActorRef发送消息
    	// !是方法名(看源码)
        sayHelloActorRef ! "Hello"
        sayHelloActorRef ! "Ok"
        sayHelloActorRef ! "Ok~"
        sayHelloActorRef ! "EXIT"

    }

}

小结和说明

当程序执行 AActorRef = actorFactory.actorOf(Props[AActor], "AActor") ,会完成如下任务 [这是非常重要的方法]:
	1. actorFactory是ActorSystem("ActorFactory") 这样创建的;
	2. 这里的Props[AActor] 会使用反射机制,创建一个AActor对象,如果是actorFactory.actorOf(Props(new 
	AActor(BActorRef)), "AActorRef") 形式,就是使用new的方式创建一个AActor对象, 注意Props()是小括号;
	3. 会创建一个AActor对象的代理对象 AActorRef , 使用AActorRef才能发送消息;
	4. 会在底层创建Dispather Message,是一个线程池,用于分发消息, 消息是发送到对应的Actor的MailBox;
	5. 会在底层创建AActor的MailBox对象,该对象是一个队列,可接收Dispatcher Message发送的消息;
	6. MailBox 实现了Runnable接口,是一个线程,一直运行并调用Actor的receive方法,因此当Dispather发送
	消息到MailBox时,Actor在receive方法就可以得到信息;
	7. AActorRef !  "hello", 表示把hello消息发送到AActor的mailbox(通过Dispatcher Message 转发)。

六、Actor模型应用实例-Actor间通讯

应用实例需求

1. 编写2个 Actor , 分别是  AActor 和 BActor;
2. AActor和BActor之间可以相互发送消息。

两个Actor的通讯机制原理图
在这里插入图片描述
示例代码:

package com.lj.akka

import akka.actor.{Actor, ActorRef, ActorSystem, Props}

import scala.io.StdIn

/**
  * @author Administrator
  * @create 2020-03-19
  */
class AActor(actorRef: ActorRef) extends Actor{

    val bActorRef: ActorRef = actorRef
    var a_num: Int = 0

    override def receive: Receive = {

        case "start" => {
//            println("AActor(黄飞鸿)出招了,开打!")
            println("开启两个Actor之间的通讯聊天......")
            self ! a_num  // 发送消息给自己a
        }

        case num: Int => {

            /**
              * 说明:
              *   1. 此时需要给BActor发送消息,就需要拥有BActorRef
              *      所以需要在创建AActor的时候就创建BActorRef
              */
            if (num == 3) {  // 出招5次,需要终止,不分胜负
                bActorRef ! a_num  // 给BActor发出消息
            } else {
//                println("AActor(黄飞鸿):厉害,看我佛山无影脚...")
                print("AActor:")
                StdIn.readLine()
                Thread.sleep(10)
                a_num += 1
                bActorRef ! a_num
            }
        }

        case "AActor Exit" => {
            println("AActor收到EXIT指令,退出系统!")
            sender() ! "BActor Exit"
            // 停止AActor的ActorRef
            context.stop(self)
            context.system.terminate()    // 退出actor system
        }

    }
}


class BActor extends Actor {

    var b_num: Int = 0

    override def receive: Receive = {
        case num: Int => {
            if (num == 3) {  // 出招5次,需要终止,不分胜负
                sendMessage
            } else {
                sendMessage
            }
        }

        case "BActor Exit" => {
            println("BActor收到EXIT指令,退出系统!")
            context.stop(self)    // 停止 actorRef
            context.system.terminate()    // 退出actor system
        }
    }


    def sendMessage(): Unit = {
//        println("BActor(乔峰):挺猛,看我降龙十八掌...")
        print("BActor:")
        StdIn.readLine()
        Thread.sleep(10)
        b_num += 1
        // 通过sender()可以获取到发送消息的Actor的ActorRef
        if (b_num == 3) {
            sender() ! "AActor Exit"
        } else {
            sender() ! b_num  // 给AActor发送消息
        }
    }

}

/**
  * 说明:
  *   1. 这里可以不写main方法,可以直接继承App(它是一个trait,实现了main方法的功能。)
  */
object ActorGame extends App {

    // 1. 创建ActorSystem
    private val actorFactory = ActorSystem("actorFactory")
    // 2. 首先需要先创建BActor的ActorRef(因为AActor应用到了BActor的ActorRef)
    private val bActorRef: ActorRef = actorFactory.actorOf(Props[BActor], "BActor")
    // 3. 再创建AActor的ActorRef
    private val aActorRef: ActorRef = actorFactory.actorOf(Props(new AActor(bActorRef)), "AActor")

    // AActor开始出招!
    aActorRef ! "start"

}
==================================运行结果==================================
开启两个Actor之间的通讯聊天......
AActor:在下黄飞鸿,阁下是?
BActor:在下乔峰,想干架还是咋地!
AActor:想扁你,看我迷踪拳!
BActor:小样,还挺迷惑人的吗!吃我降龙十八掌...
AActor:还挺猛,让你尝尝我的佛山无影脚
BActor:我操,这么厉害,溜了...
AActor收到EXIT指令,退出系统!
BActor收到EXIT指令,退出系统!
==================================运行结果==================================

两个Actor的通讯小结和说明

两个Actor通讯机制和Actor自身发消息机制基本一样,只是要注意如下两点:
	1. 如果A Actor在需要给B Actor发消息,则需要持有B Actor的ActorRef,可以通过创建时,传入B Actor的
	代理对象(ActorRef);
	2. 当B Actor在receive方法中接收到消息,需要回复时,可以通过sender() 获取到发送Actor的代理对象。

如何理解Actor的receive方法被调用

1. 每个Actor对应MailBox;
2. MailBox实现了Runnable接口,处于运行的状态;
3. 当有消息到达MailBox,就会去调用Actor的receive方法,将消息推送给receive。

七、Akka网络编程

1、基本介绍
Akka支持面向大并发后端服务程序,网络通信这块是服务端程序重要的一部分。
网络编程有两种:
	1. TCP socket编程,是网络编程的主流。之所以叫Tcp socket编程,是因为底层是基于Tcp/ip协议的. 
	比如: QQ聊天;
	2. b/s结构的http编程,我们使用浏览器去访问服务器时,使用的就是http协议,而http底层依旧是用
	tcp socket实现的。 比如: 京东商城 【属于 web 开发范畴 】
2、网络编程基础知识

协议(tcp/ip)

	TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互联协议,
又叫网络通讯协议,这个协议是Internet最基本的协议、Internet国际互联网络的基础,简单地说,就是由网络层
的IP协议和传输层的TCP协议组成的。

OSI与Tcp/ip参考模型 (推荐tcp/ip协议3卷)

tracert命令:追踪网络传输通信

在这里插入图片描述
ip地址

概述:每个internet上的主机和路由器都有一个ip地址,它包括网络号和主机号,ip地址有ipv4(32位)或者
	 ipv6(128位). 可以通过ipconfig 来查看

端口(port)-介绍

	这里所指的端口不是指物理意义上的端口,而是特指TCP/IP协议中的端口,是逻辑意义上的端口。
	如果把IP地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个IP地址的端口
可以有65535(即:256×256-1)个之多!端口是通过端口号来标记的。(端口号 0:Reserved)

端口(port)-分类

1. 0号是保留端口.
2. 1-1024是固定端口
	 又叫有名端口,即被某些程序固定使用,一般程序员不使用.
	 22: SSH远程登录协议  23: telnet使用  21: ftp使用
	 25: smtp服务使用     80: iis使用	7: echo服务
3. 1025-65535是动态端口 
	 这些端口,程序员可以使用.

端口(port)-使用注意

1. 在计算机(尤其是做服务器)要尽可能的少开端口;
2. 一个端口只能被一个程序监听;
3. 如果使用 netstat –an 可以查看本机有哪些端口在监听;
4. 可以使用 netstat –anb 来查看监听端口的pid,在结合任务管理器关闭不安全的端口。

八、Akka网络编程-客服对话

需求

1. 服务端进行监听(xxxxxx);
2. 客户端可以通过键盘输入,发送咨询问题给客服(服务端);
3. 客服(服务端)回答客户的问题。

程序框架图
在这里插入图片描述
程序网络拓扑图
在这里插入图片描述
示例代码:

===============================服务端代码===============================
package com.lj.akka.server

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.lj.akka.common.{AppleClientMessage, AppleServerMessage}
import com.typesafe.config.ConfigFactory

import scala.io.StdIn

/**
  * @author Administrator
  * @create 2020-03-19
  */
class AppleServerActor extends Actor {

    override def receive: Receive = {

        case "Apple Server Start" => println("Apple Server 已经上线,欢迎咨询 ^_^ ......")
        // 如果收到ClientMessage,进行如下匹配模式
        case AppleClientMessage(customer_message) => {

            customer_message match {
                // 设置自动回复消息
                case "Apple 11价格?" => sender() ! AppleServerMessage("6000~12000RMB")
                case "保修时间?" => sender() ! AppleServerMessage("一年内非人为损坏,换新。")
                case "Apple 11有几款颜色?" => sender() ! AppleServerMessage("目前有三种颜色:黑、白、土豪金")
                case customer_message: String => {

                    print("AppleCustomer:" + customer_message + "\nAppleServer:")
                    // 服务端回复客户端消息
                    val server_input_mes = StdIn.readLine()
                    sender() ! AppleServerMessage(server_input_mes)

                }
            }
        }
    }

}

object AppleServerActorMain extends App {

    val ip_host = "127.0.0.1"  // 服务端IP地址
    val ip_port = 13666  // 服务端IP端口

    /**
      * 第一步:创建config对象,指定协议类型,监听IP和端口PORT
      * 说明:
      *   1. 字符串中的写法是固定的;
      *   2. stripMargin:这个方法默认是以“|”分割的,同时还会去空格。
      *   3. akka.actor.warn-about-java-serializer-usage=false: 禁止Java序列化警告
      */

    val config = ConfigFactory.parseString(
        s"""
           |akka.actor.provider="akka.remote.RemoteActorRefProvider"
           |akka.remote.netty.tcp.hostname=$ip_host
           |akka.remote.netty.tcp.port=$ip_port
           |akka.actor.warn-about-java-serializer-usage=false
         """.stripMargin)

    // 第二步:创建ActorSystem
    // 名字:AppleServerSystem这个是可以随便起的
    private val appleServerSystem = ActorSystem("AppleServerSystem", config)

    // 第三步:AppleServerActor的Actor和返回ActorRef
    // 名字:AppleServerActorRef这个也是可以随便起的
    private val appleServerActorRef: ActorRef = appleServerSystem.actorOf(Props[AppleServerActor], "AppleServerActorRef")

    // 第四步:发送消息给appleServerActorRef,启动Server
    appleServerActorRef ! "Apple Server Start"

}
===============================服务端代码===============================


===============================客户端代码===============================
package com.lj.akka.customer

import akka.actor.{Actor, ActorRef, ActorSelection, ActorSystem, Props}
import com.lj.akka.common.{AppleClientMessage, AppleServerMessage}
import com.typesafe.config.ConfigFactory

import scala.io.StdIn

/**
  * @author Administrator
  * @create 2020-03-19
  */
class AppleCustomerActor(serverIPHost: String, serverIPPort: Int) extends Actor {

    // 定义一个AppleServerActorRef使用ActorSelection类型,后续可以输入自己需要的配置信息
    var appleServerActorRef: ActorSelection = _

    // 在Actor中有一个方法“PreStart方法”,它会在Actor运行之前执行
    // 在Akka的开发中,通常将初始化的工作,放在PreStart方法中
    override def preStart(): Unit = {
        // println("执行PreStart方法......")  // 查看PreStart方法执行时机
        // AppleServerActorRef:这个是服务端的Actor引用名称
        appleServerActorRef = context.actorSelection(
            s"akka.tcp://AppleServerSystem@${serverIPHost}:${serverIPPort}/user/AppleServerActorRef")
        // println("appleServerActorRef:" + appleServerActorRef)

    }


    override def receive: Receive = {
        case "Apple Customer Start" => print("Apple 客户端已经开通,欢迎使用!!!\nAppleCustomer:")
        case customer_input_mes: String => {
            // 发送消息给Apple Server ActorRef
            appleServerActorRef ! AppleClientMessage(customer_input_mes)   // 使用AppleClientMessage case class apply 方法
        }

        // 如果接收到服务器的回复
        case AppleServerMessage(server_message) => {
            print("AppleServer:" + server_message + "\nAppleCustomer:")
        }
    }

}

// 程序入口
object AppleCustomerActorMain extends App {

    val (customerIPHost, customerIPPort, serverIPHost, serverIPPort) = ("127.0.0.1", 13660, "127.0.0.1", 13666)

    /**
      * 第一步:创建config对象,指定协议类型,监听IP和端口PORT
      */

    val config = ConfigFactory.parseString(
        s"""
           |akka.actor.provider="akka.remote.RemoteActorRefProvider"
           |akka.remote.netty.tcp.hostname=$customerIPHost
           |akka.remote.netty.tcp.port=$customerIPPort
           |akka.actor.warn-about-java-serializer-usage=false
         """.stripMargin)

    // 第二步:创建ActorSystem
    private val appleCustomerActorSystem = ActorSystem("AppleCustomerActorSystem", config)
    // 第三步:创建CustomerActor的实例和引用
    private val appleCustomerActorRef: ActorRef = appleCustomerActorSystem
                                                    .actorOf(Props(new AppleCustomerActor(serverIPHost, serverIPPort))
                                                    , "appleCustomerActorRef")
    // 第四步:启动CustomerRef(Actor)
    appleCustomerActorRef ! "Apple Customer Start"

    // 客户端可以循环发送消息到服务端
    while(true) {
        val input_mes = StdIn.readLine()
        appleCustomerActorRef ! input_mes
    }
}
===============================客户端代码===============================

===============================协议端代码===============================
package com.lj.akka.common

/**
  * @author Administrator
  * @create 2020-03-19
  * 说明:使用样例类来构建协议
  */

// 客户端发送消息给服务端协议(序列化对象)
case class AppleClientMessage(customer_message: String)

// 服务端发送消息给客户端协议
case class AppleServerMessage(server_message: String)
===============================协议端代码===============================

示例效果图:

服务端

在这里插入图片描述

客户端

在这里插入图片描述

对以前的知识回顾,加深基础知识!
学习来自:北京尚硅谷韩顺平老师—尚硅谷大数据技术之Scala
每天进步一点点,也许某一天你也会变得那么渺小!!!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值