🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

sockets & MUDS

Started by
3 comments, last by khal 23 years, 2 months ago
I don''t know how many of familiar with MUDs (though I doubt there are many of you who aren''t), but I''ve recently taken a challenge to my programming skills. I''ve decided to rewrite all the antique C codebases out there into C++ (and perhaps even using the WinAPI) - in the process, teaching myself a bit about TCP/IP & socket programming. I have two general questions, for which I''m looking for some insight from those wiser than myself. A) A MUD is ultimately a telnet server, specifically design to handle theoretically infinite connections. The "meat" of the program is relatively easy, as it comprises mainly of C/C++ programming structures...(pointers & linked lists up the butt) However, one challenging aspect of the MUD is multiplatform. I hope to design the MUD to operate under the most popular OS''s namely, Windows & *nix. Secondly, although the entire game is textual, the sheer volume of users, and the fact that *everything* is processesed server side, make design issues slightly different from a FPS or multiplayer RPG/RTS. I''d really appreciate any insights on the design of such a system. I''ve been reading everywhere online, and I''m not quite sure certain techniques (such as event messaging) apply to Mud programming. Does a mud (without the benefit of the Win API) have to write their own threading & event managing system? Or does it handle everything in a loop, handling all connections in a sequential manner, repeated so many times a second? B) In studying socket programming, I''ve run across 3 main ways of opening a socket: (please excuse if my terminology is wrong) blocking, non-blocking, and asynchronous. The asynchronous technique applies only to Win API, therefore making it impossible? on *nix systems. As far as blocking & non-blocking, I''ve noticed that existing codebases (ie, ROM), uses the "select" function -- does this imply that non-blocking sockets are preferred? If so, why? Any help regarding this matter would very much be appreciated, thanks! ~khal --- ~khal
---~khal
Advertisement
A)I''ve studied a lot of MUDs out there, and found two methods for creating them. The first is running the whole mud in a loop, that is using no threads. This seems the most common and works well even on slower *nix computers. This is also the most common because it''s easier to port
The second method is to run the network stuff in it''s own thread, but threads, as I stated before, are hard to port. If you were only writing for one system (windows or *nix), I would say use threads.
All connections should be handled sequentially as fast as possible in the loop. A timer should only be used for running the internals of the mud (moving NPCs, updating ingame time).

B)I can''t quite remember if asynchronous is available to *nix, but I used the SOCK_STREAM type in the socket command. This allows full duplex (non-blocking) data transfers. Using sock_stream and the select command works well for a low cpu intensive MUD type game.

Hope this helps

-Mark
EXCELLENT! I''ve found a new friend =)

About non-blocking & blocking methods of...hmm, whats the proper terminology...binding ports?? opening sockets?? In any case, you mentioned that non-blocking is prefered. Since in non-blocking (which you curiously call full-duplex, and I would like to understand more about that) requires you to actively poll the port for..uhhm..information? message? (as you can see, I''m a bit lost) -- the success of the mud depends on managing all 10-300 of the connected users (through their respective descriptors), handling their input, writing their prompts, etc.. and afterwards, taking care of any world refreshes, and what not -- anyway, the success of the mud depends on this being fast enough so that the latency between "user input" and "expected output" is bearable.

...which brings up other questions: i''ll limit to two for now, and hope that you don''t flee from me in terror =)
(a) what *exactly* is a descriptor. from what I gather, it is a data structure that holds all pertinent information that identifies a single users -- how to send data to him (via, ports & ip address), how to recieve data from him, how to access his "character" in the world. is there more to it? is it simply stored in a link list, to be tranversed?

(b) what determines 1 execution of the main loop. Most likely 1 user input (from each user, sequentially) or perhaps a timeout if they do not input. But how should I handle output? Should I try to process the entire contents of the output (for example, the enter room description, when they type "look") or split that up into several executions? Intuitively speaking, I believe that we try to handle the entire command. This leads to something that I''ve found digging through the Circle source. It detects how many executions it is behind, and tries to catch up? letting cycles fall behind cannot be thrown away, of course (as, a user on the very end of the descriptor list may not get his command parsed)... do muds generally extend the time so that ALL commands must be parsed before going to the next cycle? do muds generally try to play the futile game of catchup? or, am I simple overlooking the fact that in a certain command is not caught on this cycle; it would left on his input stream/buffer & taken care of on the next cycle. Theoretically speaking, the ladder of the three would always give a slight advantage to the user on top of the descriptor list (as, his commands will ALWAYS be parsed) whereas the person on the bottom of the descriptor list may take 2 executions to parse 1 command?


Anyway, I think this is quite enough for you to patiently explain to me, now...and I can''t tell you how much I appreciate your time...thanks again,


---
~khal
---~khal
I''ll try to answer your questions as best as I can.
-First off, I call it full duplex because that''s what I read in my linux programming book. It''s just a different term for non-blocking. That is, it allows for sending and receiving data at the same time (hence full duplex).

A) I''m not sure what you mean by a descriptor. If you are talking about the structure on how character data is stored in memory, then it is exactly what you stated. There isn''t too much to it. Basically mine contained the following (going off of memory, I don''t have my code anymore):
-Character Data (stats, equipment, etc)
-IP Address (used to store the character data in a hash table for quick access)
-Input/Output buffer (I''ll explain these later)
-Socket address (returned by the accept command)
There were other things, but this is a good list for general MUDs

B) All I have to say is "Welcome to the world of real-time programming!" Timing is everything in MUDs. There are two ways of handling input/output and timing.
-First method is use loose timing. That is if you can''t handle all the messages in your message queue in the allotted time, finish up all the messages before continuing, even if it adds time to that round.

-Second method is what I used. Try to stay within the allotted time, and if you go over, stop processing messages. Keep the remaining messages on the top of the queue and add the new messages on the bottom. This method works if the mud is loaded with messages for short spans of time. This will keep timing constant.

By what I read, your message handling doesn''t seem correct. Most likely, you will receive a command character by character as the player types it in. That is where the input buffer comes in. Update the buffer as characters come in. When you receive a LF character, that''s when you know a command has been entered. Pop the command off the buffer and enqueue it in a queue behind all the other commands. This makes sure that no person has a distinct advantage in the message queue.

Your main loop should look something like this:

GetInput();
HandleMessages();
AIStuff();
DoOutput();

Hope this all helps
~Mark


Jeez.. there''s some chaos with terminology here..

Full-duplex just means that you can send data in both directions equally (send & receive). I don''t know of any internet protocol which is not full-duplex.

Blocking/non-blocking refers to the way OS function calls are handled. If you call the recv() function on a socket, and no data is available, the recv() function will wait until data is available on a blocking socket. On a non-blocking socket it will return with an EAGAIN/EWOULDBLOCK error.

The select() function is more or less Unix''s WaitForMultipleObjectsEx (for the Win API guys out there). You give it a number of file descriptors (just the numeric IDs you use to handle sockets&files on Unix-like systems), and it will wait until data is available on those sockets, or the timeout was reached, or your process received a signal (such as Ctrl-C or a TERM/KILL signal).
You will always use select() no matter whether you use blocking or non-blocking sockets, simply because without select(), you''d have to implement a busy loop - in other words, your program would always take 100% of the CPU speed (if it can). This is not a good idea for server applications...

Generally non-blocking sockets should be preferred over blocking sockets if your process/thread needs to handle multiple sockets, simply because it reduces the danger of accidently reading from a socket when no data is available - that would effectively "hang" the process.

As to whether you should use threads or not: you definitely shouldn''t have a one threads per client approach for a MUD, simply because the cost is too high and the OS might not support that many threads. However, if you need to send a lot of data at once (I don''t think this should happen in a MUD), then you should consider doing it in a seperate thread, as it could stress the OS socket send queues, thus forcing you to wait until they get empty.

So this is how I would write a MUD basically:
1. Load any scripts / world information
2. Set up the listen socket(s)
3. Enter an infinite loop:
a) Do a select() on all sockets (the listen socket plus any open client connection socket)
b) Check whether there are new incoming connections (accept() on the listen socket)
c) Loop through all client socket checking whether there''s incoming data, and process it accordingly

cu,
Prefect

Resist Windows XP''s Invasive Production Activation Technology!
One line of sourcecode says more than a thousand words.
Widelands - laid back, free software strategy

This topic is closed to new replies.

Advertisement