Elixir Plug Cowboy, WebSockets 브로드캐스팅으로 반응

56540 단어 websocketselixirreact
WebSockets는 웹 클라이언트에 콘텐츠를 푸시하는 가장 효과적인 방법입니다. 양쪽 간의 연결은 HTTP 핸드셰이킹으로 시작한 다음 WebSockets로 업그레이드되어 양쪽에서 데이터를 주고받을 수 있는 전이중 채널을 설정합니다.

덕분에 클라이언트가 요청하지 않고도 클라이언트에 데이터를 푸시할 수 있습니다. 주식 시세 목록, 게임 서버 점수표, 암호화폐 가격, 베팅 홀수 표시 또는 실시간 데이터에 매우 유용한 것은 무엇입니까?

기본적으로 하나의 이미 터와 많은 소비자가 있습니다.

이 기사에서는 주식 시세 목록을 구현합니다.
여기서 주식 값은 특정 시간 간격으로 모든 클라이언트에게 전송됩니다.

구조는 다음으로 구성됩니다.

  • Quotes store: 주가가 저장되고 가격 변동을 시뮬레이트합니다.

  • 스케줄러: 일정 시간마다 브로드캐스트를 트리거합니다.

  • 클라이언트 레지스트리: 모든 클라이언트에는 PID가 있으며 업데이트를 보낼 주소로 PID가 필요합니다.

  • 소켓 처리기: 클라이언트 통신을 처리합니다.

  • 클라이언트 측에는 따옴표 테이블을 수신하고 표시하는 React 앱이 있습니다.


    목차


  • 1-Dependencies
  • 2-Quote Store
  • 3-The Scheduler
  • 4-Socket Handler
  • 5-The application
  • 6-React Client

  • 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 Registry
    Erlang 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 WebSocket

    defmodule 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 UI

    React에서 WebSocket을 구현하는 방법은 여러 가지가 있습니다. 이 방법이 더 간단합니다.
    외부 라이브러리가 없는 원시 구현만 있습니다.

    //socket.js
    const wsHost = "ws://192.168.1.109:5000/ws/chat";
    export var websocket = new WebSocket(wsHost);
    
    


    부모 자식 구성 요소 관계가 활성화됩니다.Dashboardquotes_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>
    
      );
    }
    
    

    좋은 웹페이지 즐겨찾기