A little research suggests in Erlang it is safe to have many Erlang processes all calling gen_tcp:accept on a single socket; when a connection comes in one of the processes will unblock from accept, receiving a shiny new socket! This makes life relatively easy: we essentially want a program that starts N processes, each of which runs the exact same logic as the simplest echo server we wrote earlier. In newb-Erlang this winds up looking like this:
%% blocking echo server, take 2 %% based on http://www.erlang.org/doc/man/gen_tcp.html#examples -module(echoConcurrent). -export([server/2]). %% happy path; yay guards! %% server will open a listen socket on Port and boot WorkerCount workers to accept/echo on it server(WorkerCount, Port) when is_integer(WorkerCount), WorkerCount > 0, WorkerCount < 1000, is_integer(Port), Port > 0, Port < 65536 + 1 -> io:format("~p is lord and master; open the listen socket and release the gerbils!~n", [self()]), case gen_tcp:listen(Port, [{active, false}, {packet, 0}]) of {ok, ListenSocket} -> spawnServers(WorkerCount, ListenSocket), ok; {error, Reason} -> {error, Reason} end; %% badargs to server; show a message and die server(WorkerCount, Port) -> io:format("Must provider worker count between 0 and 1000, port between 1 and 65536."), io:format("WorkerCount='~p', Port='~p' is invalid~n", [WorkerCount, Port]), {error, badarg}. %% spawning 0 servers is relatively easy spawnServers(0, _) -> ok; %% to spawn Count servers on ListenSocket you just spawn 1 and recurse for Count-1 spawnServers(Count, ListenSocket) -> spawn(fun() -> acceptEchoLoop(ListenSocket) end), %% to do this we have to export acceptEchoLoop: spawn(?MODULE, acceptEchoLoop, [ListenSocket]), spawnServers(Count-1, ListenSocket). %% The heart of our server: Our core worker function %% Accept's an incoming connection, blocking until one shows up, then read/echoes %% until that connection goes away or errors, then ... does it all again! acceptEchoLoop(ListenSocket) -> io:format("Gerbil ~p is waiting for someone to talk to~n", [self()]), {ok, Socket} = gen_tcp:accept(ListenSocket), %% Show the address of client & the port assigned to our new connection case inet:peername(Socket) of {ok, {Address, Port}} -> io:format("Gerbil ~p will chat with ~p:~p~n", [self(), Address, Port]); {error, Reason} -> io:format("peername failed :( reason=~p~n", [Reason]) end, receiveAndEcho(Socket), ok = gen_tcp:close(Socket), acceptEchoLoop(ListenSocket). %% read/echo raw data from a socket, print it blindly, and echo it back receiveAndEcho(Socket) -> %% block waiting for data... case gen_tcp:recv(Socket, 0, 60 * 1000) of {ok, Packet} -> io:format("Gerbil ~p to recv'd ~p; echoing!!~n", [self(), Packet]), gen_tcp:send(Socket, Packet), receiveAndEcho(Socket); {error, Reason} -> io:format("Sad Gerbil %p: ~p~n", [self(), Reason]) end.
If we start the server on port 8888 and open a couple of telnet localhost 8888 connections each is picked up by a different process in the Erlang server, as can be seen in the console output from our Erlang server:
<0.30.0> is lord and master; open the listen socket and release the gerbils! Gerbil <0.36.0> is waiting for someone to talk to Gerbil <0.37.0> is waiting for someone to talk to Gerbil <0.38.0> is waiting for someone to talk to Gerbil <0.39.0> is waiting for someone to talk to Gerbil <0.40.0> is waiting for someone to talk to Gerbil <0.41.0> is waiting for someone to talk to Gerbil <0.42.0> is waiting for someone to talk to Gerbil <0.43.0> is waiting for someone to talk to Gerbil <0.44.0> is waiting for someone to talk to Gerbil <0.45.0> is waiting for someone to talk to ok 2> Gerbil <0.36.0> will chat with {127,0,0,1}:11067 2> Gerbil <0.36.0> to recv'd "h"; echoing!! 2> Gerbil <0.36.0> to recv'd "e"; echoing!! 2> Gerbil <0.36.0> to recv'd "l"; echoing!! 2> Gerbil <0.36.0> to recv'd "l"; echoing!! 2> Gerbil <0.36.0> to recv'd "o"; echoing!! 2> Gerbil <0.36.0> to recv'd " "; echoing!! 2> Gerbil <0.36.0> to recv'd "w"; echoing!! 2> Gerbil <0.36.0> to recv'd "o"; echoing!! 2> Gerbil <0.36.0> to recv'd "r"; echoing!! 2> Gerbil <0.36.0> to recv'd "l"; echoing!! 2> Gerbil <0.36.0> to recv'd "d"; echoing!! 2> Gerbil <0.37.0> will chat with {127,0,0,1}:11068 2> Gerbil <0.37.0> to recv'd "w"; echoing!! 2> Gerbil <0.37.0> to recv'd "a"; echoing!! 2> Gerbil <0.37.0> to recv'd "s"; echoing!! 2> Gerbil <0.37.0> to recv'd "s"; echoing!! 2> Gerbil <0.37.0> to recv'd "z"; echoing!! 2> Gerbil <0.36.0> to recv'd "a"; echoing!! 2> Gerbil <0.37.0> to recv'd "b"; echoing!!Note that Erlang process <0.30.0> initially ran and booted 10 additional processes, each of which spins in the acceptEchoLoop. When a telnet session connects and sends "hello world" the <0.36.0> process takes the connection. When another telnet session connects the <0.37.0> process picks it up. Thus we can handle many connections concurrently.
Disclaimer: I have used the things I'm claiming to be somewhat complicated "for real" (eg production) but I have not used Erlang in production so the next paragraph is partially speculation.
In "normal" server coding each thread or process started to handle communication with a single client is quite expensive; we thus find ourselves doing complex fiddly multiplexing of many clients onto a single processing thread when possible using I/O completion ports, Java NIO, thread pools, and so on. As Erlang gives us a very cheap process (Erlang process, not OS process) we can afford to spawn a new process for each client and this may lend itself to a simpler and thus less error-prone coding concurrent coding model.
1 comment:
Can i achieve 1million connection in this way
Post a Comment