Elixir Plug Cowboy, WebSockets 브로드캐스팅으로 반응
덕분에 클라이언트가 요청하지 않고도 클라이언트에 데이터를 푸시할 수 있습니다. 주식 시세 목록, 게임 서버 점수표, 암호화폐 가격, 베팅 홀수 표시 또는 실시간 데이터에 매우 유용한 것은 무엇입니까?
기본적으로 하나의 이미 터와 많은 소비자가 있습니다.
이 기사에서는 주식 시세 목록을 구현합니다.
여기서 주식 값은 특정 시간 간격으로 모든 클라이언트에게 전송됩니다.
구조는 다음으로 구성됩니다.
Quotes store: 주가가 저장되고 가격 변동을 시뮬레이트합니다.
스케줄러: 일정 시간마다 브로드캐스트를 트리거합니다.
클라이언트 레지스트리: 모든 클라이언트에는 PID가 있으며 업데이트를 보낼 주소로 PID가 필요합니다.
소켓 처리기: 클라이언트 통신을 처리합니다.
클라이언트 측에는 따옴표 테이블을 수신하고 표시하는 React 앱이 있습니다.
목차
1) 종속성
Showing the dependencies is the classic way to start an Elixir post.
defp deps do
[
{:cowboy, "~> 2.4"},
{:plug_cowboy, "~> 2.0"},
{:jason, "~> 1.3"},
]
end
2) 견적 판매점
A Elixir Agent is used in order to store the stock quote prices and simulate price changes.
Elixir Agent설명서 내용: 서로 다른 프로세스 또는 동일한 프로세스가 서로 다른 시점에서 액세스해야 하는 상태를 공유하거나 저장해야 할 때 유용합니다.
defmodule Quote do
@derive {Jason.Encoder, only: [:symbol, :name, :price, :share, :arrow, :dif, :difp]}
defstruct [:symbol, :name, :price, :share, :arrow, :dif, :difp]
def new({symbol, name, price, share}) do # I love C++
%Quote{symbol: symbol, name: name, price: price, share: share}
end
end
defmodule QuoteStore do
use Agent
def initial_values() do
[
{"BRK.B", "Berkshire Hathaway", 289.30, 637},
{"JPM", "JPMorgan Chase & Co.", 115.52, 342},
{"BAC", "Bank of America", 34.41, 308},
{"WFC", "Wells Fargo & Company", 44.30, 168},
{"MS", "Morgan Stanley", 88.30, 151},
{"HSBC", "HSBC", 31.41, 128},
{"AXP", "American Express", 157.33, 118},
{"GS", "The Goldman Sachs Group", 340.18, 116},
{"AAPL", "Apple Inc. ", 167.23, 368},
{"MSFT", "Microsoft Corporation", 276.44, 340},
{"GOOG", "Alphabet Inc.", 114.77, 200},
{"AMZN", "Amazon.com,", 133.62, 500},
{"TSLA", "Tesla, Inc", 889.36, 120},
{"NVDA", "NVIDIA Corporation ", 171, 512}
]
|> Enum.map(&Quote.new(&1))
end
def start_link(quotes) do
IO.puts("QuoteStore.start_link, length(quotes): #{length(quotes)}")
Agent.start_link(fn -> quotes end, name: __MODULE__)
end
def values do
Agent.get(__MODULE__, & &1)
end
def update do
values = Enum.map(Agent.get(__MODULE__, & &1), fn q -> newValue(q) end)
Agent.update(__MODULE__, fn _ -> values end)
values
end
def newShare(value), do: value * (101..105 |> Enum.random()) / 100
def newPrice(value), do: value * (90..110 |> Enum.random()) / 100
defp newValue(q) do
newprice = newPrice(q.price)
newshare = newShare(q.share)
dif = Float.round(newprice - q.price, 2)
# IO.puts(dif)
difp = dif / q.price * 100
# 2% threshold
arrow =
cond do
difp > 2 -> "u"
difp < -2 -> "d"
true -> "e"
end
%Quote{
symbol: q.symbol,
name: q.name,
price: newprice,
share: newshare,
arrow: arrow,
dif: dif,
difp: difp
}
end
end
3) 스케줄러
The scheduler will be in charge of trigger the broadcasting process. Every specific time interval it send a message to all the PID stored in the Registry. The Registry is based in ETS (Erlang Term Storage). The PID of every new connection is stored in the Registry, it will be showed bellow, in the Socket Handler.
Elixir RegistryErlang Term Storage
defmodule Scheduler do
def start(interval) do
IO.inspect(self(), label: "Scheduler Start")
:timer.apply_interval(interval, __MODULE__, :send_quotes, [])
{:ok, self()}
end
def send_quotes() do
IO.inspect(self(), label: "Broadcast messages from")
Registry.StockTickerApp
|> Registry.dispatch("stock_ticker", fn entries ->
for {pid, _} <- entries, do: send(pid, :broadcast, [])
end)
end
end
5) 소켓 핸들러
The start point to understand socket handler is the documentation: Cowboy WebSocketdefmodule StockTickerApp.SocketHandlerTicker do
@behaviour :cowboy_websocket
require Logger
# Not run in the same process as the Websocket callbacks.
def init(request, _state) do
{:cowboy_websocket, request, nil, %{idle_timeout: :infinity}}
end
# websocket_init: Called once the connection has been upgraded to Websocket.
def websocket_init(state) do
Registry.StockTickerApp |> Registry.register("stock_ticker", {})
str_pid = to_string(:erlang.pid_to_list(self()))
IO.puts("websocket_init: #{str_pid}")
stime = String.slice(Time.to_iso8601(Time.utc_now()), 0, 8)
{:ok, json} = Jason.encode(%{time: stime, quotes: QuoteStore.update()})
{:reply, {:text, json}, state}
end
# websocket_handle: Will be called for every frame received
# Can be used to retrieve info about some specific quote ex: "TSLA"
# but in this case we don't needed.
# def websocket_handle({:text, json}, state) do
# end
# websocket_info: Will be called for every Erlang message received.
# the scheduler send messages to every PID stored in the Registry
def websocket_info(:broadcast, state) do
stime = String.slice(Time.to_iso8601(Time.utc_now()), 0, 8)
{:ok, json} = Jason.encode(%{time: stime, quotes: QuoteStore.update()})
{:reply, {:text, json}, state}
end
end
3) 신청
The application is the core where all the involved processes will be started.
defmodule StockTickerApp do
use Application
def start(_type, _args) do
children = [
{QuoteStore, QuoteStore.initial_values()},
%{id: Scheduler, start: {Scheduler, :start, [5000]}},
Plug.Cowboy.child_spec(
scheme: :http,
plug: StockTickerApp.Router,
options: [dispatch: dispatch(), port: 5000]
),
Registry.child_spec(
keys: :duplicate,
name: Registry.StockTickerApp
)
]
opts = [strategy: :one_for_one, name: StockTickerApp.Application]
Supervisor.start_link(children, opts)
end
defp dispatch do
[
{:_,
[
{"/ws/[...]", StockTickerApp.SocketHandlerTicker, []}
]}
]
end
end
6) 반응 클라이언트
In the client side is a React app, where the Material UI library will provide us of a nice look and feel user interface. Material UIReact에서 WebSocket을 구현하는 방법은 여러 가지가 있습니다. 이 방법이 더 간단합니다.
외부 라이브러리가 없는 원시 구현만 있습니다.
//socket.js
const wsHost = "ws://192.168.1.109:5000/ws/chat";
export var websocket = new WebSocket(wsHost);
부모 자식 구성 요소 관계가 활성화됩니다.
Dashboard
및 quotes_table
두 구성 요소입니다.계기반
Dashboard
는 소켓 연결을 처리하고 받은 견적을 해당 상태로 저장합니다.import React from 'react';
import Typography from '@material-ui/core/Typography';
import QuotesTable from "./quotes_table";
import {websocket} from "./socket.js";
class Dashboard extends React.Component {
constructor(props) {
super(props);
this.state = { status: "Disconnected", quotes:[]};
}
setStatus = (evt,value) => {
console.log("setStatus",value);//,Object.keys(evt));
//console.log("isTrusted",evt.isTrusted);
this.setState({status: value});
}
onMessage= (evt) => {
console.log(" websocket.onmessage:");
var data = JSON.parse(evt.data);
console.log("Data", data);
this.setState({quotes: data.quotes, time : data.time});
}
componentDidMount() {
console.log("Dashboard componentDidMount:");
websocket.onopen = (evt) => this.setStatus(evt,"Open");
websocket.onclose = (evt) => this.setStatus(evt,"Close");
websocket.onerror = (evt) => this.setStatus(evt,"Error");
websocket.onmessage = (evt) => this.onMessage(evt);
}
render() {
console.log("DashBoard: render", this.state.quotes.length);
return (
<div style={{margin: "0 auto",padding:"5px", border: "2px solid gray", maxWidth: "35%" , display: "flex", flexDirection: "column"}} >
<QuotesTable quotes={this.state.quotes} style={{border: "1px solid yellow"}} />
<Typography >
Last update:{this.state.time} ,Status: {this.state.status}
</Typography>
</div>
)}}
export default Dashboard;
따옴표 테이블
끝으로
QuotesTable
가 있습니다. 여기에서 모든 주식 시세는 잘 알려진 시각적 해석을 보여주기 위해 화살표와 색상으로 표시됩니다.import React from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles({
customTable: {
"& .MuiTableCell-sizeSmall": {
padding: "2px 6px 2px 6px "
},
"& .MuiTableCell-head": {
fontWeight: "bold",
backgroundColor: "#00a0a0"
}
,
"& .MuiTableRow-root": {
height: "40px"
}
,
"& .MuiTableRow-root:hover": {
backgroundColor: "#c0c0c0"
}
},
});
function drawArrowIcon(d){
if (d==="u") return (<ArrowDropUpIcon style={{fill:"green", transform: "scale(1.5)"}}/>);
if (d==="d") return (<ArrowDropDownIcon style={{fill:"red", transform: "scale(1.5)"}}/>);
}
function getColor(d){
if (d==="u") return ("green");
if (d==="d") return ("red");
return "black";
}
export default function QuotesTable(props) {
const { quotes } = props;
console.log("QuotesTable: render", quotes.length);
const classes = useStyles();
return (
<Table classes={{root: classes.customTable}}
style={{ width: "auto", tableLayout: "auto", border: "2px solid gray" }} size="small" >
<TableHead >
<TableRow >
<TableCell >Symbol</TableCell>
<TableCell align="right">Price</TableCell>
<TableCell align="right">Move</TableCell>
<TableCell align="right">Dif</TableCell>
<TableCell align="right">Dif%</TableCell>
<TableCell align="right">Share</TableCell>
</TableRow>
</TableHead>
<TableBody>
{quotes && quotes.map((row) => (
<TableRow key={row.symbol} onClick={() => { console.log("Open info for:",row.symbol); }} >
<TableCell align="left">{row.symbol}</TableCell>
<TableCell align="right">{row.price.toFixed(2)} </TableCell>
<TableCell align="center" >{drawArrowIcon(row.arrow) }</TableCell>
<TableCell align="right" style={{color: getColor(row.arrow)}} >{row.dif.toFixed(2)}</TableCell>
<TableCell align="right" style={{color: getColor(row.arrow)}} >{row.difp.toFixed(2)}</TableCell>
<TableCell align="right">{row.share.toLocaleString(undefined, {minimumFractionDigits: 2,maximumFractionDigits: 2})}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
Reference
이 문제에 관하여(Elixir Plug Cowboy, WebSockets 브로드캐스팅으로 반응), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/lionelmarco/elixir-plug-cowboy-websockets-broadcasting-to-react-46if텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)