ErlMUD Commentary

A Deep Breath Before the Plunge

This section tells the story of how the v0.1 "raw" Erlang

2014-11-04: Author's Note

For the benefit of the two other people who might accidentally see this...

Baby steps, or a few leaps?

I'm torn between the idea of incrementally explaining all the code as I write it the first time and the reality that we're going to be seeing essentially the same code quite a few times as ErlMUD moves through its various phases. If I explain the initial write as I go we would see micro-evolutions and have a chance to discuss the micro nature of style and structure decisions in programs. But that is not really what this book is about. Instead I'm going to show the very barest version of a project template, explain that, then blow through a lot of the scaffolding and then fill the skeleton back in code and then explain through it as I go.

What follows is currently half-baked and not the focus of my attention and will be replaced with a per-element walkthrough of the elements of the system skeleton.

Begin at the Beginning

Taking the Mad Hatter's advice, we will begin at the beginning. All I'm interested in is a bare-bones template that spawns a registered erlang process, executes an init function, and responds to messages in a loop. I'm probably just being emotionally shallow, but it makes me feel good to start with something that already talks back, even if the chatter is pointless.

erlmud-0.1/erlmud.erl
 1  -module(erlmud).
 2  -export([start/0]).
 3
 4  start() ->
 5      register(erlmud, spawn(fun() -> init() end)).
 6
 7  init() ->
 8      io:format("~p erlmud: Starting up.", [self()]),
 9      loop().
10
11  loop() ->
12    receive
13      shutdown ->
14          io:format("~p erlmud: Shutting down.~n", [self()]),
15          exit(shutdown);
16      Any ->
17          io:format("~p erlmud: Received~n~n~tp~n~n", [self(), Any]),
18          loop()
19    end.

And playing with that in the shell...

1> c(erlmud).
{ok,erlmud}
2> erlmud:start().
<0.40.0> erlmud: Starting up.
true
3> erlmud ! "something".
<0.40.0> erlmud: Received

"something"

"something"
4> erlmud ! {message, "Some message."}.
<0.40.0> erlmud: Received

{message,"Some message."}

{message,"Some message."}
5> erlmud ! shutdown.
<0.40.0> erlmud: Shutting down.
shutdown

From here I'll put stubs in for the system components and game elements identified in the last section, but just as stubs and nothing more. What I'm really looking for here is to force myself to think through system dependencies and identify if anything is obviously out of order, or if order even matters. (protip: If we can design the pieces so that startup order doesn't matter at all our life will be much easier. This is almost always impossible to achieve 100% if the supervisors are a tree, though, so there will always be some compromise.)

I haven't done a telnet server in ages, so I don't remember exactly how it goes but I definitely want to make sure I've got a relatively clean interface to the system across the network before I get ahead of myself. To give this fuzzy idea a shave I'm going to write a very basic TCP server that is something less than "real" telnet but will behave well enough it can be tested from a real telnet client. I'm not going worry with the details of the actual telnet standard, the point here is to make something useful to me now to prove that I'm not pointing my evil genius at the wrong target. The minimum telnet standard is easy enough to implement later, anyway (its basically checking for a few required control bytes, and saying "no" to extra options -- I actually don't remember just now, but implementing ASCII-only telnet with no extra features is something like this).

telnet.erl
 1  -module(telnet).
 2  -export([start/0, start/1]).
 3
 4  start() -> start(23).
 5
 6  start(Port) ->
 7      register(telnet, spawn(fun() -> init(Port) end)).
 8
 9  init(Port) ->
10      {ok, Listen} = gen_tcp:listen(Port, [binary, {active, true}]),
11      {ok, Socket} = gen_tcp:accept(Listen),
12      gen_tcp:close(Listen),
13      io:format("~p telnet: Starting up on port ~p.~n", [self(), Port]),
14      loop(Socket).
15
16  loop(Socket) ->
17    receive
18      {tcp, Socket, Bin} ->
19          io:format("~p telnet: Received ~tp~n", [self(), Bin]),
20          Str = binary_to_list(Bin),
21          io:format("~p telnet: Unpacked ~tp~n", [self(), Str]),
22          Reply = "You: " ++ Str,
23          gen_tcp:send(Socket, Reply),
24          loop(Socket);
25      {send, Message} ->
26          M = "#system: " ++ Message ++ "\r\n",
27          gen_tcp:send(Socket, M),
28          loop(Socket);
29      {tcp_closed, Socket} ->
30          io:format("~p telnet: Socket closed. Retiring.~n", [self()]),
31          exit(tcp_closed);
32      shutdown ->
33          io:format("~p telnet: Shutting down hard.~n", [self()]),
34          exit(shutdown);
35      Any ->
36          io:format("~p telnet: Received ~tp~n", [self(), Any]),
37          loop(Socket)
38    end.

And... hey, it works!

In the Erlang shell:

1> c(telnet).
{ok,telnet}
2> telnet:start(2222).
true
<0.40.0> telnet: Starting up on port 2222.
<0.40.0> telnet: Received <<"foo.\r\n">>
<0.40.0> telnet: Unpacked "foo.\r\n"
3> telnet ! {send, "bar."}.
{send,"bar."}
<0.40.0> telnet: Received <<"Yay! It works!\r\n">>
<0.40.0> telnet: Unpacked "Yay! It works!\r\n"
4> telnet ! {send, "Sure, but its not *real* telnet... meh."}.
{send,"Sure, but its not *real* telnet... meh."}
<0.40.0> telnet: Socket closed. Retiring.

In the telnet client:

ceverett@changa:~$ telnet localhost 2222
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
foo.
You: foo.
#system: bar.
Yay! It works!
You: Yay! It works!
#system: Sure, but its not *real* telnet... meh.
^]
telnet> close
Connection closed.

Just for a moment it is nice to reflect on how ridiculously easy socket programming in Erlang is. The total time spent on this was around 30 minutes, most of it making sure that "\r\n" was the telnet line delimiter, writing this prose, and formatting the source and terminal output for inclusion in this text. The code itself is insanely simple.

From here I'll parallelize the network code so we can accept several connections at once, start the telnet service as part of erlmud:start(), and move on to other bits. I know there will be some things to adjust later on — making connections spawn controllers, writing a skeleton controller-based chat system, and so on — but this little network module will work well enough to make sure users can talk in and the system can talk out for now.

But how should it be integrated? I'd rather "start up networking" than "start up telnet". Within the concept of starting up the network all that may happen is that telnet gets started, of course, but I don't want to cloud the highest abstraction in the whole system with a really narrow detail like what protocol we are using. To look at it another way, consider all the annoying embedded HTTP server software that has been built around the assumption that HTTP was good enough and had HTTP handling code and the verbiage of HTTP built directly in. Back when HTTP was literally the only thing it could do the code was tight and easy to understand. But now everything requires conformance with HTTPS, TLS, an emergency serial interface, and almost any nontrivial system necessitates shell access over SSH. Changing the original code around is a pain, so this is often not done. Instead either an HTTP service starts up, then a start_networking() type function is called which does all the other stuff, or every single service definition lives within or very close to main(). That's just plain confusing to read for anyone who wasn't on the original development team. Gross.

It is a relatively minor point, but I'm going to go ahead now and create a network manager process, and it can do supervision of whatever networking services we happen to use. Right now that just means our nonstandard telnet routine, but later it might mean more. Whether we do more than telnet later on or not, the code will easy to understand and easy to extend.

In addition to that I'm going to put stub modules in place for the elements we will want later as part of erlmud:init/0, and arrange them in at least sort of the order we think we'll want them to start up. I don't really care what works or doesn't yet. I just want a basic top-level structure in place I can hack on and that talks back because I know I'll be stumbling through quite a few issues the first time I implement the system, particularly since we're not using any of that fancy OTP stuff to help us out by managing the abstractions early.