为什么80%的码农都做不了架构师?>>>   

频道是Phoenix中非常exciting和强大的一部分。它让我们能简单地为应用添加软实时特性。频道基于一个简单的想法 - 收发信息。发送者发布关于话题的信息。接收者关注了这个话题,然后他们就能得到那些信息。发送者和接收者在任何时候都能互换角色。

Elixir本身就是基于信息传送的,你可能会想知道为什么我们需要这个额外的机制来收发信息。使用频道,发送者和接收者都可以不是Elixir进程。它们可以使任何能与频道交流的东西 - JavaScript客户端,iOS应用,另一个Phoenix应用,我们的手表。而且,一个频道中广播的信息可能会有很多接收者。而Elixir进程的交流是一对一的。

“频道”这个词很难描述一个有着许多组件的分层系统。让我们快速浏览一下它们,以便看到它的全貌。

配件

  • Socket Handlers

Phoenix有一个到服务器的连接,在这个连接上会多路复用你的频道sockets。Socket handlers,例如web/channels/user_socket.ex,是一个模块,用于验证和鉴定一个socket连接,并允许你为所有频道设置默认socket。

  • Channel Routes

它们定义与Socket handlers中,例如web/channels/user_socket.ex,这使得它们与别的routes不同。它们匹配与话题字符串,并调遣匹配任务 到给定的频道模块。星形符*的作用像是通配符,所以在下面的route例子中,对sample_topic:pizzasample_topic:oranges的请求都会被调遣到SampleTopicChannel

channel "sample_topic:*", HelloPhoenix.SampleTopicChannel
  • Channels

Channels处理客户端来的事件,所以和控制器类似,但有两个关键的不同。Channel 事件是双向的 - 进和出。频道连接也存在于单个请求/回应的循环之外。频道是Phoenix中实时交流组件的最高等级抽象。

每个频道都会为join/3, terminate/2, handle_in/3, 和 handle_out/3这四个回调函数中的每一个实现一个或更多从句。

  • PubSub

Phoenix PubSub层由Phoenix.PubSub模块和各种不同适配器和它们的GenServer的模块组成。这些模块中包含了组成频道交流的函数 - 关注话题,取消关注,在话题内广播信息。

如果需要的话,可以定义我们自己的PubSub适配器。请到Phoenix.PubSub docs 查看更多。

单独使用这些模块对于Phoenix来说毫无意义。Channels使用它们作为自己的发动机。作为用户,我们不需要在应用中直接使用它们。

  • Messages

Phoenix.Socket.Message模块用下面的keys定义了一个结构,来表示合法message。Phoenix.Socket.Message docs。

  • topic - 话题字符串,或话题:子话题命名空间对,比如“messages”, “messages:123”

  • event - 事件名字符串,例如“phx_join”

  • payload - 消息有效载荷

  • ref - 独特字符串ref

  • Topics

Topics是字符串id - 不同层使用的名字,为了确认消息在正确的地方结束。正如我们在上面看到的,topics可以使用通配符。这对"topic:subtopic"很有用。你经常会使用你的model层中的记录ID来组成topics,例如"users:123"

  • Transports

transport层是公路的最上层。Phoenix.Channel.Transport模块负责调遣所有message对一个Channel的进出。

  • Transport Adapters

默认的transport机制来源于WebSockerts,如果WebSockets不可用,它会退回到LongPolling。使用其它的交通适配器是可能的,只要遵守适配器协议,我们也可以自己编写一个。例子请看Phoenix.Transports.WebSocket

  • 客户端库

Phoenix现在装载了它自己的JavaScript客户端。iOS, Android, 还有 C#。

尝试它们全部

让我们构建一个简单的聊天应用,来将这些东西都用上。在generating a new Phoenix application 之后,我们会看到endpoint已经为我们设置好了,在lib/hello_phoenix/endpoint.ex中:

defmodule HelloPhoenix.Endpoint douse Phoenix.Endpoint, otp_app: :hello_phoenixsocket "/socket", HelloPhoenix.UserSocket...
end

我们在endpoint中所指向的HelloPhoenix.UserSocket,已经生成应用时创建好了,在web/channels/user_socket.ex中。我们需要确认messages已经获得了到正确的channel的route。所以,我们将对"room:*" channnel定义取消注释:

defmodule HelloPhoenix.UserSocket douse Phoenix.Socket## Channelschannel "room:*", HelloPhoenix.RoomChannel...

现在,无论何时,一个客户端发送一个话题开头为"room:"的消息,都会route到我们的RoomChannel。下一步,我们将定义一个HelloPhoenix.RoomChannel模块来管理我们的聊天室消息。

加入频道

你的channels要做的第一件事就是授权客户端加入一个给定的topic。为了授权,我们必须在web/channels/room_channel.ex中实现join/3

defmodule HelloPhoenix.RoomChannel douse Phoenix.Channeldef join("room:lobby", _message, socket) do{:ok, socket}enddef join("room:" <> _private_room_id, _params, _socket) do{:error, %{reason: "unauthorized"}}end
end

对于我们的聊天app,我们允许任何人加入"room:lobby"话题,但是其他任何房间都会被认为是私人的并需要特殊授权,从数据库的角度说,是被要求的。这里,我们不必管那些私人聊天室。为了授权socket加入topic,我们返回{:ok, socket}{:ok, reply, socket}。拒绝访问,我们会返回{:error, reply}。关于token授权的更多信息,请看Phoenix.Token documentation 。

channel已经设置好,让我们的客户端和服务器开始对话吧。

Phoenix项目使用Brunch 来构建,除非你在运行mix phoenix.new时加上了--no-brunch选项。

如果你使用了brunch,在web/static/js/socket.js中会有一个基于socket实现定义的简单客户端。

我们可以使用这个库来连接到我们的socket并加入我们的channel,我们只需要将房间名"room:lobby" 放到那个文件中。

// web/static/js/socket.js
...
socket.connect()// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("room:lobby", {})
channel.join().receive("ok", resp => { console.log("Joined successfully", resp) }).receive("error", resp => { console.log("Unable to join", resp) })export default socket

之后,我们需要确认web/static/js/socket.js被import到了我们的应用JavaScript文件中。所以,将web/static/js/app.js的最后一行取消注释。

...
import socket from "./socket"

保存文件,你的浏览器应该会自动刷新,感谢Phoenix的热重载功能。如果一切正常,你会在浏览器的JavaScript控制台中看到"Joined successfully" 。我们的客户端和服务器现在通过一个持续的连接在对话。现在来开启聊天功能。

web/templates/page/index.html.eex中,我们将用一个可以容纳我们的聊天消息的容器,和一个输入框来代替已存在的代码:

<div id="messages"></div>
<input id="chat-input" type="text"></input>

我们也将在web/templates/layout/app.html.eex中添加jQuery到应用的layout:

  ...<%= render @view_module, @view_template, assigns %></div> <!-- /container --><script src="//code.jquery.com/jquery-1.12.4.min.js"></script><script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>

现在让我们添加几个事件监听器到web/static/js/socket.js

...
let channel           = socket.channel("room:lobby", {})
let chatInput         = $("#chat-input")
let messagesContainer = $("#messages")chatInput.on("keypress", event => {if(event.keyCode === 13){channel.push("new_msg", {body: chatInput.val()})chatInput.val("")}
})channel.join().receive("ok", resp => { console.log("Joined successfully", resp) }).receive("error", resp => { console.log("Unable to join", resp) })export default socket

我们要做的就是,监测到回车被按下,然后push一个包含了消息本体的事件到channel。我们为这个事件取名为"new_msg"。让我们继续构建这个聊天应用的其他部分,包括监听新的消息和追加它们到消息容器中。

...
let channel           = socket.channel("room:lobby", {})
let chatInput         = $("#chat-input")
let messagesContainer = $("#messages")chatInput.on("keypress", event => {if(event.keyCode === 13){channel.push("new_msg", {body: chatInput.val()})chatInput.val("")}
})channel.on("new_msg", payload => {messagesContainer.append(`<br/>[${Date()}] ${payload.body}`)
})channel.join().receive("ok", resp => { console.log("Joined successfully", resp) }).receive("error", resp => { console.log("Unable to join", resp) })export default socket

我们使用channel.on来监听"new_msg" 事件,然后将消息本体追加到DOM。现在让我们来处理服务器上进出的消息,完成最后的步骤。

传入事件

我们使用handle_in/3来处理传入事件。我们可以对事件名进行模式匹配,类似"new_msg",然后抓住客户端传送给channel的payload。对于我们的聊天应用,我们只需要通过broadcast!/3通知所有其他room:lobby的关注者,有新信息。

defmodule HelloPhoenix.RoomChannel douse Phoenix.Channeldef join("room:lobby", _message, socket) do{:ok, socket}enddef join("room:" <> _private_room_id, _params, _socket) do{:error, %{reason: "unauthorized"}}enddef handle_in("new_msg", %{"body" => body}, socket) dobroadcast! socket, "new_msg", %{body: body}{:noreply, socket}enddef handle_out("new_msg", payload, socket) dopush socket, "new_msg", payload{:noreply, socket}end
end

broadcast!/3会通知所有已加入这个socket的话题的客户端,并调用它们的handle_out/3回调。handle_out/3不是一个必须的回调,但它允许我们自定义和过滤广播,在它们到达每个客户端之前。默认的,handle_out/3就只是简单地push消息到客户端,就像我们这里定义的一样。而将它和传出事件联系在一起,就能进行强大的消息自定义和过滤。

拦截传出事件

我们不会为应用实现这个,但想象一下,我们的聊天app允许用户忽略新用户加入房间的通知。我们可以这样实现它,明确地告知Phoenix,我们想要拦截的传出事件,然后为这些事件定义一个handle_out/3回调。(当然,需要假设我们有一个带有ignoring?/2函数的User model,而且我们通过assigns映射来传入user。)

intercept ["user_joined"]def handle_out("user_joined", msg, socket) doif User.ignoring?(socket.assigns[:user], msg.user_id) do{:noreply, socket}elsepush socket, "user_joined", msg{:noreply, socket}end
end

这就是我们的基础聊天app。打开多个浏览器窗口,你会看到你的消息被push并广播到了所有窗口!

Socket Assigns

与连接结构,%Plug.Conn{},类似,也有可能将值赋给一个channel socket。Phoenix.Socket.assign/3可以很方便地以assign/3的形式import到一个channel模块中:

socket = assign(socket, :user, msg["user"])

Sockets将得到的值以映射形式存放在socket.assigns

容错性和可靠性

服务器重启,网络中断,客户端失去连接。为了设计出健壮的系统,我们需要理解Phoenix是如何响应这些事件,以及是什么在保证它们。

  • 处理重连

客户端关注了话题,Phoenix将这些关注选项存放在一个内存中的ETS表格里。如果一个channel崩溃了,客户端会需要重连到它们之前关注了的话题。幸运的是,Phoenix的JavaScript客户端知道怎么做。服务器会通知所有客户端崩溃的消息。这会触发每个客户端的Channel.onError回调。客户端会试图使用一个指数后退策略来重连到服务器。一旦重连,它们会试图加入它们之前关注了的话题。一旦成功,它们会开始从这些话题继续接受消息。

  • 重新发送客户端消息

channel客户端将传出消息按队列放入一个PushBuffer,并在有连接时将它们发送到服务器。没有连接时,客户端会保留这些消息,直到建立新的连接,或者直到收到一个timeout 事件。默认的超时事件是5000毫秒。客户端不会将这些消息保留在本地存储中,所以如果浏览器窗口关闭了,消息就会消失。

  • 重新发送服务器消息

Phoenix在发送消息给客户端时,会使用一个at-most-once策略。如果客户端掉线了并丢失了消息,Phoenix不会重新发送。Phoenix不会再服务器上保留消息。如果服务器重启,未发送的消息会消失。如果我们的应用需要保证消息的送达,我们就要自己写那些代码。常用方法包括保留消息在服务器上,以及让客户端请求丢失的消息。例如,看看Chris McCord的Phoenix练习:client code 以及server code 。

应用范例

想看看我们刚才做的应用的范例,请到(https://github.com/chrismccord/phoenix_chat_example)。

你也可以看看这个demo(http://phoenixchat.herokuapp.com/)。

转载于:https://my.oschina.net/ljzn/blog/734329

Phoenix官方教程 (九) Channel相关推荐

  1. Phoenix官方教程 (一) 构建和运行

    为什么80%的码农都做不了架构师?>>>    首个教程的目的在于尽可能快地让一个Phoenix应用构建好并运行起来. 在我们开始前,请花一分钟阅读Installation Guid ...

  2. Digital Vision Phoenix 2019(凤凰电影修复软件)官方正式版V2019.1 R2 | 数字电影修复软件下载 | 含Digital Vision Phoenix安装教程

                         Digital Vision Phoenix 是一款经典老牌同时也是世界顶级水准的专业电影修复软件,也叫凤凰修复软件,拥有超过25年的图像处理.数字视觉经验和 ...

  3. Caffe官方教程翻译(10):Editing model parameters

    前言 最近打算重新跟着官方教程学习一下caffe,顺便也自己翻译了一下官方的文档.自己也做了一些标注,都用斜体标记出来了.中间可能额外还加了自己遇到的问题或是运行结果之类的.欢迎交流指正,拒绝喷子! ...

  4. Unity官方教程Ruby大冒险的自学笔记

    Unity官方教程Ruby大冒险的自学笔记 一. //正确例子: void Update(){//获取运动矢量moveX = Input.GetAxisRaw("Horizontal&quo ...

  5. TensorFlow2.0 Guide官方教程 学习笔记17 -‘Using the SavedModel format‘

    本笔记参照TensorFlow官方教程,主要是对'Save a model-Training checkpoints'教程内容翻译和内容结构编排,原文链接:Using the SavedModel f ...

  6. pytorch官方教程中文版(一)PyTorch介绍

    pytorch编程环境是1.9.1+cu10.2 建议有能力的直接看官方网站英文版! 下面所示是本次教程的主要目录: pytorch官方教程中文版: PyTorch介绍 学习PyTorch 图像和视频 ...

  7. UE官方教程笔记01-实时渲染基础上

    对官方教程视频[官方培训]01-实时渲染基础上 | 陈拓 Epic的笔记 部分没听懂的地方就按自己的理解瞎写了 介绍 实时渲染(Real-Time Rendering,RTR)是指在计算机上快速生成图 ...

  8. Ubuntu Touch 和 Android 双系统安装官方教程

    Ubuntu Touch 和 Android 双系统安装官方教程 时间:2016-02-19 22:32来源:未知 作者:在下不才 举报 点击:63次 Ubuntu 双系统安装器是作为技术预览版本发布 ...

  9. opencv python下载_[福利] OpenCV4 Python 最新中文版官方教程来了(附下载)

    教程简介 OpenCV 是计算机视觉中经典的专用库,然而其中文版官方教程久久不来.近日,一款最新 OpenCV4.1 版本的完整中文版官方教程出炉,读者朋友可以更好的学习了解 OpenCV 相关细节. ...

最新文章

  1. 4行代码,让app自动化框架支持 webview 混合应用操作
  2. char* 和jstring转换
  3. [YTU]_2633( P3 数钱是件愉快的事)
  4. mysql 字段唯一性问题
  5. Office文档上传后实时转换为PDF格式_图片文件上传后实时裁剪_实现在线预览Office文档
  6. 分布式模块之间的调用_分布式事务
  7. java打印杨辉三角_java算法之打印杨辉三角
  8. Java RMI 服务易受 SSRF 攻击
  9. win10录屏怎么用_怎么用Win10电脑系统进行录音教你两种简单实用的方法
  10. 衡量神经网络的三个指标,参数量,multi-add,flop计算力
  11. python 正则表达式1
  12. 通用规范汉字表 一级字表(3500字)
  13. java的程序的创建快捷方式_关于Swing:如何为Java程序创建快捷方式图标
  14. JVM 1.8 永久代---元空间 的变动
  15. Windows7下pip安装包报错 Microsoft Visual C++ 9 0 is required Unabl
  16. 通过 pip 安装软件时提示 0.1.36ubuntu1,0.23ubuntu1 is an invalid version 的完美解决方案。
  17. 根据某个特定字符截取字符串(js)
  18. Hadoop学习——Hadoop概述
  19. win2008不能连接mysql_win2008 r2 安装sql server 2005/2008 无法连接服务器解决方法
  20. LeetCode--597. 好友申请 I :总体通过率

热门文章

  1. arduino 步进电机驱动库_Arduino驱动 步进电机
  2. 高德地图自定义点标记踩坑
  3. 浅谈网页设计的形式美法则
  4. 《图像超分辨率研究综述》笔记
  5. win10家庭版用户实现远程桌面解决办法
  6. linux驱动工程面试必问知识点
  7. ESP32基础应用之使用两个ESP32通过阿里云物联网平台实现相互通信
  8. MIGO为玩家带来接近无限的可能
  9. 怎么做国外问卷调查站点查
  10. python用bbp公式计算圆周率_圆周率π现在已经算到多少位了?具体是什么数字?...