The random rantings of a concerned programmer.

Erlang MUD IV

January 15th, 2008 | Category: Random

So, after watching Dennou Coil, I thought back to my idea for a card game. Well, it wasn’t so much an idea for a specific card game, but more along the lines of “lol cards are cool”. The idea came back mostly because of the awesomeness of Metatags in Dennou Coil. Really an awesome series.

Anyway, so I was thinking some more, and you know, card games really aren’t that fun unless you play them with other people. Which means I’m gonna need a server backend for the whole thing. Which means… Erlang!

It’s been a long time since I last thought about it, and you know, it really needs some more thought. The shitty gen_tcp wrapper I wrote a while back is really dumb, and easily killable. Additionally, the network layer needs to not only abstract the client stuff, but it needs to do other tasks like prevent flooding, basic input processing (backspaces…), etc.

So, lulz, I’m once again rewriting the network layer. Because that’s what I seem to do in Erlang. Like many of my “projects” I’ll probably get bored with it after a couple of days, then come back to it at a later date. Here’s the interface that I’ve laid out for it:

-module( mud_serv ).
-export( [
        start_async/2,
        serv_listen/2,
        serv_handle_clients/2,
        client_listen/3,
        client_sockrecv/2
] ).

% Primary function, opens a listener port and spawns the listener.
% Non-blocking.
%
% Arguments:
%       Port - port number to open a listener port on.
%       ClientFunc - a fun/1 which gets spawned for each client; is given
%               the PID of the client_listen process for control. Will be passed
%               { sock_recv, Msg }, { sock_connect } and { sock_disconnect }.
%
start_async( Port, ClientFunc )
        ->
        ok
        .

% Listener process for the server socket. Waits until a connection is
% incoming, and then hands the request to a serv_handle_clients/2 process
% which then decides whether or not to accept the connection. Essentially
% a gen_tcp:accept blocker.
%
% Arguments:
%       ServSock - the pre-created valid socket handle to listen with
%
serv_listen( ServSock, PID_Handler )
        ->
        ok
        .

% Dedicated process for handling incoming connections and already connected
% clients. Maintains a list of clients (and their associted IP addresses and
% sockets). Is notified when a client disconnects by client_listen.
%
% Arguments:
%       ClientFunc - passed in by start_async/2
%       ClientList - a list of currently-connected sockets.
%
serv_handle_clients( ClientFunc, ClientList )
        ->
        ok
        .

% Process which is spawned for each client; serves as an interface between the
% socket and the actual application code.
%
% Arguments:
%       ClientSock - the socket handle for the client
%       PID_Client - PID to the application process handling this client, spawned from
%               the ClientFunc passed to serve_async/2.
%       PID_Handler - the PID to the serv_handle_clients/2 process, to notify the
%               handler process when the client disconnects.
%
client_listen( ClientSock, PID_Client, PID_Handler )
        ->
        ok
        .

% Process which is spawned for each client; acts as a gen_tcp:recv loop
% to allow the socket to be processed in an asynchronous manner.
%
% Arguments:
%       ClientSock - the socket handle for the client
%       PID_Listener - PID to the client_listen/3 process to forward messages
%               received from the socket.
%
client_sockrecv( ClientSock, PID_Listener )
        ->
        ok
        .
No comments

LOATHSOME BACKSPACES

December 24th, 2007 | Category: Random

So one of the issues I wanted so solve in my Erlang MUD was backspaces. When a shitty telnet client sends a backspace, the server should be able to recognize that and treat it in a sane manner. This implies two things:

  1. Each backspace should remove the first preceeding non-backspace character from the string.
  2. No backspaces should be remaining in the string after processing.

I’m not sure if it’s possible to do with a regex, but there are some funny strings that actually come up quite often which broke a couple of my previous hacks. So I finally sat down and wrote this (which appears to handle them properly) -

handle_backspaces( 8, { Work, N } )
        ->
        % This character is a backspace
        { Work, N + 1 }
        ;
handle_backspaces( A, { Work, 0 } )
        ->
        % Just append A onto the front of Work
        { [ A | Work ], 0 }
        ;
handle_backspaces( _, { Work, N } )
        ->
        % Delete a character, delete a backspace
        { Work, N - 1 }
        .

Which is invoked with foldr like so -

{ Output, _ } =
        lists:foldr(
                fun( X, Y ) -> mmo_client:handle_backspaces( X, Y ) end,
                { [], 0 },
                Input
        )
.

This, of course, can be wrappered so it can be used without spilling the guts of the implementation everywhere. Hooray <3

2 comments

(Untitled)

December 10th, 2007 | Category: Random

lol, so I put my shitty little Erlang telnet server up and posted the link on /prog/, who managed to crash it completely twice, and DOS it a third time. There’s a lot of weird issues which I need to work out -

  1. Truncate usernames (or disallow excessively long ones).
  2. Only allow a certain number of connections from a given IP address, then start killing the sockets.
  3. Better message sanitation. Don’t let any control characters go through, and handle backspaces correctly.
  4. Put flooding controls in place, so a given user can’t just send an unlimited amount of data.

Among others, of course. Despite my software being really shitty, I had a good time talking with my fellow /prog/rammers and watching them bash the shit out of my game. I wasn’t keeping logs, but I think it managed to handle ~500 concurrent connections before it died, and it was doing fine when someone cat‘d Emacs into it (albeit, echoing that back out to all the clients consumed my entire upstream, but that isn’t a software error).

All in all, I think my shit held out fairly well considering.

EDIT: haha! Someone took my code and made a revised version. Awesome!!

No comments

lighttpd, etc.

December 09th, 2007 | Category: Random

I made delicious ramen today. I finally managed to use the exact right amount of water to essentially steam the noodles so they both come out cooked and without the “soup” portion. I then seasoned them with some cumin, red pepper, Tabasco and salt (and msg) and it was delicious. Yum yum. Overcooked them a little bit, but overall pretty good and spicy.

Anyway, two points of interest for this post -

  1. I got Erlang running off two machines (one master, one node)! I kind of cheated and NFS mounted the master’s filesystem on the node by hand; I need to figure out how I’m going to export things like applications and stuff. I tried just booting the node from the root filesystem but that didn’t go over so well.
  2. lighttpd has built-in load-balancing. This, in addition to lighttpd already sounding fucking sweet, is just fucking awesome. I’m trying to install and configure it now.

I also went ahead and downloaded BProc just to see how many modifications were made to the Linux kernel to get it to work, and I lol’ed like a motherfucker. I doubt I’ll be able to get that shit working on FreeBSD for a long, long time, simply because I’m not at all familiar with the structure of FreeBSD’s kernel.

Ha ha ha oh wow.

No comments

oh god I came

December 04th, 2007 | Category: Random

You know, the thing that has irked me about Erlang for the past week has been the arguments passed to spawn/3, which require you to pass an atom representing the module to use and another atom representing the function from the indicated module. This always seemed a bit retarded to me, especially when Erlang, like any sane language, provides constructs for high-order functions. And it also provides a secret spawn/1 interface which, although offhandedly mentioned in the documentation, I’d never seen before. It takes a function object and launches a new process to run it. Though the calling cost for using functors is double that of a normal call, the syntax really does look a lot nicer because you no longer have to pass around { Module, Function } esoteric nonsense.

In any case, I took another look at my code and decided everything was too coupled, so I’m rethinking the complete structure, pulling things out and remolding them so I can put more shit in later without too much hassle. And reuse them or something.

The first thing I did was to pull out the “open a socket on port X to wait for connections, then spawn a thread for each connection that you get” code, because I’ve rewritten it (copypasta it) three times now:

-module( mmo_servsock ).
-export( [ serve_async/2 ] ).

serve_async( Port, Fun )
        ->
        io:format( "Opening port ~p~n", [ Port ] ),
        % Create a listen socket to listen on
        case gen_tcp:listen( Port, [
                        binary,
                        { packet, 0 },
                        { active, false } ] )
                of

                { ok, SockServ }
                        ->
                        io:format( "Listening for connections~n", [] ),

                        % Spawn a new thread for the listener.
                        spawn( mmo_servsock, listener, [ Fun, SockServ ] )
                        ;

                { error, Reason }
                        ->
                        { error, Reason }
        end
        .

listener( Fun, SockServ )
        ->
        % Accept loop for the server socket.
        case gen_tcp:accept( SockServ ) of
                { ok, SockClient }
                        ->
                        { ok, { Address, _ } } = gen_tcp:peername( SockClient ),
                        io:format( "Client connected (~p)~n", [ Address ] ),

                        % Spawn a new process to handle the callback, then loop
                        spawn( fun() -> Fun( SockClient ) end ),
                        listener( Fun, SockServ )
                        ;
                { error, Reason }
                        ->
                        % I don't know what would cause this, but kill the socket.
                        io:format( "ERROR (mmo_servsock:listener): ~p~n", [ Reason ] )
                        gen_tcp:close( SockServ )
        end
        .

lawds.

No comments

Next Page »