Tutorial: Using Cowboy Gun Client WebSocket

Jeevan Singh
3 min readMar 20, 2018

Understanding implementation of cowboy gun as client websocket.

Gun is a http client with websocket support. Establishing a websocket connection with gun requires handling of gun events as it connects. Lets start straight with the code

-module(gun_ex).
-export([connection/1]).
connection(State) ->
receive
start ->
#{host := Host,port := Port} = State,
{ok,WPID} = gun:open(Host,Port),
connection(State#{wpid => WPID});
{gun_up,WPID,_Proto} ->
#{path := Path} = State,
gun:ws_upgrade(WPID,Path,[],#{compress => true}),
connection(State);
{gun_down,_WPID,ws,closed,_,_} ->
connection(State);
{gun_upgrade,_WPID,_Ref,_Proto,_Data} ->
connection(State);
{gun_response, _WPID, _Ref, _Code, _HttpStatusCode, _Headers} ->
connection(State);
{gun_error, _WPID, _Ref, _Reason} ->
connection(State);
{'DOWN',_PID,process,_WPID,_Reason} ->
connection(State);
{gun_ws, _WPID, Frame} ->
case Frame of
close ->
self() ! stop;
{close,_Code,_Message} ->
self() ! stop;
{text,TextData} ->
io:format("Received Text Frame: ~p~n",[TextData]);
{binary,BinData} ->
io:format("Received Binary Frame: ~p~n",[BinData]);
_ ->
io:format("Received Unhandled Frame: ~p~n",[Frame])
end,
connection(State);
stop ->
#{wpid := WPID} = State,
gun:flush(WPID),
gun:shutdown(WPID);
Message ->
io:format("Received Unknown Message on Gun: ~p~n",[Message]),
connection(State)
after 30 * 1000 ->
Socket = maps:get(wpid,State,notfound),
case Socket of
notfound ->
ok;
_ ->
gun:ws_send(Socket,ping)
end,
connection(State)
end.

Actions and Events

The connection server accept a parameter State in the following form

#{host => "example.com",port => 80/443, path => "/ws"}

When it receives the event “start” , it asks gun to connect with given host and port address

{ok,WPID} = gun:open(Host,Port)

Here WPID is the child process which actually establishes a connection.

If the gun connects successfully, it sends an event to parent process

{gun_up,WPID,Protocol} 

Here WPID is the child process Id and Protocol it connects with, usually http.

If gun is not able to establish a connection due to network connectivity or server being down, it will send another event

{gun_response, WPID, Ref, Code, HttpStatusCode, Headers}

Here Ref is the monitor reference, monitoring the child process, Code is either fin/nofin, HttpStatusCode for error — 503/404, Headers are http headers.

When the connection is established, we ask gun to upgrade the connection to websocket

gun:ws_upgrade(WPID,Path,[],#{compress => true}),

Here Path is websocket handler path on the server — “/ws” which was given in the State parameter. When the connection upgrade is successful we receive following event

{gun_upgrade,_WPID,_Ref,_Proto,_Data}

At this point you can start sending messages on websocket connection with following command

gun:ws_send(WPID,MessageFrame)

Here WPID is the child process PID and MessageFrame could be following usually used

ping|{text,TextData}|{binary,BinaryData}

As a note, websocket connections are usually disconnected if theres no transfer of data for 50–60 seconds. If this is something not desired, you should set up a timer to send a ping to server after every 30–40 seconds.

When messages are received on websocket connection, gun sends them to parent process as an event

{gun_ws,_WPID,Frame}

Where a Frame can be either of following

close|{close,Code,Message}|{text,TextData}|{binary,BinaryData}

Websocket connections follow the standards to send a close frame before actually closing the connection from either side (client or server). Its optional to send it with a payload.

At any point of time if the gun connection breaks up or a close frame is received, it sends following event to parent process

{gun_down,_WPID,ws,closed,_,_}

Gun process is supervised. So if this happens, gun will try to re-establish the connection.

If the child process itself goes down, we receive following event in the parent process

{'DOWN',_PID,process,_WPID,_Reason}

Or if the child process faces any error, we receive following event

{gun_error, _WPID, _Ref, _Reason}

This can also be implemented using gen_statem behaviour. Use callback mode handle_event_function and handle all the events from gun in info.

The above code is also available here

If you have any question, shoot a mail to g1@abona.in

Happy Hunting With The Gun !!

--

--