1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
-module(test).
-export([listen/1,
handle_client/1,
maintain_clients/1,
start/1,
stop/0,
controller/1]).
-author("jerith").
-define(TCP_OPTIONS,[list, {packet, 0}, {active, false}, {reuseaddr, true}]).
-record(player, {name=none, socket, mode}).
%% To allow incoming connections, we need to listen on a TCP port.
%% This is also the entry point for our server as a whole, so it
%% starts the client_manager process and gives it a name so the rest
%% of the code can get to it easily.
listen(Port) ->
{ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
register(client_manager, spawn(?MODULE, maintain_clients, [[]])),
do_accept(LSocket).
%% Accepting a connection gives us a connection socket with the
%% newly-connected client on the other end. Since we want to accept
%% more than one client, we spawn a new process for each and then wait
%% for another connection on our listening socket.
do_accept(LSocket) ->
case gen_tcp:accept(LSocket) of
{ok, Socket} ->
spawn(?MODULE, handle_client, [Socket]),
client_manager ! {connect, Socket};
{error, Reason} ->
io:format("Socket accept error: ~s~n", [Reason])
end,
do_accept(LSocket).
%% All the client-socket process needs to do is wait for data and
%% forward it to the client_manager process which decides what to do
%% with it. If the client disconnects, we let client_manager know and
%% then quietly go away.
handle_client(Socket) ->
case gen_tcp:recv(Socket, 0) of
{ok, Data} ->
client_manager ! {data, Socket, Data},
handle_client(Socket);
{error, closed} ->
client_manager ! {disconnect, Socket}
end.
%% This is the main loop of the client_manager process. It maintains
%% the list of "players" and calls the handler for client input.
maintain_clients(Players) ->
io:format("Players:~n", []),
lists:foreach(fun(P) -> io:format(">>> ~w~n", [P]) end, Players),
receive
{connect, Socket} ->
Player = #player{socket=Socket, mode=connect},
send_prompt(Player),
io:format("client connected: ~w~n", [Player]),
NewPlayers = [Player | Players];
{disconnect, Socket} ->
Player = find_player(Socket, Players),
io:format("client disconnected: ~w~n", [Player]),
NewPlayers = lists:delete(Player, Players);
{data, Socket, Data} ->
Player = find_player(Socket, Players),
NewPlayers = parse_data(Player, Players, Data),
NewPlayer = find_player(Socket, NewPlayers),
send_prompt(NewPlayer)
end,
maintain_clients(NewPlayers).
%% find_player is a utility function to get a player record associated
%% with a particular socket out of the player list.
find_player(Socket, Players) ->
{value, Player} = lists:keysearch(Socket, #player.socket, Players),
Player.
%% delete_player returns the player list without the given player. It
%% deletes the player from the list based on the socket rather than
%% the whole record because the list might hold a different version.
delete_player(Player, Players) ->
lists:keydelete(Player#player.socket, #player.socket, Players).
%% Sends an appropriate prompt to the player. Currently the only
%% prompt we send is the initial "Name: " when the player connects.
send_prompt(Player) ->
case Player#player.mode of
connect ->
gen_tcp:send(Player#player.socket, "Name: ");
active ->
ok
end.
%% Sends the given data to all players in active mode.
send_to_active(Prefix, Players, Data) ->
ActivePlayers = lists:filter(fun(P) -> P#player.mode == active end,
Players),
lists:foreach(fun(P) -> gen_tcp:send(P#player.socket, Prefix ++ Data) end,
ActivePlayers),
ok.
%% We don't really do much parsing, but that will probably change as
%% more features are added. Currently this handles naming the player
%% when he first connects and treats everything else as a message to
%% send.
parse_data(Player, Players, Data) ->
case Player#player.mode of
active ->
send_to_active(Player#player.name ++ ": ",
delete_player(Player, Players), Data),
Players;
connect ->
UPlayer = Player#player{name=bogostrip(Data), mode=active},
[UPlayer | delete_player(Player, Players)]
end.
%% Utility methods to clean up the name before we apply it. Called
%% bogostrip rather than strip because it returns the first continuous
%% block of non-matching characters rather stripping matching
%% characters off the front and back.
bogostrip(String) ->
bogostrip(String, "\r\n\t ").
bogostrip(String, Chars) ->
LStripped = string:substr(String, string:span(String, Chars)+1),
string:substr(LStripped, 1, string:cspan(LStripped, Chars)).
%% Here we have some extra code to test other bits of pygments' Erlang
%% lexer.
get_timestamp() ->
{{Year,Month,Day},{Hour,Min,Sec}} = erlang:universaltime(),
lists:flatten(io_lib:format(
"~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
[Year, Month, Day, Hour, Min, Sec])).
a_binary() ->
<< 100:16/integer, 16#7f >>.
a_list_comprehension() ->
[X*2 || X <- [1,2,3]].
map(Fun, [H|T]) ->
[Fun(H) | map(Fun, T)];
map(Fun, []) ->
[].
%% pmap, just because it's cool.
pmap(F, L) ->
Parent = self(),
[receive {Pid, Result} ->
Result
end || Pid <- [spawn(fun() ->
Parent ! {self(), F(X)}
end) || X <- L]].
|