Search code examples
erlangwebsocketyaws

Using Yaws and websockets to subscribe to data


I have a gen_server speaking to several hardware sensors. This setup works fine. Now I want to extend this system with a real time visualization of the data. And I want to deliver this data to a web client over websockets.

As I might have several "listeners", e.g. several people visualizing this data on different web browsers, I am thinking of something resembling the Observer Pattern. Each time a web client asks to subscribe to a sensor, it should be added to a list of stakeholders for that sensor. As soon as new sensor data arrives, it should be pushed out to the client without delay.

I am using yaws to quickly get websocket functionality. My problem is related to the way that yaws seems to work. On the server side I only "see" the client at connection time through the A#arg.clisock value (e.g. #Port<0.2825>). In the code below I register ws_server to receive the callbacks when new data enters from the client. After this point yaws seems to only allow me to respond to messages entering server side.

out(A) ->
  CallbackMod = ws_server,
  Opts = [{origin, "http://" ++ (A#arg.headers)#headers.host}],

  {websocket, CallbackMod, Opts}.

This is what the callback module looks like:

% handle_message(Incoming)
% Incoming :: {text,Msg} | {binary,Msg} | {close, Status, Reason}
handle_message({Type,Data}) ->
  gen_server:cast(?SERVER,{websocket,Data}),
  noreply.

Nowhere, it seems, am I able to react to a message such as subscribe <sensor> and (after connection time) dynamically add this stakeholder to a list of observers.

How can I use the yaws server to accomplish asynchronous pushing of data to client and during session add and remove sensors that I want to listen to. Basically the easiest way would be if yaws could call back handle_message/2 with first argument being From. Otherwise I need to add a ref to keep a ref on both sides and send that to server each time, it seems backwards that I need to keep that information.


Solution

  • When the client starts a websocket connection Yaws will start a new gen_server process to handle all communication between client and server. The gen_server dispatches all client-sent messages to your callback function handle_message/1, so it is not used only for the initial connect.

    Also, the gen_server process is used to push data from the server to the client - use the yaws_api:websocket_send(Pid, {Type, Data}) function. The Pid parameter is the pid of the websocket gen_server. Your callback module could be changed to:

    handle_message({Type,Data}) ->
          gen_server:cast(?SERVER,{self(), Data}),
          noreply.
    

    This gives your server the pid of the websocket gen_server to be used with yaws_api:websocket_send/2. The Data parameter contains the client request and needs to be parsed by your server to check for sensor subscription requests and associate the websocket pid with the appropriate sensor.