Related
I am experimenting with Suave to send a stream of data updates; I want to replace a system we have that does polling with a socket implementation.
Here's some code:
let updateStreamSocket (webSocket : WebSocket) (context: HttpContext) =
socket {
printfn "connection"
candleUpdateEvent.Publish.Add(fun d ->
(webSocket.send Binary (d |> ByteSegment) true |> Async.RunSynchronously |> ignore)
)
let mutable loop = true
while loop do
let! msg = webSocket.read()
match msg with
| (Close, _, _) ->
let emptyResponse = [||] |> ByteSegment
do! webSocket.send Close emptyResponse true
loop <- false
| _ -> ()
printfn "disconnection"
}
Since I'm testing, I just care about the Close message, but eventually I'll have to process the Text messages to handle subscriptions.
The model is that data gets processed and each batch triggers an event (through a mailbox processor to separate threads). In the socket code, I need to handle both the socket messages I receive but also these events to send the data.
How could I join this in a single loop and wait for either event?
Right now the event handler in the socket {} section will be added / removed with connection / disconnections, but it would be possible that the close get called and then an event arrives and tries to send data, etc.. while it works while testing, this is not right.
First of all, there is a genuine lack of documentation for Cowboy altogether and Websockets in particular but in general it is excellent to use once it is decyphered. Then getting that info from Erlang to Elixir is another step. Thanks to this post by 7stud I was able to get a functioning websocket going for testing purposes but I can't get it to listen and optionally send messages at the same time. I think this is because receive is blocking the thread which is needed to send and this is intrinsically linked to to the websocket connection so it can't send while it's waiting to receive. Maybe this understanding is flawed. I would love to be corrected. I have tried spawning to no avail which is why I am thinking the receive is blocking the websocket thread.
def ws do
localhost = 'localhost'
path = '/ws/app/1'
port = 5000
{:ok, _} = :application.ensure_all_started(:gun)
connect_opts = %{
connect_timeout: :timer.minutes(1),
retry: 10,
retry_timeout: 100
}
{:ok, conn_pid} = :gun.open(localhost, port, connect_opts)
IO.inspect(conn_pid, label: "conn_pid")
{:ok, protocol} = :gun.await_up(conn_pid)
IO.inspect(protocol, label: "protocol")
# Set custom header with cookie for device id
stream_ref = :gun.ws_upgrade(conn_pid, path, [{"cookie", "device_id=1235"}])
IO.inspect(stream_ref, label: "stream_ref")
receive do
{:gun_upgrade, ^conn_pid, ^stream_ref, ["websocket"], headers} ->
upgrade_success(conn_pid, headers, stream_ref)
{:gun_response, ^conn_pid, _, _, status, headers} ->
exit({:ws_upgrade_failed, status, headers})
{:gun_error, _conn_pid, _stream_ref, reason} ->
exit({:ws_upgrade_failed, reason})
whatever ->
IO.inspect(whatever, label: "Whatever")
# More clauses here as needed.
after 5000 ->
IO.puts "Took too long!"
:erlang.exit("barf!")
end
:ok
end
def upgrade_success(conn_pid, headers, stream_ref) do
IO.puts("Upgraded #{inspect(conn_pid)}. Success!\nHeaders:\n#{inspect(headers)}\n")
IO.inspect(self(), label: "upgrade self")
# This one runs and message is received
run_test(conn_pid)
# This should spawn and therefore not block
listen(conn_pid, stream_ref)
# This never runs
run_test(conn_pid)
end
def listen(conn_pid, stream_ref) do
spawn receive_messages(conn_pid, stream_ref)
end
def receive_messages(conn_pid, stream_ref) do
IO.inspect conn_pid, label: "conn_pid!"
IO.inspect stream_ref, label: "stream_ref!"
IO.inspect(self(), label: "self pid")
receive do
{:gun_ws, ^conn_pid, ^stream_ref, {:text, msg} } ->
IO.inspect(msg, label: "Message from websocket server:")
other_messages ->
IO.inspect(other_messages, label: "Other messages")
after 5000 ->
IO.puts "Receive timed out"
end
receive_messages(conn_pid, stream_ref)
end
def send_message(message, conn_pid) do
:gun.ws_send(conn_pid, {:text, message})
end
def run_test(conn_pid) do
IO.puts "Running test"
message = "{\"type\":\"init\",\"body\":{\"device_id\":1234}}"
send_message(message, conn_pid)
end
def stop(conn_pid) do
:gun.shutdown(conn_pid)
end
From the gun docs:
Receiving data
Gun sends an Erlang message to the owner process for every Websocket
message it receives.
and:
Connection
...
Gun connections
...
A Gun connection is an Erlang process that manages a socket to a
remote endpoint. This Gun connection is owned by a user process that
is called the owner of the connection, and is managed by the
supervision tree of the gun application.
The owner process communicates with the Gun connection by calling
functions from the module gun. All functions perform their respective
operations asynchronously. The Gun connection will send Erlang
messages to the owner process whenever needed.
Although it's not specifically mentioned in the docs, I'm pretty sure the owner process is the process that calls gun:open(). My attempts also reveal that the owner process has to call gun:ws_send(). In other words, the owner process has to both send messages to the server and receive the messages from the server.
The following code operates gun with a gen_server in such a way that the gen_server both sends messages to the server and receives messages from the server.
When gun receives a message from the cowboy http server, gun sends the message, i.e. Pid ! Msg, to the owner process. In the following code, the gen_server creates the connection in the init/1 callback, which means that gun will bang (!) messages that it receives from cowboy at the gen_server. A gen_server handles messages sent directly to its mailbox with handle_info().
In handle_cast(), the gen_server uses gun to send requests to cowboy. Because handle_cast() is asynchronous, that means you are able to send asynchronous messages to cowboy. And, when gun receives a message from cowboy, gun sends(!) the message to the gen_server, and the gen_server's handle_info() function handles the message. Inside handle_info(), gen_server:reply/2 is called to relay the message to the gen_server client. As a result, the gen_server client can jump into a receive clause whenever it wants to check the server messages sent from gun.
-module(client).
-behavior(gen_server).
-export([start_server/0, send_sync/1, send_async/1, get_message/2, go/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
-export([terminate/2, code_change/3]). %%% client functions
-export([sender/1]).
%%% client functions
%%%
start_server() ->
gen_server:start({local, ?MODULE}, ?MODULE, [], []).
send_sync(Requ) ->
gen_server:call(?MODULE, Requ).
send_async(Requ) ->
gen_server:cast(?MODULE, {websocket_request, Requ}).
get_message(WebSocketPid, ClientRef) ->
receive
{ClientRef, {gun_ws, WebSocketPid, {text, Msg} }} ->
io:format("Inside get_message(): Ref = ~w~n", [ClientRef]),
io:format("Client received gun message: ~s~n", [Msg]);
Other ->
io:format("Client received other message: ~w~n", [Other])
end.
receive_loop(WebSocketPid, ClientRef) ->
receive
{ClientRef, {gun_ws, WebSocketPid, {text, Msg} }} ->
io:format("Client received Gun message: ~s~n", [Msg]);
Other ->
io:format("Client received other message: ~w~n", [Other])
end,
receive_loop(WebSocketPid, ClientRef).
go() ->
{ok, GenServerPid} = start_server(),
io:format("[ME]: Inside go(): GenServerPid=~w~n", [GenServerPid]),
[{conn_pid, ConnPid}, {ref, ClientRef}] = send_sync(get_conn_pid),
io:format("[ME]: Inside go(): ConnPid=~w~n", [ConnPid]),
ok = send_async("ABCD"),
get_message(ConnPid, ClientRef),
spawn(?MODULE, sender, [1]),
ok = send_async("XYZ"),
get_message(ConnPid, ClientRef),
receive_loop(ConnPid, ClientRef).
sender(Count) -> %Send messages to handle_info() every 3 secs
send_async(lists:concat(["Hello", Count])),
timer:sleep(3000),
sender(Count+1).
%%%%%% gen_server callbacks
%%%
init(_Arg) ->
{ok, {no_client, ws()}}.
handle_call(get_conn_pid, From={_ClientPid, ClientRef}, _State={_Client, WebSocketPid}) ->
io:format("[ME]: Inside handle_call(): From = ~w~n", [From]),
{reply, [{conn_pid, WebSocketPid}, {ref, ClientRef}], _NewState={From, WebSocketPid} };
handle_call(stop, _From, State) ->
{stop, normal, shutdown_ok, State}; %Calls terminate()
handle_call(_Other, _From, State) ->
{ok, State}.
handle_cast({websocket_request, Msg}, State={_From, WebSocketPid}) ->
gun:ws_send(WebSocketPid, {text, Msg}), %{text, "It's raining!"}),
{noreply, State}.
handle_info(Msg, State={From, _WebSocketPid}) ->
io:format("[ME]: Inside handle_info(): Msg=~w~n", [Msg]),
gen_server:reply(From, Msg),
{noreply, State}.
terminate(_Reason, _State={_From, WebSocketPid}) ->
gun:shutdown(WebSocketPid).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%% private functions
%%%
ws() ->
{ok, _} = application:ensure_all_started(gun),
{ok, ConnPid} = gun:open("localhost", 8080),
{ok, _Protocol} = gun:await_up(ConnPid),
gun:ws_upgrade(ConnPid, "/please_upgrade_to_websocket"),
receive
{gun_ws_upgrade, ConnPid, ok, Headers} ->
io:format("[ME]: Inside gun_ws_upgrade receive clause: ~w~n",
[ConnPid]),
upgrade_success_handler(ConnPid, Headers);
{gun_response, ConnPid, _, _, Status, Headers} ->
exit({ws_upgrade_failed, Status, Headers});
{gun_error, _ConnPid, _StreamRef, Reason} ->
exit({ws_upgrade_failed, Reason})
after 1000 ->
exit(timeout)
end.
upgrade_success_handler(ConnPid, _Headers) ->
io:format("[ME]: Inside upgrade_success_handler(): ~w~n", [ConnPid]),
ConnPid.
=======
Whoops, the answer below shows how to get the server to push data to the client.
Okay, I got it--in erlang. This example is a little bit tortured. You need to do a couple of things:
1) You need to get the pid of the process running the websocket_* functions, which is not the same as the pid of the request:
Post-upgrade initialization
Cowboy has separate processes for handling the connection and
requests. Because Websocket takes over the connection, the Websocket
protocol handling occurs in a different process than the request
handling.
This is reflected in the different callbacks Websocket handlers have.
The init/2 callback is called from the temporary request process and
the websocket_ callbacks from the connection process.
This means that some initialization cannot be done from init/2.
Anything that would require the current pid, or be tied to the current
pid, will not work as intended. The optional websocket_init/1 can be
used [to get the pid of the process running the websocket_ callbacks]:
https://ninenines.eu/docs/en/cowboy/2.6/guide/ws_handlers/
Here's the code I used:
init(Req, State) ->
{cowboy_websocket, Req, State}. %Perform websocket setup
websocket_init(State) ->
io:format("[ME]: Inside websocket_init"),
spawn(?MODULE, push, [self(), "Hi, there"]),
{ok, State}.
push(WebSocketHandleProcess, Greeting) ->
timer:sleep(4000),
WebSocketHandleProcess ! {text, Greeting}.
websocket_handle({text, Msg}, State) ->
timer:sleep(10000), %Don't respond to client request just yet.
{
reply,
{text, io_lib:format("Server received: ~s", [Msg]) },
State
};
websocket_handle(_Other, State) -> %Ignore
{ok, State}.
That will push a message to the client while the client is waiting for a reply to a request that the client previously sent to the server.
2) If you send a message to the process that is running the websocket_* functions:
Pid ! {text, Msg}
then that message will get handled by the websocket_info() function--not the websocket_handle() function:
websocket_info({text, Text}, State) ->
{reply, {text, Text}, State};
websocket_info(_Other, State) ->
{ok, State}.
The return value of the websocket_info() function works just like the return value of the websocket_handle() function.
Because your gun client is now receiving multiple messages, the gun client needs to receive in a loop:
upgrade_success_handler(ConnPid, Headers) ->
io:format("Upgraded ~w. Success!~nHeaders:~n~p~n",
[ConnPid, Headers]),
gun:ws_send(ConnPid, {text, "It's raining!"}),
get_messages(ConnPid). %Move the receive clause into a recursive function
get_messages(ConnPid) ->
receive
{gun_ws, ConnPid, {text, "Greeting: " ++ Greeting} } ->
io:format("~s~n", [Greeting]),
get_messages(ConnPid);
{gun_ws, ConnPid, {text, Msg} } ->
io:format("~s~n", [Msg]),
get_messages(ConnPid)
end.
Thanks to 7stud for the example code and the edits which are reflected below:
Here is my Elixir interpretation to give a basic WebSocket client for gun:
defmodule WebsocketTester.Application do
use Application
def start(_type, _args) do
path = '/ws/app/1'
port = 5000
host = 'localhost'
args = %{path: path, port: port, host: host}
children = [
{ WebSocket.Client, args }
]
Supervisor.start_link(children, strategy: :one_for_one, name: WebsocketTester.Supervisor)
end
end
defmodule WebSocket.Client do
use GenServer
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
# GenServer callbacks
def init(args) do
# Set up the websocket connection
# get > upgrade
# Initial state with gun_pid and stream_ref
# %{gun_pid: gun_pid, stream_ref: stream_ref} = ws(args)
{:ok, init_ws(args)}
end
# Give back gun_pid from state
def handle_call(:get_conn, from, %{gun_pid: gun_pid, stream_ref: stream_ref}) do
IO.inspect(gun_pid, label: "handle call gun pid")
{:reply, %{gun_pid: gun_pid, stream_ref: stream_ref}, %{from: from, gun_pid: gun_pid} }
end
# Everything else
def handle_call(other, from, state) do
IO.inspect(other, label: "other call")
IO.inspect(from, label: "from")
{:ok, state}
end
# Client sends message to server.
def handle_cast({:websocket_request, message}, %{gun_pid: gun_pid} = state) do
IO.puts message
IO.inspect(gun_pid, label: "gun_pid")
:gun.ws_send(gun_pid, {:text, message})
{:noreply, state}
end
def handle_info(message, %{from: from} = state) do
IO.inspect(message, label: "Inside handle_info(): ")
GenServer.reply(from, message)
{:noreply, state}
end
def terminate(reason, _state) do
IO.puts "Terminated due to #{reason}."
:ok
end
def code_change(_old_version, state, _extra) do
{:ok, state}
end
## Client functions
# Used for getting gun_pid from state
def send_sync(request) do
GenServer.call(__MODULE__, request)
end
# Send a message async
def send_async(request) do
GenServer.cast(__MODULE__, {:websocket_request, request})
end
# Receive a single message
def get_message(stream_ref, gun_pid) do
receive do
{^stream_ref, {:gun_ws, ^gun_pid, {:text, message} }} ->
IO.puts("Client received gun message: #{message}")
other ->
IO.inspect(other, label: "Client received other message")
end
end
# Receive all messages recursively
def receive_loop(stream_ref, gun_pid) do
IO.puts "Listening"
get_message(stream_ref, gun_pid)
receive_loop(stream_ref, gun_pid)
end
def go() do
# Get the gun_pid from state
%{gun_pid: gun_pid, stream_ref: stream_ref} = send_sync(:get_gun_pid)
IO.inspect(gun_pid, label: "Inside go(): gun_pid=")
# Send messages manually
:ok = send_async(Jason.encode!(%{type: "info", greet: "yo"}))
# Or to send just text
# :ok = send_async("yo")
# Receive messages manually
get_message(stream_ref, gun_pid)
# Start sending loop
spawn sender 1
# Start listening
receive_loop(stream_ref, gun_pid)
end
# Send messages to handle_info() every 3 secs
def sender(count) do
send_async("count is #{count}")
:timer.sleep(3000)
sender(count+1)
end
## End of client functions
# Initialize the websocket connection
def init_ws(args) do
%{ path: path, port: port, host: host} = args
{:ok, _} = :application.ensure_all_started(:gun)
connect_opts = %{
connect_timeout: :timer.minutes(1),
retry: 10,
retry_timeout: 100
}
{:ok, gun_pid} = :gun.open(host, port, connect_opts)
{:ok, _protocol} = :gun.await_up(gun_pid)
# Set custom header with cookie for device id - set_headers can be left out if you don't want custom headers
stream_ref = :gun.ws_upgrade(gun_pid, path, set_headers("I like cookies"))
receive do
{:gun_upgrade, ^gun_pid, ^stream_ref, ["websocket"], headers} ->
upgrade_success(gun_pid, headers, stream_ref)
{:gun_response, ^gun_pid, _, _, status, headers} ->
exit({:ws_upgrade_failed, status, headers})
{:gun_error, _gun_pid, _stream_ref, reason} ->
exit({:ws_upgrade_failed, reason})
whatever ->
IO.inspect(whatever, label: "Whatever")
# More clauses here as needed.
after 5000 ->
IO.puts "Took too long!"
:erlang.exit("barf!")
end
# stop(gun_pid)
end
def set_headers(cookie_value) do
[{"cookie", "my_cookie=#{cookie_value}"}]
end
# This just returns the gun_pid for further reference which gets stored in the GenServer state.
def upgrade_success(gun_pid, headers, stream_ref) do
IO.puts("Upgraded #{inspect(gun_pid)}. Success!\nHeaders:\n#{inspect(headers)}\n")
%{stream_ref: stream_ref, gun_pid: gun_pid}
end
# To stop gun
def stop(gun_pid) do
:gun.shutdown(gun_pid)
end
end
To use this:
iex -S mix
iex> WebSocket.Client.go
I have a ws_handler which receives a websocket connection.
This process waits for input starting with <<"h to login.
It then saves the default Websocket state, a Player process ID (which is spawned with a reference to the current PID), and an outbox message, `<<"turn1">>
websocket_handle({text, <<"h", Name/binary>>}, State) ->
{ok, PID} = player:go(self(), <<"profiledata", Name/binary>>),
erlang:start_timer(1000, self(), <<"Hello!">>),
{reply, {text, <<"You joined, ", Name/binary>>}, {State, PID, <<"turn1">>}};
I want to control data flow from this seperate player process, then have my websocket handler retrieve messages and pass them on to its client via the Outbox element.
So I add this in to manually trigger a message:
websocket_handle({text, <<"myscreen">>}, S = {_, P, _}) ->
gen_server:call(P, myscreen),
{ok, S};
and in player.erl,
handle_call(myscreen, _, {WS, Profile, Cab}) ->
gen_server:cast(WS, myscreenupdate),
{reply, ok, {WS, Profile, Cab}};
Back in ws_handler I expect this to get called:
websocket_info(myscreenupdate, State = {St,P, _}) ->
{reply, {text, <<"My screen update">>}, {St, P, <<"turn2">>}};
But the websocket output in my browser continuously prints turn1, instead of turn2.
I tried gen_server:call in player.erl and I get a timeout crash. I think this is because the {reply tuple of websocket_handle in ws_handler is supposed to be replying to the websocket.. but if that were true, then I'd expect the data to be updated:
websocket_info(myscreenupdate, State = {St,P, _}) ->
{reply, {text, <<"My screen update">>}, {St, P, <<"turn2">>}};
So I'm uncertain what's happening here.
How do I update state from a Player process then have my websocket handler retrieve that State and send it out to its connection?
ws_handler.erl:
-module(ws_handler).
-export([init/2]).
-export([websocket_init/1]).
-export([websocket_handle/2]).
-export([websocket_info/2]).
init(Req, Opts) ->
{cowboy_websocket, Req, Opts}.
websocket_init(State) ->
{ok, State}.
websocket_handle({text, <<"h", Name/binary>>}, State) ->
{ok, PID} = player:go(self(), <<"profiledata", Name/binary>>),
erlang:start_timer(1000, self(), <<"Hello!">>),
{reply, {text, <<"You joined, ", Name/binary>>}, {State, PID, <<"turn1">>}};
websocket_handle({text, <<"myscreen">>}, S = {_, P, _}) ->
gen_server:call(P, myscreen),
{ok, S};
websocket_handle({text, <<"auth", Auth/binary>>}, S = {_St, P, _}) ->
case s:s(P, Auth) of
{ok, Valid} -> {reply, {text, << "Authorized">>}, S};
_ -> {reply, {text, <<"Error">>}, S}
end;
websocket_handle({text, Msg}, S = {_St, P, Outbox}) ->
{reply, {text, Outbox}, S};
websocket_handle(_Data, State) ->
{ok, State}.
websocket_info(myscreenupdate, State = {St,P, _}) ->
{reply, {text, <<"My screen update">>}, {St, P, <<"turn2">>}};
websocket_info({timeout, _Ref, _Ignored}, State = {_, P, Outbox}) ->
erlang:start_timer(1000, self(), <<"This is ignored">>),
Msg = Outbox,
{reply, {text, Msg}, State};
websocket_info(_Info, State) ->
{ok, State}.
player.erl:
-module(player).
-compile(export_all).
handle_call(myscreen, _, {WS, Profile, Cab}) ->
gen_server:cast(WS, myscreenupdate),
{reply, ok, {WS, Profile, Cab}};
handle_call(get_profile, _, State = {_WSPID, Profile, _}) ->
{reply, Profile, State}.
init([WSPID, Profile]) ->
{ok, {WSPID, Profile, null}};
init([WSPID, Profile, CabinetPID]) ->
{ok, {WSPID, Profile, CabinetPID}}.
go(WSPID, Profile, CabinetPID) ->
gen_server:start_link(?MODULE, [WSPID, Profile, CabinetPID], []).
go(WSPID, Profile) ->
gen_server:start_link(?MODULE, [WSPID, Profile], []).
The problem is that cowboy's websocket_info/2 handler will only receive messages sent to the websocket process by using the erlang built-in message operator ! (or, equivalently, the erlang:send/{2,3} functions).
So you should do:
WS ! myscreenupdate
instead of
gen_server:cast(WS, myscreenupdate)
When you use gen_server:cast the message is probably discarded by the cowboy message loop since it's not a recognized message. And when you use gen_server:call you get a deadlock.
I have a crude Erlang-to-Golang port example, passing data from Erlang to Golang and echoing the response.
Problem is the amount of data I can transfer seems to be limited to 2^8 bytes (see below). I thought the problem was probably on the Golang side (not creating a big enough buffer) but replacing bufio.NewReader with bufio.NewReaderSize didn't work. So am now thinking the problem is maybe on the Erlang side.
What do I need to do to increase the buffer size / be able to echo a message larger than 2^8 bytes ?
TIA
justin#justin-ThinkPad-X240:~/work/erlang_golang_port$ erl -pa ebin
Erlang/OTP 17 [erts-6.4.1] [source] [64-bit] [smp:4:4] [async-threads:10] [kernel-poll:false]
Eshell V6.4.1 (abort with ^G)
1> port:start("./echo").
<0.35.0>
2> port:ping(65000).
65000
3> port:ping(66000).
** exception error: bad argument
in function port:call_port/1 (port.erl, line 20)
4> port:start("./echo").
<0.40.0>
5> port:ping(66000).
65536
Go
package main
import (
"bufio"
"os"
)
const Delimiter = '\n'
func main() {
// reader := bufio:NewReader(os.Stdin)
reader := bufio.NewReaderSize(os.Stdin, 1677216) // 2**24;
bytes, _ := reader.ReadBytes(Delimiter)
os.Stdout.Write(bytes[:len(bytes)-1])
}
Erlang
-module(port).
-export([start/1, stop/0, init/1]).
-export([ping/1]).
-define(DELIMITER, [10]).
start(ExtPrg) ->
spawn(?MODULE, init, [ExtPrg]).
stop() ->
myname ! stop.
ping(N) ->
Msg=[round(65+26*random:uniform()) || _ <- lists:seq(1, N)],
call_port(Msg).
call_port(Msg) ->
myname ! {call, self(), Msg},
receive
{myname, Result} ->
length(Result)
end.
init(ExtPrg) ->
register(myname, self()),
process_flag(trap_exit, true),
Port = open_port({spawn, ExtPrg}, []),
loop(Port).
loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, Msg++?DELIMITER}},
receive
{Port, {data, Data}} ->
Caller ! {myname, Data}
end,
loop(Port);
stop ->
Port ! {self(), close},
receive
{Port, closed} ->
exit(normal)
end;
{'EXIT', Port, _Reason} ->
exit(port_terminated)
end.
If you use start_link instead, you'll see that the port crashes after the first command:
1> port:start('go run port.go').
<0.118.0>
2> port:ping(65000).
65000
** exception error: port_terminated
If you change the Go code to run in a loop, this crash can be avoided:
func main() {
for {
// reader := bufio:NewReader(os.Stdin)
reader := bufio.NewReaderSize(os.Stdin, 1677216) // 2**24;
bytes, _ := reader.ReadBytes(Delimiter)
os.Stdout.Write(bytes[:len(bytes)-1])
}
}
Now we can see another interesting result:
33> c(port).
{ok,port}
40> port:ping(66000).
65536
41> port:ping(66000).
464
42> port:ping(66000).
65536
43> port:ping(66000).
464
Now we can see that no data is actually lost, it's just buffered in the port. Since you have not specified a framing protocol (using {packet, N} or {line, N} you are responsible yourself for collecting the data. It also seems that the internal buffer size of an Erlang port is 64K (although I found no documentation of this and no way to change it).
If you change your receive to get all data before returning, you'll every byte each time:
loop(Port) ->
receive
{call, Caller, Msg} ->
Port ! {self(), {command, Msg++?DELIMITER}},
Caller ! {myname, receive_all(Port, 10)},
loop(Port);
stop ->
Port ! {self(), close},
receive
{Port, closed} ->
exit(normal)
end;
{'EXIT', Port, _Reason} ->
exit(port_terminated)
end.
receive_all(Port, Timeout) -> receive_all(Port, Timeout, []).
receive_all(Port, Timeout, Data) ->
receive
{Port, {data, New}} ->
receive_all(Port, Timeout, [New|Data])
after Timeout ->
lists:flatten(lists:reverse(Data))
end.
Running this, we get:
1> c(port).
{ok,port}
2>
3> port:start('go run port.go').
<0.311.0>
4> port:ping(66000).
66000
5> port:ping(66000).
66000
6> port:ping(66000).
66000
2^8 is 256, not 65536 which is 2^16 (or 2 bytes).
For excluding golang program you can simply replace your echo with GNU cat
Default message max size for port communication is 64k, so when your port receives messages, the first one is leading 64k of the string. You can read port again to gain remaining data but you just drop them in your code.
If you really want to communicate on line-based protocol you should configure your port accordingly:
{line, L}
Messages are delivered on a per line basis. Each line
(delimited by the OS-dependent newline sequence) is delivered in one
single message. The message data format is {Flag, Line}, where Flag is
either eol or noeol and Line is the actual data delivered (without the
newline sequence).
L specifies the maximum line length in bytes. Lines longer than this
will be delivered in more than one message, with the Flag set to noeol
for all but the last message. If end of file is encountered anywhere
else than immediately following a newline sequence, the last line will
also be delivered with the Flag set to noeol. In all other cases,
lines are delivered with Flag set to eol.
The {packet, N} and {line, L} settings are mutually exclusive.
So your code would be
Port = open_port({spawn, ExtPrg}, [{line, ?PACKET_SIZE]),
%%...
{call, Caller, Msg} ->
Port ! {self(), {command, Msg++?DELIMITER}},
D = read_data(Port, []),
Caller ! {myname, D},
loop(Port);
%%...
read_data(Port, Prefix) ->
receive
{Port, {data, {noeol, Data}}} ->
read_data(Port, Prefix ++ Data);
{Port, {data, {eol, Data}}} ->
Prefix ++ Data
end.
I have been struggling with the similar problem.
Here the complete code of pipe module.
It allows sent text data to port and read all replies.
-module(apr_pipe).
-export([open_pipe/2,send/2,close/1]).
-export([loop/1,status/1,init/1]).
-include_lib("kernel/include/logger.hrl").
-define(MAX_LINE_LEN,4096).
open_pipe(Path,Cmd) ->
State = #{path => Path, cmd => Cmd},
Pid = spawn(?MODULE,init,[State]),
Pid.
init(State) ->
#{path := Path,cmd := Cmd} = State,
FullFn = filename:join(Path,Cmd),
Settings = [{line,?MAX_LINE_LEN},use_stdio,stderr_to_stdout,hide,binary,exit_status],
Port = erlang:open_port({spawn_executable,FullFn},Settings),
State2 = State#{port => Port, data => #{}},
loop(State2).
send(Pid,Data) -> Pid!{self(),send,Data}.
close(Pid) -> Pid!{self(),send,close}.
status(Pid) -> Pid!{self(),status}.
get_eol() -> <<"\n">>.
loop(State) ->
receive
{_Pid,send,close} ->
?LOG(notice,"got cmd: Close",[]),
Port = maps:get(port,State),
port_close(Port),
exit(normal);
{Pid,send,Data} ->
?LOG(notice,"Send Data ...",[]),
Port = maps:get(port,State),
port_command(Port,Data),
port_command(Port,get_eol()),
State2 = State#{status => data_sent, client => Pid},
loop(State2);
{Pid,status} ->
Port = maps:get(port,State),
?LOG(notice,"Status: Port: ~p State: ~p",[Port,State]),
Pid!{status,Port,State},
loop(State);
% port messages.
{Port, {data,{noeol,Data}}} ->
?LOG(notice,"Port: ~p Data: ~p",[Port,Data]),
CurData = maps:get(cur_data,State,[]),
State2 = State#{cur_data => [Data | CurData]},
loop(State2);
{Port, {data, {eol,Data}}} ->
?LOG(notice,"Port: ~p Data: ~p",[Port,Data]),
CurData = [Data | maps:get(cur_data,State,[])],
CurData2 = lists:reverse(CurData),
Reply = list_to_binary(CurData2),
Client = maps:get(client,State,undefined),
State2 = State#{cur_data => [], client => undefined},
case Client of
undefined -> ?LOG(error,"can not sent reply. Client: ~p Reply: ~p", [Client,Reply]),
loop(State2);
_ -> Client!{reply,Reply},
loop(State2)
end;
{_Port, closed} ->
?LOG(warning, "Port: ~p closed",[]),
exit(normal);
{'EXIT', Port, Reason} ->
?LOG(notice,"Port: ~p exit. Reason: ~p",[Port,Reason]),
exit(Reason);
_Other -> ?LOG(error,"unexpected message: ~p",[_Other]),
exit({error,{unexpected_message,_Other}})
end.
I'm trying to spot the syntax error in the following piece of code however I just can't really see what went wrong. I'm sure it's basic but I just need a fresh pair of eyes on this code:
sloop(Listen)->
{ok, Socket} = gen_tcp:accept(Listen),
io:format("Someone connected"),
master ! {add,Socket},
spawn(fun() -> sloop(Listen) end),
receive
{tcp, Socket, Bin} ->
case read(Bin) of
{join,Channel} ->
tracker ! {self(),get,Socket},
receive
void ->
master ! {delete, Socket},
A = lists:dropwhile(fun(A) -> A /= Channel end, resgistered()),
case A of
[H|T] -> H ! {add,Socket};
_ -> register(Channel,spawn(fun() -> listenerSocket([]) end))
end,
Channel ! {add, Socket},
tracker ! {insert,Socket, Channel};
{ok, Value} ->
Value ! {delete,Socket},
tracker ! {delete,Socket},
A = lists:dropwhile(fun(A) -> A /= Channel end, resgistered()),
case A of
[H|T] -> H ! {add,Socket};
_ -> register(Channel,spawn(fun() -> listenerSocket([]) end))
end,
Channel ! {add, Socket},
tracker ! {insert,Socket, Channel};
{message, Msg} ->
tracker ! {self(),get,Socket},
receive
{ok, Value} -> Value ! {broadcast, Msg}
end
end;
{tcp_closed, Socket} -> io:format("Someone disconnected")
end.
Syntax error before "." line 50.
Line 50 is the last line in this snippet of code
You're missing an end. The final end you show matches case read(Bin)..., so you need one more for the enclosing receive.