3

I'm currently writing a simple server which will work with clients connecting and then talking with eachother where the server acts as an intermediary.

The setup is:

  • Server starts
  • 2 Clients connects to server
  • Client1/2 sends a message with its unique ID(an atom)
  • Server saves this ID together with Socket PID
  • Client1 sends {send_to_id, client2id, Message}
  • Server messages Client2

But this doesn't work for me, I get a function_clause error.

So basically I want to send a message to a client with tcp_send without having the client being a "server" but only with recv. Is this possible?

Server code:

-export([start/1]).

-define(TCP_OPTIONS, [binary, {packet, 2}, {active, false}, {reuseaddr, true}]).
-define(PORT, 8080). 

-spec start(Port) -> pid() when
      Port::integer().
start(Port) ->
    process_flag(trap_exit, true),
    ClientDict = dict:new(),
    GamesDict = dict:new(),
    HandlerPID = spawn_link(fun() -> handler(ClientDict, GamesDict) end),
    imup_listener:listen(Port, HandlerPID).





%%------------------------------
% Internal Functions
%%------------------------------

handler(Clients, Games) ->
    receive
    {insert_client, Socket, Alias} ->
        TmpClients = dict:append(Socket, Alias, Clients),
        TmpClients2 = dict:append(Alias, Socket, TmpClients),
        handler(TmpClients2, Games);
    {get_client_id, ReceiverPID, ClientPID} ->
        {ok , CID} = dict:find(ClientPID, Clients),
        ReceiverPID ! {id, CID},
        handler(Clients, Games);
    {get_client_pid, ReceiverPID, ClientID} ->
        {ok, CPID} = dict:find(ClientID, Clients),
        ReceiverPID ! {pid, CPID},
        handler(Clients, Games);
    {host_game, HostID, GameID} ->
        TmpGames = dict:append_list(GameID, [HostID], Games),
        handler(Clients, TmpGames);
    {add_player, PlayerID, GameID} ->
        TmpGames = dict:append_list(GameID, [PlayerID], Games),
        handler(Clients, TmpGames);
    {get_host, ReceiverPID, GameID} ->
        {ok, [HID|T]} = dict:find(GameID, Games),
        {ok, HPID} = dict:find(HID, Clients),
        ReceiverPID ! {host_is, HID, HPID},
        handler(Clients, Games);

    _ ->
        {error, "I don't know what you want from me :("}
    end.

Listener code:

-export([listen/2]).

-define(TCP_OPTIONS, [binary, {packet, 2}, {active, false}, {reuseaddr, true}]).

listen(Port, HandlerPID) ->
    {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
    spawn_link(fun() -> accept(LSocket, HandlerPID) end),
    LSocket.




% Wait for incoming connections and spawn a process that will process incoming packets.
accept(LSocket, HandlerPID) ->
    {ok, Socket} = gen_tcp:accept(LSocket),
    Pid = spawn(fun() ->
            io:format("Connection accepted ~n", []),
            %%DictPID ! {insert, Socket, Socket},
            loop(Socket, HandlerPID)
        end),
    gen_tcp:controlling_process(Socket, Pid),
    accept(LSocket, HandlerPID).



% Echo back whatever data we receive on Socket
loop(Sock, HandlerPID) ->
    inet:setopts(Sock, [{active, once}]),
    receive
    {tcp, Socket, Data} ->
        io:format("Got packet: ~p == ", [Data]),

        FormatedData = process_data(Socket, Data, HandlerPID),
        io:format("~p~n", [FormatedData]),
        convey_message(Socket, FormatedData),

        loop(Socket, HandlerPID);
    {tcp_closed, Socket} ->
        io:format("Socket ~p closed~n", [Socket]);
    {tcp_error, Socket, Reason} ->
        io:format("Error on socket ~p reason: ~p~n", [Socket, Reason])
    end.




%%------------------------------
% Internal Functions
%%------------------------------


-spec process_data(S, D, P) -> term() when
      S::port(),
      D::binary(),
      P::pid().
process_data(Socket, Data, HandlerPID) when is_binary(Data) ->
    case binary_to_term(Data) of
    {send_host, GameID, Msg} ->
        HandlerPID ! {get_host, self(), GameID},
        receive
        {host_is, _HID, HSOCK} ->
            HSOCK;
        _ ->
            {error, nohost}
        end,
        Msg;
    {send_all, GameID, Msg} ->
        Msg;
    {send_to_id, ReceiverID, Msg} ->
        HandlerPID ! {get_client_pid, self(), ReceiverID},
        receive
           {pid, SockPID} ->
            gen_tcp:send(SockPID, term_to_binary(Msg));
        _ ->
            {error, noid}
        end,
        term_to_binary({ok, delivered});
    {host_game, GameID} ->
        GameID;
    {join_game, GameID} ->
        GameID;
    {start_game, GameID} ->
        GameID;
    {enter, SenderID} ->
        HandlerPID ! {insert_client, Socket, SenderID};
    Dat ->
        Dat
    end;
process_data(Socket, Data, DictPID) ->
    Data.


convey_message(Socket, Data) when is_binary(Data) ->
    gen_tcp:send(Socket, Data);
convey_message(Socket, Data) ->
    gen_tcp:send(Socket, term_to_binary(Data)).

Client code:

-export([connect/1, connect/2, disconnect/1, send/2, recv/1]).

connect(PortNo) ->
    {ok, Socket} = gen_tcp:connect("localhost", PortNo, [{active, false}, {packet, 2}]),
    spawn(fun() -> recv(Socket) end),
    Socket.

connect(IP, PortNo) ->
    {ok, Socket} = gen_tcp:connect(IP, PortNo, [{active, false}, {packet, 2}]),
    spawn(fun() -> recv(Socket) end),
    Socket.


send(Socket, Message) ->
    BinMsg = term_to_binary(Message),
    gen_tcp:send(Socket, BinMsg).
%%    {ok, A} = gen_tcp:recv(Socket, 0),
    %%A.

recv(Socket) ->
    {ok, A} = gen_tcp:recv(Socket, 0),
    io:format("Received: ~p~n", [A]),
    recv(Socket).


disconnect(Socket) ->
    gen_tcp:close(Socket).

Do you recommend me to rewrite everything or is my idea somehow possible? Thanks beforehand!

EDIT: Added a testrun.

Eshell V5.8.5  (abort with ^G)
1> imup_server:start(1234).
#Port<0.669>
2> Socket1 = imup_client:connect(1234).
Connection accepted 
#Port<0.681>
3> Socket2 = imup_client:connect(1234).
Connection accepted 
#Port<0.683>
4> imup_client:send(Socket1, {enter, cOne}).
ok
Got packet: <<131,104,2,100,0,5,101,110,116,101,114,100,0,4,99,79,110,101>> == {insert_client,#Port<0.682>,cOne}
Received: [131,104,3,100,0,13,105,110,115,101,114,116,95,99,108,105,101,110,
           116,102,100,0,13,110,111,110,111,100,101,64,110,111,104,111,115,
           116,0,0,2,170,0,100,0,4,99,79,110,101]
5> imup_client:send(Socket2, {enter, cTwo}).
ok
Got packet: <<131,104,2,100,0,5,101,110,116,101,114,100,0,4,99,84,119,111>> == {insert_client,#Port<0.684>,cTwo}
Received: [131,104,3,100,0,13,105,110,115,101,114,116,95,99,108,105,101,110,
           116,102,100,0,13,110,111,110,111,100,101,64,110,111,104,111,115,
           116,0,0,2,172,0,100,0,4,99,84,119,111]
6> imup_client:send(Socket1, {send_to_id, cTwo, hello}).
ok
Got packet: <<131,104,3,100,0,10,115,101,110,100,95,116,111,95,105,100,100,0,4,
              99,84,119,111,100,0,5,104,101,108,108,111>> == 7> 
=ERROR REPORT==== 5-May-2013::23:25:49 ===
Error in process <0.39.0> with exit value: {function_clause,[{gen_tcp,send,[[#Port<0.684>],<<9 bytes>>]},{imup_listener,process_data,3},{imup_listener,loop,2}]}


=ERROR REPORT==== 5-May-2013::23:25:49 ===
Error in process <0.40.0> with exit value: {{badmatch,{error,closed}},[{imup_client,recv,1}]}
Klesken
  • 35
  • 1
  • 6

1 Answers1

4

So you're getting a function_clause error when calling gen_tcp:send with the arguments [#Port<0.684>] and <<9 bytes>>. The first argument is a list containing a "port" (a socket in this case), but it should be just the port.

If I'm reading the code correctly, this happens because you're putting sockets into the dictionary with dict:append, which causes the values of the dictionary to be lists. Unless you actually need to store several sockets for each client or vice versa, maybe dict:store would be more suitable.

legoscia
  • 39,593
  • 22
  • 116
  • 167
  • Thanks, I must've been blind since I even tested the dict functions, now I can receive messages. Another problem came up though, when receiving on server it is binary, but when the client receivs it becomes a list instead. I have no idea why this happens since I use the same term_to_binary/binary_to_term on both sends. Any idea? – Klesken May 06 '13 at 13:10
  • 3
    Your client should open the Socket as binary if you want to receive messages as binary data. Add the atom binary to the list of options when you open the tcp Socket in the client: gen_tcp:connect(IP, PortNo, [binary, {active, false}, {packet, 2}]) – David Wickstrom May 06 '13 at 13:41