|
MUD Pies: Part 1 - Let's make a MUD server
Prelude
So, you have made your Tetris/Pong clone and now you have a brilliant idea for your next project! Almost invariably this is how a beginner game programmer starts out, and almost invariably that idea is to create a RPG or a MMORPG. (If this is not you, and you have no idea what I am talking about: MUD is Multi User Dungeon, RPG is Role Playing Game and MMORPG is Massively Multiplayer Online RPG) Well hold your horses. Let's not go leaping in. Chances are you have a great story line, or you have a brilliant battle engine worked out. You probably have this fantastic plan (read: delusion of grandeur) of thousands of people connecting to your game and making you millions of dollars. Well I'm sorry to be the one busting your bubble (and for newbie bashing), but I am afraid your plan isn't as great as you think. Chances are you do not have the ability to do all the content creation (music, artwork, models, etc) required for a game as large as an RPG, and chances are you probably don't have the complete programming ability to do it either. An RPG simply is not the next step after Tetris. But wait! Don't close the article, because I am here to help! Over this series I am going to show you how to make a MUD. It basically amounts to a cross between a chat server and a small RPG. All you will need to know is basic C++ and some Windows code. As an added bonus, I will be trying to keep it as extendable as possible, so that you can slowly evolve your server from a simple chat server to an advanced graphical multi-user universe. Topics I plan to cover over this series will include:
Just don't get ahead of yourself, though. First thing's first. In part one of this series I will be teaching you how to get a basic Windows application going with sockets. We will:
If you already know how to use Winsock, then you can probably skip this part of the series - although it may be useful to at least skim over the code. I will try and keep this one simple and explain as much as I can, including some coding techniques you may not know about. There are things that some of you may already understand fully, so bear with me. And finally, before we launch into it, a quick note about my code. I use Borland C++ Builder 3, so if you use MSVC you may have to change around a few things. For example, you may need to import libraries manually. It may be useful to have access to the Win32 SDK (including the Winsock2 API documentation) for this article, as I will not explain every API call in too much detail. For this particular article, I want to say a big thanks to Tim C. Schr?der (tcs) who used to run glVelocity here at GameDev. Also a very special thanks to Matthew Daley (MattD), as large parts of code in this article are based off his code. You can download the source code here. Thomas "Cow In The Well" Cowell has been kind enough to change the source around so that it compiles under MSVC. This version should also compile under BCB. You can also download it here. The Application Main Function OK, we are going to start by adding the basic application functionality. We are going to encapsulate Windows functionality in the class CApp. I will try to explain how to make a basic Windows application, but it is recommended that you know how to do this already. Create the class CApp and add constructor and deconstructor if you wish. Now we add the public function Main, which will be called directly from WinMain (the program entry point). It should look like this: class CApp { public: int Main(HINSTANCE hInstance, LPSTR lpCmdLine, int nCmdShow); }; Next we will create the body of the Main function. We will start with creating the window. WNDCLASSEX wcex; HWND hWnd; message window // Register the Windows class: wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC) GlobalWndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = NULL; wcex.lpszClassName = WINDOW_CLASS; wcex.hIconSm = NULL; if (!RegisterClassEx(&wcex)) return 0; // Create the Window hWnd = CreateWindowEx(NULL, WINDOW_CLASS, MUD_WINDOWTITLE, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX, GetSystemMetrics(SM_CXSCREEN) / 2 - 300 / 2, GetSystemMetrics(SM_CYSCREEN) / 2 - 200 / 2, 300, 200, NULL, NULL, hInstance, this); if(!hWnd) return 0; ShowWindow(hWnd, SW_SHOW); UpdateWindow(hWnd); I am not going to explain that in too much detail, basically we are registering with Windows what we want our window to look like. You will notice the lack of data in the wcex structure; this will be a very simple window - I hate making interfaces in Windows. Next we actually create the window. hWnd is a handle to the window which will be used to refer to it. This is fairly obvious, just setting up appearance and sizes. The last argument is for user data. Later we will need to get a pointer to our CApp class back from Windows, so we use this. If you still don't understand how I did that, look up "WNDCLASSEX", "RegisterClassEx' and "CreateWindowEx" in the Win32 SDK. You will also need to define the following: #define MUD_WINDOWTITLE "Andrew Russell's MUD Server (v0.0)" #define WINDOW_CLASS "MUD_SRV" Last but not least, let's make it go into a message loop like so: // Go into the message loop MSG msg; while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 1; This keeps getting messages until GetMessage returns false. When it returns false, it indicates that the program needs to end, so the function will go to return 1 and exit. Handling Messages Now if you have programmed Windows before, you will know about the message system. Basically you register a callback function (using a function pointer). Windows will call that function giving it all sorts of different messages for it to process. This is how the application knows about what Windows is doing. Now, I am assuming you know about classes and how you can create multiple instances of a class. However you may not know about how function pointers work, particularly with classes. A function pointer is basically a pointer that, instead of pointing to data, points to a function to be executed. When you register your window with Windows you give it a pointer to the function that handles messages (look at wcex.lpfnWndProc above). If you need more info about function pointers, try your compiler help files. Now the problem that occurs with classes is that a function only exists once in memory, and that function or its function pointer does not have any information on which instance of the class it came from. Basically, a function used in a function pointer must be global (not in a class). There is a work around for this. The function must be declared static. If data is static, then it only exists once (no more, no less) no matter how many times you create its class. A static member function can only directly access static member data, as it is not bound to any particular instance of that class. This will be important when we get on to singletons. So now we add the static member function, GlobalWndProc to the public section. You will recall that this is what we set wcex.lpfnWndProc to point to. static LRESULT CALLBACK GlobalWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { LPCREATESTRUCT cs; if (message == WM_NCCREATE) { cs = (LPCREATESTRUCT) lParam; SetWindowLong(hWnd, GWL_USERDATA, (LONG) cs->lpCreateParams); } CApp *pApp = (CApp *) GetWindowLong(hWnd, GWL_USERDATA); return pApp->WndProc(hWnd, message, wParam, lParam); } If you have never made a Windows application before, never mind about all the funny data types, you can learn them from a proper Windows programming tutorial. Now you will notice that I have filled in the contents of this function. The first part will respond to a WM_NCCREATE message and take the this pointer that we put in the call to CreateWindowEx, and use the SetWindowLong function to transfer that pointer to the user data of the hWnd. The next section responds to every other message. Because the static function does not know about any particular instance of CApp, it gets the pointer to the instance we want from the window user data. Then it calls the specific WndProc function of the application. In the end the major benefit of this is that you can make all the other functions virtual and then inherit CApp and you will still call the correct version of WndProc. The other benefit is that we can keep everything nicely in a class. But of course, we actually have to write WndProc. Add it to the private section of CApp. private: LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); Again, don't worry if you don't know about the data types. Look them up in the SDK if you really need to know. And onto the body of the function: LRESULT CALLBACK CApp::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CLOSE: // Quit the program PostQuitMessage(0); break; case WM_PAINT: PAINTSTRUCT ps; HDC hdc; hdc = BeginPaint(hWnd, &ps); RECT rt; GetClientRect(hWnd, &rt); DrawText(hdc, MUD_TITLE, strlen(MUD_TITLE), &rt, DT_CENTER); // Note: the folowing line will move the rectangle down so you can add more text // rt.top += 16; EndPaint(hWnd, &ps); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } At the moment the function only processes two messages, the close message and the paint message. All other messages get sent through DefWindowProc, which means Default Window Process. This just defines the default behaviour. The close message here simply sends a quit message, but it doesn't have to. This is what is used for those "Are you sure you wish to close?" message boxes. The paint message is what we will add to throughout this tutorial to display important status in our window. This does not need to display much, as we will be using remote administration for pretty much everything. Note that you will also need the following define: #define MUD_TITLE "MUD Pies - Example server" Run it Finally, you have to create the actual WinMain function to call the Main function of CApp. WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { CApp Application; Application.Main(hInstance, lpCmdLine, nCmdShow); return 0; } At this point, just make sure you have all your includes in the right place. Make sure to include windows.h and winsock2.h before any calls to Windows or Winsock specific functions. If your compiler pukes, then you probably don't have your includes right. If your linker pukes, remember to have the lib files added correctly. Fun with Sockets I will be honest with you. I hate programming sockets. I would much rather simply use a library. If you are like this, just download the source code and do it from there, you won't really be losing out on much. Although I do have a good explanation of pure virtual functions here. If you really want to learn, then here we go. Sockets are the computer's representation of their connections to each other. Basically there are two types of sockets, a listening socket and a normal socket. A listening socket is also known as a server socket, as it takes incoming connections. When the listening socket gets an incoming connection (on a multi-user server), then it passes that connection on to a normal socket so it is free for other users to connect to. Apart from taking a connection from a listening socket, a normal socket can also be used to make a connection to a server's listening socket. While we will not be doing this at this point in time, the socket classes I will present here have this functionality so they can be reused in any program you like. As they will be sharing some functionality, we will be creating a pure virtual base class to contain the shared functionality. A pure virtual base class is a class that has at least one pure virtual function. You cannot create an instance of a pure virtual class. A pure virtual function is defined as a virtual function that has no contents in that particular class. Your compiler will prevent you from creating any instances of a pure virtual class; however like any inherited class you can reference any of the child classes using the parent's type. You create a pure virtual function by tagging = 0 onto the end like so: class parent { virtual int foo() = 0; }; class child : public parent { virtual int foo() {return 1234;} }; int function() { parent* bar = new child; bar->foo(); } As you can see in this example, we create an instance of the class child, but it is referenced as a pointer to the class parent. When you call the function foo, it in fact resolves that the class is of type child and calls the appropriate version of foo. Now I will show you the socket base class. I am just going to shove it all here and then explain it in detail if you want to read it. class CSocket { public: inline int GetStatus() {return status;} inline SOCKET GetSocket() {return sock;} // Send text to the socket inline void SendText(char* message) { send(sock, message, strlen(message), 0); } // Pure Virtual to be defined by child classes. virtual void GetText(char* dest, int length) = 0; // Disconnect the socket void CloseSocket(); protected: // The Winsock representation of a socket SOCKET sock; // The address of the socket (on TCP/IP this is hostname/ip and port) SOCKADDR_IN addr; // Connected? int status; // binds the address to the socket int BindSocket(); }; Note that we use protected instead of private, as we will be using this as a base class, and we want the child classes to have access to items in this section. This piece of code and its comments should be fairly self-explanatory. I will go through the function bodies in a moment. First I will explain the keyword inline for those who don't know how it works. Say I created two functions like so: inline int foo(int n) { output(n); return n+5; } int bar() { int temp = foo(6)+100; return temp*12; } The compiler would compile the code like so (before optimisations) int bar() { int temp; output(6); temp = 6+5; return temp*12; } This saves the overhead required to call a function. It should be used on small and frequently used functions to gain a speed increase. Getting back to the functions. GetText has no function body; it is a pure virtual function as I explained before. SendText is self-explanatory; it calls the Winsock function, send, with the appropriate arguments. GetStatus and GetSocket simply return the values of the appropriate variables. This is an important aspect of object-oriented programming. It hides the variables away in the private or protected section, but allows you to read their value using the interface functions. Notice that there is no way to set these variables externally. This helps prevent potentially buggy code and more importantly, if a bug is introduced, it is contained to one class, as only the owner class is allowed to access its variables. While some programmers, especially new ones, do not like the extra work in creating interface functions, it helps save much work later and also improves code reusability. The function CloseSocket has the following body: void CSocket::CloseSocket(void) { shutdown(sock, SD_SEND); // Stop socket from sending any more data closesocket(sock); // Cancel any pending transfers and release sock status = 0; } You can probably work out this function from the comments. The other function that needs a body is BindSocket. int CSocket::BindSocket(void) { if(bind(sock, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR) { return 0; } return 1; } This function calls the Winsock function bind. When you create a socket, you give it an address you want it to connect to (or if it is a listening socket, an address where you want it to accept connections from). The function, bind, will bind the address structure, addr, to the actual socket. Let me tell you a little more about addresses. You may have read the comment that was on the addr variable in the class body above. TCP/IP is a type of communications protocol. A hostname is what a computer is called. It corresponds to an IP address. Take www.google.com, it is a hostname, where as 123.12.31.23 is an IP address. An IP address is how computers identify themselves on a network. A hostname is something that people can understand easily, it will resolve to an IP address, which computers can understand easily. A port is how a computer can sustain many connections at once. Each connection is given a different port. A listen socket opens itself on a port and waits for incoming connections. A normal socket will usually be on a high numbered port. Any port can be connected to any other port on any other computer. Let us now create the listening port. First create the class definition, inheriting the base class. If you don't know how this is done, here it is. class CListenSocket : public CSocket You will need to add the definition of the function GetText. Remember this is the pure virtual function from before. Now we will give it a body. void CListenSocket::GetText(char* dest, int length) { int bytes = recv(sock, dest, length - 1, 0); if(bytes == SOCKET_ERROR) { dest[0] = ' '; return; } dest[bytes] = ' '; } This function uses the Winsock function recv to fill the buffer passed to it in the argument dest, up to the length of the buffer specified. It then appends the nul character to indicate the end of the string of text. To create a buffer, you would do the following: char* buffer = new char[1024]; // creates a buffer 1024 charcters long lstnsock->GetText(buffer, 1024); // fills that buffer // use the buffer here delete[] buffer; // do this when you are finished with the buffer Note that it is probably a good idea to #define how long the buffer is, instead of using the value every time. This is to prevent one value from changing and forgetting to set the other. If the buffer size and the argument given for length differed, then it may cause a buffer overrun. If this occurs, then it is possible to overwrite other variables in the program (Not a good thing. Buffer over running is a major security hazard for a server.) You may be thinking, "where is the function to open a connection?" well, here it is. Because it is different between each type of socket, we have not added it to the parent class. As such, if you had a socket referenced as CSocket and not a specific type, you would have to know what sort of socket it was and cast it to the correct type before you could call (drum roll) CreateSocket: int CListenSocket::CreateSocket(HWND hWnd, UINT message, int port) { if(status) return 0; sock = socket(AF_INET, SOCK_STREAM, 0); if(!sock) return 0; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = htonl(INADDR_ANY); if(!BindSocket()) return 0; if(!MakeListenSocket(hWnd, message)) return 0; status = 1; return 1; } Going down through the code, we first come to a call to the function socket. This creates the socket. Next we start adding values to the addr structure. This tells our socket what sort of a connection we are creating and to where. The INADDR_ANY means that our listening socket can accept a connection from anywhere. The port is which port the socket will listen on. Next we call BindSocket to bind the address to the socket. Then we call MakeListenSocket. You will notice that I haven't provided this function yet. Here it is: int CListenSocket::MakeListenSocket(HWND hWnd, UINT message) { if(listen(sock, CSOCK_BACKLOG) == SOCKET_ERROR) return 0; if(WSAAsyncSelect(sock, hWnd, message, (FD_ACCEPT | FD_READ | FD_CLOSE)) == SOCKET_ERROR) return 0; return 1; } The function listen creates a listening socket, while the function WSAAsyncSelect sets it up to be asynchronous. You may have been wondering why we needed a window handle and a window message type as arguments. The reason for this is that Windows will send us messages whenever there is a connection, a disconnection, or waiting data on a socket. Later we will add handling of these messages to our WndProc function from before. Finally we make the deconstructor close the socket if it is connected. CListenSocket::~CListenSocket(void) { if(status) CloseSocket(); } Before we do anything else we will need our normal socket. It should be created like our listening socket class, inherited from CSocket. I have called it (obviously) CNormalSocket. To begin with, quickly copy the deconstructor and the function GetText from the CListenSocket. We still want to disconnect when we delete the socket, and we a still getting text in the same way. What will be different about this socket is the way we connect. This socket will have two connection types. First we will put in the connection we use if we use the socket to connect to the server (a client connection). Copy the function CreateSocket from the listening socket. It will be almost the same. Now we do not want to be connecting to any IP, so remove htonl(INADDR_ANY) and replace it with inet_addr(ip). The argument of that function needs to be a nul terminated string of characters so add "char* ip" (no inverted commas) to the arguments section of the function. Finally, remove the call to MakeListenSocket (that function does not exist in this class) and replace it with the following: if(hWnd) if(WSAAsyncSelect(sock, hWnd, message, (FD_READ | FD_CLOSE)) == SOCKET_ERROR) return 0; This works in the same way as the call in MakeListenSocket, it makes the socket send messages when it either has data waiting, or it is disconnected. That was easy. The next way to create this sort of socket is to accept a connection from a listen socket. So we create the following function: int CNormalSocket::CreateSocket(CListenSocket* serversock) { if(status) // Don't create a socket if one already exists return 0; int addrsize = sizeof(SOCKADDR); sock = accept(serversock->GetSocket(), &asyncaddr, &addrsize); if(sock == INVALID_SOCKET) return 0; status = 1; return 1; } The important line there is where it calls accept. This takes the connection to the listening socket and transfers it to the normal socket. Then communication can take place via the normal socket. It requires the piece of data asyncaddr, which is of type SOCKADDR. Add this to the protected section of the class. This variable is used to contain the address data of an accepted connection. Well, that's it. We have created our socket classes. If you want more information, look up those data types and function calls in the Winsock2 API. Of Users and Servers I am sure that by now you will have had enough of sockets. Well there is a little more to do. We need to use those sockets so we can start accepting connections. This is also to test that we created our socket classes correctly. First add the classes CUser and CServer. Each of these will contain a socket, so go ahead and add a listening socket CServer and a normal socket to CUser. Put them both in the public section. We are not putting them in the private section because there is no reason to. It cannot be deleted because it is not a pointer. Any editing of it can be done weather there were a get method or not. The way to decide if you should put data in the private section is to think if its value can be modified in a way harmful to the program. First we are going to make the server class a singleton. This is a more advanced method of making a global than the one used in CApp. It still keeps everything in a nice neat class. Basically a singleton allows us to only ever have one instance of a class exist at any one time. We are going to make it (almost) impossible to ever have more than one CServer. We do this by using static to make a global exist in a class. First we create a static variable. To the private section, add a static pointer to a CServer. I have called it theserver. We need to initialise the value of this pointer. This happens before the program starts (before WinMain). Do the initialisation like this: CServer* CServer::theserver = NULL; This next function is what makes it all tick. In it you must check if theserver exists (not equal to NULL). If it does not, then create it. Then return theserver. This is the code: static inline CServer* GetServer() { if(theserver==NULL) { theserver=new CServer; } return theserver; } The upshot of this that if it exits, you get a pointer to it and if it does not exist, it gets created and you get a pointer to it. Note that this function is static, so it is availably globally. To get a pointer to the server, use CServer::GetServer(). Finally add the finishing touches by making the constructor private (so it cannot be called except by a member of CServer - i.e. GetServer). Also make the deconstructor set the value of theserver to NULL again so a new server can be created if need be. Next we are going to make a linked list, using CUser as the node class and CServer to contain the root node. If you do not know what a linked list is, basically it maintains a pointer to the next piece of data in the list. This is better than an array, because the number of items in an array is fixed and exists in a fixed section of memory. The number of items in a linked list can continue to grow or shrink and each item in the list can exist anywhere in memory. I am going to create a double linked list for this, mostly because it makes the removal of items in the middle of the list far simpler. It means that you do not have to search through the list up until the item before hand, as you already know where it is. To create the list, we add two pointers to CUser called prev and next. You can put them in the public section and remember not to set them directly, or you can make them private and add get interface functions. The difficulty with making them private is that the list handler (CServer) needs to have access to them, so you could use the friend keyword to get that access, but I leave it as an exercise for the reader. Just remember that the more places that a variable can be accessed, the more places a bug can exist. Next add a pointer to CUser in the server class. This will hold the base of the linked list. The CServer class is where we will add the functionality to handle the list of users. To start with, we need to be able to add users. Because it is so simple, I will just put the code in. CUser* CServer::AddUser() { CUser* user = new CUser; user->next = rootuser; if(rootuser){rootuser->prev = user;} rootuser = user; return user; } When you add a new user, it will make it the new root node and move the whole list down by one. I have also added an overloaded function that takes user as an argument, rather than creating it. Now you can add users, you need to be able to delete them. The easiest way is the delete operator, which calls the deconstructor of CUser. Here is the deconstructor code. CUser::~CUser() { // Join up the list if(next != NULL) next->prev = prev; if(prev != NULL) prev->next = next; // Give the server a new root user if it will end up having none if(CServer::GetServer()->rootuser == this) { CServer::GetServer()->rootuser = next; // If this is the root user, it should have no previous node assert(prev == NULL); } } First we make the next and previous nodes join up. We are checking that they are not NULL so that we don't cause a memory violation (trying to access NULL). Next we make sure that we are not about to delete the root node. This is where the global-like nature of the singleton comes in handy. We simply get the pointer to the server and see if we are trying to delete the root node. If we are, we must make the next node the root node. Notice also the call to assert. When compiled in debug mode, this macro will halt the program if prev is anything other than NULL. If it is, it means that there is a bug in our program because when we delete the root node while next is NULL, we will loose prev and any nodes it points to. This is known as a memory leak. When we have no pointers to an allocated object, there is no way of deleting it and it will simply sit there taking up resources. The benefit of this deconstructor is that we can sit there deleting the root node until it is NULL, as in this function: void CServer::RemoveAllUsers() { while(rootuser != NULL) {delete rootuser;} } That's it. Our linked list now works. Before we finish, we should make the CServer deconstructor call RemoveAllUsers to make sure we don't delete CServer when there are still users in the list. This would cause a memory leak. Now we go back and make our application start up the socket code. In the main function, before the message loop, add in the following: // Init winsock2 int error = WSAStartup(0x0202, &wsaData); if(error) { MessageBox(hWnd, "Could not Init Winsock2", "ERROR", MB_OK); return 0; } else if(wsaData.wVersion != 0x0202) { MessageBox(hWnd, "Could not Init Winsock2 - Wrong Version", "ERROR", MB_OK); WSACleanup(); return 0; } // Open the listen socket (this has the side effect of creating the CServer singleton) if( CServer::GetServer()->socket.CreateSocket(hWnd, SM_WINSOCK, MUD_LSTNPORT) == 0) { MessageBox(hWnd, "Could not create listen socket", "ERROR", MB_OK); } This should be fairly self-explanatory. It starts up Winsock and then tells the server to start up its listen socket. SM_WINSOCK is defined as WM_USER+1. WM_USER is the starting point of where users can create their own messages. MUD_LSTNPORT can be defined as any port on which you want your server to listen on. I have defined it as 3000. Now it will start sending us SM_WINSOCK messages, so we need to respond. We are going to add the handling of individual messages to CServer, so add the following code to initially handle the message: case SM_WINSOCK: CServer::GetServer()->SocketMessageHandler(wParam, lParam); Now we need to add that function to CServer. Like the message processing function, we need a switch statement. As all the messages will need to keep track of a user, throw a pointer to CUser in there as well. Your function should look like this: void CServer::SocketMessageHandler(WPARAM wParam, LPARAM lParam) { CUser* user; switch(WSAGETSELECTEVENT(lParam)) { } } You may have seen them referenced before, and here is where you add the following cases: case FD_ACCEPT: user = AddUser(); user->socket.CreateSocket(&socket); break; This adds a user into the list and transfers the socket from the listen socket. case FD_CLOSE: user = GetUser((SOCKET)wParam); delete user; break; This message happens when a user is disconnected. It deletes the user. Notice the function GetUser, which I shown you yet. It searches through the list and finds a user based on their socket, as a socket is what Winsock passes with its messages, and we want to know which user owns that socket. The following function simply steps through the list until the socket matches. CUser* CServer::GetUser(SOCKET sock) { for(CUser* curuser=rootuser; curuser != NULL; curuser=curuser->next) if(curuser->socket.GetSocket()==sock) return curuser; return 0; } Finally, the last case is this one: case FD_READ: user = GetUser((SOCKET)wParam); break; It finds the corresponding user in the same way as the close method, only here it means that the socket has text waiting in the buffer and it can be read out (using CSocket::GetText) and used in the server. Final words Now that your server is complete (or you use my example program), you may connect to it. It will not do anything useful, but it will accept your connection. To do so go to the run prompt and use (assuming you used port 3000 like I did): telnet localhost 3000 I have given you the tools and now you can go on and create your own MUD server from here on, but next article I will explain how to get users identifying themselves to the server and then talking to each other, using what is known as a finite state machine (FSM). I will also be implementing a user database so that you can store user names and passwords. In addition to all that, I will try to get onto colour for colour telnet clients (Windows 2000/XP has a simple console based client and I will supply a more chat/MUD-oriented one for Windows 9x users) Until next time, have fun, Andrew
Andrew Russell is a Moderator at the GameDev.net forums. He is a game developer and a regular MUD user. Staff Comments
|
|
|||||||||||||||||||||||||
|