Middle men wanted

Joe Armstrong joe@REDACTED
Wed May 5 14:46:24 CEST 2004



I've now made gen_middle_man (appended)

The plugins are easy (see middle_man_eterm - appended)
and middle_man_http.

A middle man must export

	start(Args, Bin) -> {[Msg], Cont}
	
		Args is ignored (for now)
		Bin = initial data sent to the port
		Msg = message to sent to the client
		Cont = continuation to be called in received/2

	received(Bin, Cont) -> {[Msg], Cont'}

		return values as above

	term_to_bin(Term) -> IOList

		serialise Term. IOList is sent to the socket

With this structure handing sockets in a generic way is easy.

Open a socket and call gen_middle_man(.....,Mod) to handle the input

With this abstraction *all* servers connected to TCP ports see streams of
Erlang terms.

Now we can do some fun things. Here is an example:

Here is some server code:

handler0(Client) ->
    receive
	{Client, {fac, N}} when integer(N) ->
	    J = fac(N),
	    reply(Client, J),
	    handler0(Client);
        ...

Now here are three client dialogues:

   Try number one:

	telnet localhost 1288
	Trying 127.0.0.1...
	Connected to localhost.localdomain.
	Escape character is '^]'.
	erl.                        <--- this says we're to use the 
				         eterm middle man
	{fac,25}.                   <--- the query
	15511210043330985984000000. <--- response

   Number two:

      telnet localhost 1288        <---- same port
      Trying 127.0.0.1...
      Connected to localhost.localdomain.
      Escape character is '^]'.
      <?xml version="1.0"?>          <---- this says use XML
      <tuple>                               <--- the query starts here
       <element><atom>fac</atom></element>
       <element><int>10</int></element>
      </tuple>
      
       <tuple>			    <--- the response starts here
        <element>
         <atom>reply</atom>
        </element>
        <element>
         <int>3628800</int>
        </element>
       </tuple>

   Number three (uses estream and an erlang client)

	$ erl
      Erlang (BEAM) emulator version 5.3 [source] [hipe]
      
      1> {ok, S} = session:start("localhost", 1288).
      {ok,<0.33.0>}
      2> session:rpc(S, {fac,10}).
      3628800

  So the *same* server is polylingual - ie the port understands erlang
terms, XML  expressions or Erlang binaries  - and it's  really easy to
write.

  I'll be adding soap and xmlrpc real soon :-)

  Cheers

	/Joe


-module(middle_man_http).

%% Copyright (C) 2004 by Joe Armstrong (joe@REDACTED)
%% All rights reserved.
%% The copyright holder hereby grants the rights of usage, distribution
%% and modification of this software to everyone and for any purpose, as
%% long as this license and the copyright notice above are preserved and
%% not modified. There is no warranty for this software.
 

-export([start/2, received/2, term_to_bin/1]).

-import(lists, [map/2, reverse/1]).

start(Vsn, Bin) ->
    received(Bin, {header, []}).

received(Bin, Cont) ->
    received(binary_to_list(Bin), Cont, []).

received(Data, {header, Buff}, L) ->
    case scan_header(Data, Buff) of
	{no, Buff1} ->
	    {reverse(L), {header, Buff1}};
	{yes, Header, After} ->
	    got_header(Header, After, L)
    end;
received(Data, {post, Buff, Len, X}, L) ->
    case collect_chunk(Len, Data, Buff) of
        {yes,PostData,After} ->
            Args2 = parse_uri_args(PostData),
	    {Op,Vsn,URI,Args1,Env} = X,
	    Request = {Op,Vsn,URI,Args1++Args2,Env},
            received(After, {header,[]}, [X|L]);
        {no,Buff1, Len1} ->
            State = {post, Buff1, Len1, X},
	    {reverse(L), State}
    end.

got_header(Header, After, L) ->
    %% We've got the header - parse it
    case parse_header(Header) of
	Result = {Op, ContentLen, Vsn, URI, Args, Env} ->
	    case ContentLen of
		0 ->
		    received(After, {header, []}, [{Op,Vsn,URI,Args,Env}|L]);
		_ ->
		    State = {post, [], ContentLen, {Op,Vsn,URI,Args,Env}},
		    received(After, State, L)
		end;
	Other ->
	    io:format("Oops ~p ~n", [Other]),
	    exit(debug)
    end.

collect_chunk(0,New,Buf)      -> {yes, reverse(Buf), New};
collect_chunk(N, [H|T], Buff) -> collect_chunk(N-1,T,[H|Buff]);
collect_chunk(N, [], Buff)    -> {no, Buff, N}.

scan_header([$\n|T], [$\r,$\n,$\r|L]) -> {yes, reverse(L), T};
scan_header([H|T],  L)                -> scan_header(T, [H|L]);
scan_header([], L)                    -> {no, L}.
				
%%----------------------------------------------------------------------

parse_header(Str) ->
    {ok, Fields} = regexp:split(Str, "\r\n"),
    PRequest = parse_request(hd(Fields)),
    %% Args = "KeyWord: Str" ..
    PArgs = map(fun isolate_arg/1, tl(Fields)),
    make_return_value({PRequest, PArgs}).

make_return_value({{Op,Vsn,{URI,Args}}, Env}) ->
    {Op, content_length(Env), Vsn, URI, Args, Env}.


content_length([{"content-length",Str}|T]) ->
    list_to_integer(Str);
content_length([_|T]) -> 
    content_length(T);
content_length([]) -> 
    0.

urlencoded2str([$%,Hi,Lo|T]) -> [decode_hex(Hi, Lo)|urlencoded2str(T)];
urlencoded2str([$+|T])       -> [$ |urlencoded2str(T)];
urlencoded2str([H|T])        -> [H|urlencoded2str(T)];
urlencoded2str([])           -> [].


isolate_arg(Str) -> isolate_arg(Str, []).

isolate_arg([$:,$ |T], L) -> {httpd_util:to_lower(reverse(L)), T};
isolate_arg([H|T], L)     -> isolate_arg(T, [H|L]).

%% decode_hex %%

decode_hex(Hex1, Hex2) ->
    hex2dec(Hex1)*16 + hex2dec(Hex2).

hex2dec(X) when X >=$0, X =<$9 -> X-$0;
hex2dec($A) -> 10;
hex2dec($B) -> 11;
hex2dec($C) -> 12;
hex2dec($D) -> 13;
hex2dec($E) -> 14;
hex2dec($F) -> 15;
hex2dec($a) -> 10;
hex2dec($b) -> 11;
hex2dec($c) -> 12;
hex2dec($d) -> 13;
hex2dec($e) -> 14;
hex2dec($f) -> 15.

parse_request(Str) ->
    {ok, Args} = regexp:split(Str, " "),
    case Args of
	["POST", URI, Vsn] ->
	    {post, parse_vsn(Vsn) ,parse_uri(URI)};
	["GET", URI, Vsn] ->
	    {get, parse_vsn(Vsn), parse_uri(URI)};
	_  -> 
	    oops
    end.

parse_vsn("HTTP/1.0") -> {1,0};
parse_vsn(X) -> X.

%% A typical URI looks
%% like
%% URI = "/a/b/c?password=aaa&invisible=A+hidden+value"

parse_uri(URI) ->
    case string:tokens(URI, "?") of
	[Root] ->
	    {Root, []};
	[Root, Args] ->
	    {Root, parse_uri_args(Args)}
    end.

parse_uri_args(Args) ->
    Args1 = string:tokens(Args, "&;"),
    map(fun(KeyVal) ->
	       case string:tokens(KeyVal, "=") of
		   [Key, Val] ->
		       {urlencoded2str(Key), urlencoded2str(Val)};
		   [Key] ->
		       {urlencoded2str(Key), ""};
		   _ ->
		       io:format("Invalid str:~p~n",[KeyVal]),
		       {"error", "error"}
	       end
       end, Args1).

term_to_bin({Headers, Data}) ->
    B1 = list_to_binary(Data),
    Len = size(B1),
    Headers1 = [Headers,"Content-Length ",integer_to_list(Len),"\r\n\r\n"],
    [Headers1, B1].


-module(middle_man_eterm).

%% Copyright (C) 2004 by Joe Armstrong (joe@REDACTED)
%% All rights reserved.
%% The copyright holder hereby grants the rights of usage, distribution
%% and modification of this software to everyone and for any purpose, as
%% long as this license and the copyright notice above are preserved and
%% not modified. There is no warranty for this software.

-export([start/2, received/2, term_to_bin/1]).

start(_, <<"erl.\r\n",B/binary>>) -> received(B, []);
start(_, <<"erl.\r",B/binary>>)   -> received(B, []);
start(_, <<"erl.\n",B/binary>>)   -> received(B, []);
start(_, _)                       -> exit(protocol).

received(Bin, Cont) -> 
    received(binary_to_list(Bin), Cont, []).

received(Str, Cont, L) ->
    case erl_scan:tokens(Cont, Str, 1) of
	{more, C1} ->
	    {lists:reverse(L), C1};
	{done, Result, Rest} ->
	    case Result of 
		{ok, Toks, _} ->
		    case  erl_parse:parse_term(Toks) of
			{ok, Term} ->
			    io:format("sending:~p~n",[Term]),
			    received(Rest, [], [Term|L]);
			_ ->
			    exit(bad_term)
		    end;
		E ->
		    exit(bad_term)
	    end	
    end.

term_to_bin(Term) ->
    io_lib:format("~p.\n",[Term]).


---- gen_middle man ---



-module(gen_middle_man).
 
%% Copyright (C) 2004 by Joe Armstrong (joe@REDACTED)
%% All rights reserved.
%% The copyright holder hereby grants the rights of usage, distribution
%% and modification of this software to everyone and for any purpose, as
%% long as this license and the copyright notice above are preserved and
%% not modified. There is no warranty for this software.

-export([start/5]).

start(Args, Client, Socket, Bin, Mod) ->
    erlang:monitor(process, Client),
    {Msgs, Cont} = Mod:start(Args, Bin),
    send_messages(Client, Msgs),
    loop(Client, Socket, Cont, Mod).

send_messages(Client, [])    -> void;
send_messages(Client, [H|T]) -> Client ! {self(), H}, send_messages(Client, T).
				 
%% ---> {tcp,Socket,Bin}
%% ---> {tcp_closed, Socket}
%% ---> close
%% ---> {send, X}
%% ---> {'DOWN',_,_,_,_}       (from client)

loop(Client, Socket, Cont, Mod) ->
    receive
	{tcp, Socket, Bin} ->
	    case Mod:received(Bin, Cont) of
		{Msgs, Cont1} ->
		    send_messages(Client, Msgs),
		    loop(Client, Socket, Cont1, Mod)
	    end;
	{tcp_closed, Socket} ->
	    %% revese the order (sneaky)
	    Client ! {closed, self()};
	close ->
	    gen_tcp:close(Socket);
	{send, Term} ->
	    B  = Mod:term_to_bin(Term),
	    gen_tcp:send(Socket ,B),
	    loop(Client, Socket, Cont, Mod);
	{'DOWN',_,process,Client,_} ->
	    gen_tcp:close(Socket);
	Other ->
	    ed_log:error({middle_man_eterm,funny,msg,Other})
    end.













More information about the erlang-questions mailing list