Of Users and ServersI 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 wordsNow 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 here at the GameDev.net forums. He is a game developer and a regular MUD user. |
|