Hi folks. This might not necessarily be directly SMAUG-related, as it's rather some thought on how MUDs in general handle network output.
To start, a few months ago I was investigating the way SMAUG - at least, the heavily modified SMAUG 1.0 codebase I work on - handles network traffic. By traffic, I mean everything regarding sending and receiving information (i.e. text) between the MUD (server) and players (clients).
I found to my surprise that it seemed fairly inefficient, and certainly against the conception I had of the way the select function works and how it is to be used.
So, I did a little investigation into the main game loop (in the game_loop function.) In pseudo-code, this is what I found:
Now, the output handling is where things really didn't seem right. For starters, the game would loop over and over and over and over until ALL pending output (unless it's over 4K in size) was sent to the client. So if there is network lag or some other problem, and we can't send a lot at a time, do we just sit around waiting forever? It seems that that's what it was doing. Furthermore, when you write once, you're supposed to select again, in order to make sure that the socket is still ready for output. And if you output again straight away, you run the risk of breaking something.
The other thing that bothered me is the time wasted while sleeping until it's time for the next clock tick. That just sounded completely wrong.
So that was the "preliminary study". Then I went to action.
First off, I converted everything to C++. Not a really hard task - I just had to rename a few variables here and there (like the mobs' "class" which obviously conflicts with the keyword). Then came the more interesting part.
I wrote from scratch some generic C++ network modules that handle text-based input and output, as well as a manager class that is in charge of (you guessed it :P) managing these sockets. The idea was to remove, as much as possible, the network code from the rest of the code. I'd always found that the network stuff was too intertwined with the rest of the game, and that the two really shouldn't have a lot to do with each other.
So after I had that code, I threw desc_data out the window, and created the class cPlayerConnection, which inherits from the generic cSocketConnection, which is the text handler. The player connection version is what has everything a MUD needs to do that isn't necessarily generic - things such as connection states, input receiver, that sort of stuff. (That's another thing I did. Input is sent to "receivers", instead of handled in an outside game loop.)
(continued)
To start, a few months ago I was investigating the way SMAUG - at least, the heavily modified SMAUG 1.0 codebase I work on - handles network traffic. By traffic, I mean everything regarding sending and receiving information (i.e. text) between the MUD (server) and players (clients).
I found to my surprise that it seemed fairly inefficient, and certainly against the conception I had of the way the select function works and how it is to be used.
So, I did a little investigation into the main game loop (in the game_loop function.) In pseudo-code, this is what I found:
1) for every controlling descriptor (why are
there two created: port and port+1...?):
- create the descriptor sets (loop through
descriptors and add them - regardless of
whether or not a connection has output pending)
- select with a timeout of 0, so that it
returns immediately, modifying the sets
- accept any new connections on this controlling
descriptor
- idem for next controlling descriptor.
2) now that we have our sets made and rezeroed
and made and rezeroed and made (maybe you see
why this is becoming inefficient), we process
input on them. Nothing too bad there.
3) game logic
4) handle output
(NOTE: maybe 3 and 4 are in the other order-
I don't really remember)
5) sleep the amount of time required until next
clock tick. In other words, sit around doing
nothing until then.
Now, the output handling is where things really didn't seem right. For starters, the game would loop over and over and over and over until ALL pending output (unless it's over 4K in size) was sent to the client. So if there is network lag or some other problem, and we can't send a lot at a time, do we just sit around waiting forever? It seems that that's what it was doing. Furthermore, when you write once, you're supposed to select again, in order to make sure that the socket is still ready for output. And if you output again straight away, you run the risk of breaking something.
The other thing that bothered me is the time wasted while sleeping until it's time for the next clock tick. That just sounded completely wrong.
So that was the "preliminary study". Then I went to action.
First off, I converted everything to C++. Not a really hard task - I just had to rename a few variables here and there (like the mobs' "class" which obviously conflicts with the keyword). Then came the more interesting part.
I wrote from scratch some generic C++ network modules that handle text-based input and output, as well as a manager class that is in charge of (you guessed it :P) managing these sockets. The idea was to remove, as much as possible, the network code from the rest of the code. I'd always found that the network stuff was too intertwined with the rest of the game, and that the two really shouldn't have a lot to do with each other.
So after I had that code, I threw desc_data out the window, and created the class cPlayerConnection, which inherits from the generic cSocketConnection, which is the text handler. The player connection version is what has everything a MUD needs to do that isn't necessarily generic - things such as connection states, input receiver, that sort of stuff. (That's another thing I did. Input is sent to "receivers", instead of handled in an outside game loop.)
(continued)