Winsock Sending Data Back To Clients?

Started by
-1 comments, last by C0UNT Z3R0 20 years ago
Hi all, This is the server part of a networking tutorial from gametutorials.com was just wondering if anyone could help me sort it out so that the data gets sent back to the relevant clients. I would really appreciate the help. Heres the code... /**************************************************** Networking Tutorials #5 Multiple Hosts - ServerSide by redKlyde ****************************************************/ // Things that will be covered in this tutorial: // Handling connections from more than 1 host // using select() // using file descriptor sets // A quick note on threading: // In order to accomplish the goals of this tutorial, it will be necessary to do some threading. // However, since it is not the aim of this tutorial to give an in depth education on threading, // we will have to do a little magical hand-waving at some point in the threading discussion. // Even though we aren''t going to TEACH threading, we should still cover the basics // so we have a clear understanding of what is going on and why, so here we go ... // Why should we have threads in the first place? Well there are 2 basic types of functions // when we talk about networking. They are either blocking or non-blocking. A blocking function is // one that will halt the program until the function completes, or "blocking" the program from // continuing. A non-blocking function is one that will, obviously, NOT halt the program but // let it run while the function completes. This information is important to some of our functions, // in particular, the accept() function. When we call accept(), the program will wait inside the // accept() function until it receives a connection from a user. Obviously, if we want to send and // receive information from users, we can''t do this if our program is waiting to accept a connection. So // we have to make a thread to accept connections for us, while we run the program and communicate // with the users. We have a similar problem with the recv() function. The recv() function will ALSO // block until it receives data from the specified socket. This is bad because what if we read from // a socket that hasn''t sent any data? Well, we will have to wait until data is sent before we can // continue with the rest of the program. Of course, we don''t want to do this, because especially when // we are making a game, we need to send and receive data very quickly so we can process our game // objects. This was not a problem in our earlier examples because the system of sending and receiving // was very well defined. From the client we ALWAYS sent our message and then received a confirmation // from the server. On the server, we ALWAYS received a message from the client and then sent // our confirmation. Not only that, but we only had one client, so it didn''t matter when the client sent // the data, there wasn''t another client waiting for service. // How on earth are we going to fix these problems? Well, with a couple different solutions. Keep in // mind that again, this is not the ONLY way to get around these issues, but it is a simple, and effective // way to create a server application. The first thing we are going to do is create a thread to // accept connections. The second thing we will do is utilize the select() function to find out which // sockets already have data on them. This way we will not block while we wait to receive data // with recv(). Instead, the data will already be there. // Let us talk about threading first. // What is a thread anyway? In simple terms, a thread is a function that we can run at the same time // as another function. What we will do, is create a procedure (aka. a function) that will handle accepting // new connections, run this function as a thread (so it happens at the same time, or in other terms, so it // runs asynchronously), and our main program will handle sending and receiving to all the clients that // our thread accepts. // Now on to select(). // How does select() work to handle the blocking recv() function? What select() will do is create for us // a list of sockets that already have data to read. We will have to create a "socket set" that has // all our client sockets, and "select" from that set all the sockets with data ready. This way, when we // call recv(), we are guaranteed that we will receive data immediately, and we will not have to block // until data is sent. // That''s probably enough explanation for now, lets dig into the code. We start as usual with all the // proper setup. // winsock for our network communication #include <winsock2.h> // stdio for I/O #include <stdio.h> // ----------------------------------------------------------------------------------- // startupServerForListening() - will return us a socket that is bound to the // port we specify, and is listening for connections if all operations succeeded, // and a -1 if startup failed. int startupServerForListening(unsigned short port); // ----------------------------------------------------------------------------------- // shutdownServer() - will take the socket we specify and shutdown the // network utilities we started with startupServerForListening() // Note: In order to function properly, the socket passed in MUST be the // socket created by the startupServerForListening() function void shutdownServer(int socket); // Note: When programming multi-threaded applications, it is necessary to tell the compiler that we // are using more than one thread. This is so the compiler can chose certain functions that are said // to be "thread-safe". Some functions that we commonly use are not designed to work with more than // one thread. Calling these functions would cause some unexpected results, or even some serious // problems. So be certain to always set the libraries to be thread-safe. Do this by clicking Project/Settings. // Go to the C++ Tab. Change the category to "Code Generation". Change the "Use run-time library" // drop down box to "Debug Multithreaded", and you are ready to go. // Normally, after our definitions and includes we would jump right into the main() and start setting // up the network, but this time we will explore the acceptance thread first, since it is the most new // concept to our architecture. // First some definitions. // For threading, we will need a thread handle. We will receive this handle when we create the thread // and can use this handle any time we need to reference our thread. HANDLE threadHandle; // The beauty of threading is that both of our threads will have access to the same memory locations. // But this is dangerous because we could both potentially attempt to read or write to that memory at // the same time! So we need some way to protect that memory so that only one thread at a time // can control it. For this, we need a mutex. A mutex is a structure we can lock and unlock any time // we write to a shared section of memory. No other thread can lock our mutex while we have it locked, // and our thread will block until we gain access to the mutex. // a mutex for my shared data HANDLE mutexHandle; // Finally the file descriptor set (FD_SET). A FD_SET is simply our list of sockets. // my master socket set which will be protected by my mutex FD_SET masterSet; // A FD_SET has data members: // - fd_count - the number of file descriptors in the set // - fd_array - the array of file descriptors // There are a few convenient macros we can use to manipulate the set. // - FD_SET(a socket, FD_SET to manipulate) - This macro will add a socket to a set // - FD_ZERO(FD_SET to manipulate) - This macro will zero a set // - FD_CLR(a socket, FD_SET to manipulate) - This macro will remove a socket from a set // - FD_ISSET(a socket, FD_SET to manipulate) - This macro will check to see if a socket is in a set // A quit flag I will use to exit my program with if something goes wrong bool gQuitFlag = false; // Now that we have everything defined that we will need, lets look at the acceptance thread procedure. // The sole responsibility of this function is to accept connections from our clients, and add them to the // masterSet. This way our main thread can be free to send and receive data without blocking during // the accept call. // ----------------------------------------------------------------------------------- // acceptingThreadProcedure() - a procedure that will accept clients and add their // sockets to the master file descriptor set void acceptingThreadProcedure(int* serverSocket) { // copy my socket over to a local variable int mySocket = *serverSocket; // run forever for (; { // accept a client like normal unsigned int clientSocket = accept(mySocket, 0, 0); // make sure things are ok if (clientSocket == SOCKET_ERROR) { // just stop of we received any errors printf("Accept Failed!\n"); // signal to quit the application gQuitFlag = true; // and quit this thread return; } else { // Everything went well. We need to take the client socket we received, and add // it to our master socket set so the other thread can use it. Remember though, // since our masterSet can be accessed by both threads, we need to make sure // that we are not trying to write to it in this thread while at the same time we are trying // to read from it in the other. We need to lock our mutex. We do this with the function // WaitForSingleObject(). We need to give WaitForSingleObject() our mutex handle // and a time value to wait. We will use INFINITE for the time value, because there // really isn''t anything else our thread needs to do. // lock the mutex WaitForSingleObject(mutexHandle, INFINITE); // add this socket to the master set using our FD_SET() macro FD_SET(clientSocket, &masterSet); // Now we have to unlock our mutex so that our main thread can also access the data // To be absolutely clear on this, here is how it works. When we call WaitForSingleObject() // we have locked the data. We have control of the mutex until we call ReleaseMutex(). // If the main thread were to call WaitForSingleObject() before we call ReleaseMutex(), // WaitForSingleObject() will block until we unlock the mutex. At that point, WaitForSingleObject() // would immediately get a lock on the mutex, and the main thread would be able to continue. // So always lock the mutex, write/read the data, and then unlock the mutex. This will make // everyone trying to access the data very happy. // unlock the mutex ReleaseMutex(mutexHandle); // a quick message printf("client on %d connected\n", clientSocket); } } } // Well, that''s all our acceptance thread needs to do. Once we start the thread, we will begin accepting // clients and adding them to the masterSet. // Now to write our main(), we will need to startup the network like normal, start our acceptance thread // and then send and receive data from our clients. Since we are servicing multiple clients, we will have // to create a loop and continuously check our network. void main() { printf("Welcome to redKlyde''s Networking Tutorials!\n"); printf("Tutorial # 5 : Multiple Hosts - ServerSide\n\n"); // Startup our network as usual. // the socket my server will use for listening int serverSocket; // startup my network utilities with my handy functions serverSocket = startupServerForListening(7654); // check for errors if (serverSocket == -1) { printf("Network Startup Failed!\nProgram Terminating\n"); return; } // And now ... the magical hand waving act. It would be great if this were a REAL threading tutorial, // but unfortunately it isn''t. So, at this point we will just have to say that this stuff works. We will // create our mutex and our thread and receive handles for both. // create the mutex mutexHandle = CreateMutex(NULL, false, NULL); if (mutexHandle == NULL) { printf("Error creating mutex\n"); shutdownServer(serverSocket); return; } // create the thread int threadId; threadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)acceptingThreadProcedure, &serverSocket, 0, (LPDWORD)&threadId); if (threadHandle == NULL) { printf("Could not start acceptance thread\n"); shutdownServer(serverSocket); return; } // sleep the main() so the acceptance thread has time to start ... this is cheesy Sleep(100); // Now that that is all over with, it''s down to business. // It''s always a good idea to initialize our structures before we access them, so let''s zero our masterSet. FD_ZERO(&masterSet); // the main loop ... run forever for (; { if (gQuitFlag) { break; } // Now the fun part. At this point we have our masterSet that contains a list of all our client sockets. // We need to look in that set for clients that have data, and then read their data. This means we have // to access our masterSet that is, again, ALSO used by our acceptance thread. Which means we have // to use our mutex, so let''s get a lock on our mutex so we can properly access masterSet. // lock the mutex WaitForSingleObject(mutexHandle, INFINITE); // Instead of reading directly from masterSet we are going to make a copy of it instead. // This will keep us from having to lock the mutex for the entire duration of the loop. Remember, // as long as we are accessing masterSet, we need to keep the data locked. So we will need a temporary // set to use with the select call, we will call this the polling set. // make the polling set and copy everything from masterSet FD_SET pollingSet = masterSet; // unlock the mutex ReleaseMutex(mutexHandle); // Here is an interesting scenario. If we were to select() from a set with count 0, we would actually // generate an error. We could use WSAGetLastError() and handle the error properly, but it is easier // to avoid the situation entirely. We can do this by NOT calling select() if the set was empty. So check // the count, and just skip the select if our set is empty. // check if our set is empty if (pollingSet.fd_count == 0) { continue; } // Now it is time to use select(). We have a set that contains all of our socket, and that set is not empty. // Select() will actually alter the set we give it, and remove any sockets that do NOT have data ready. For // that reason, we always want to use the copy of the master set when we use select. If not, we will lose // all the sockets that did not have data read, which is never a good thing. // select() has 5 parameters. They are: // - the number of file descriptors // - a FD_SET to check for readability // - a FD_SET to check for writeability // - a FD_SET to check for errors // - a wait time // The wait time is a timeval structure. You can set the number of seconds and microseconds for select // to wait before it times out. This can be useful in specialized circumstances, but we will just make it 0 // so it times out immediately and returns. // the wait time timeval waitTime; waitTime.tv_sec = 0; waitTime.tv_usec = 0; // and select(). select from the polling set using fd_count as the number of sockets. We do not have a // write set nor an errors set, so just pass NULL for them. int result = select(pollingSet.fd_count, &pollingSet, NULL, NULL, &waitTime); // The return value for select() is the number of sockets that have data ready to read. Like most our // networking function, if an error occurred, this value will be SOCKET_ERROR, otherwise it will indicate // success. // But select() can return 0 if there was no data to be read. In this case, just continue on with the // rest of the program. // check for no sockets with data if (result == 0) { // no sockets have data continue; } // check for errors if (result == SOCKET_ERROR) { printf("Error in select()\n"); continue; } // Now that we have the polling set that contains just the sockets that have data, let''s step through // the list of sockets and read from them all. // for every socket in my polling set for (unsigned int i = 0; i < pollingSet.fd_count; i++) { // We can access the socket list directly using the fd_array member of the FD_SET unsigned int clientSocket = pollingSet.fd_array; // We will be using the same variable length data system that we implemented in tutorial #4. // So we need a few variables to facilitate the communication. // the number of bytes we received int nBytes; // a buffer to hold my data #define MAX_MESSAGE_SIZE 4096 char buffer[MAX_MESSAGE_SIZE]; // the size of the message that is being sent unsigned long messageSize; // receive the message size first nBytes = recv(clientSocket, (char*)&messageSize, sizeof(messageSize), 0); // check for errors if (nBytes == SOCKET_ERROR) { // Uh Oh! We have our first real use of error handling! Something can happen here that is // pretty significant. Let''s grab the error number from winsock. int error = WSAGetLastError(); // The error we got should be WSAECONNRESET. This error says that the connection was either // closed or somehow reset from the other end. That means that our client has shutdown his // application. We have 2 real ways of dealing with closing connections. One is to send a // disconnect message from the client to the server. The other is to handle the dropped // connection error. The disconnect message is something that we would have to write ourselves. // We would somehow make an identifier message that tells our server that we are disconnecting, // that way we can handle it properly. That will work assuming that all our connections are closed // cleanly (like they are supposed to). But what if someone just shuts down their application, or // looses power to their computer. They can''t send a message if their computer isn''t on! // So we still need to check for the error condition. // handle the dropped connection if (error == WSAECONNRESET) { // When we receive this error. Just get a lock on the master set, and remove this socket from // set using the FD_CLR() macro. // lock our mutex WaitForSingleObject(mutexHandle, INFINITE); // remove the socket from our master set FD_CLR(clientSocket, &masterSet); // unlock our mutex ReleaseMutex(mutexHandle); // close the socket on our side, so our computer cleans up properly closesocket(clientSocket); // a quick message printf("client on %d disconnected\n", clientSocket); // move on to the next client continue; } else { // we failed, but it wasn''t an error we were expecting ... so kill the server printf("Recv Failed!\n"); gQuitFlag = true; break; } } // We have already handled the unexpected disconnect from a client, but what about the first case we // talked about, the "clean disconnect"? Well in the case of the clean disconnect (the client called closesocket()), // we will NOT generate an error. Instead when we read from the socket, it will return us 0 bytes. // When this happens we should simply handle it just like we did the disconnect, remove the socket from our // master set, and continue. if (nBytes == 0) { // lock our mutex WaitForSingleObject(mutexHandle, INFINITE); // remove the socket from our master set FD_CLR(clientSocket, &masterSet); // unlock our mutex ReleaseMutex(mutexHandle); // close the socket on our side, so our computer cleans up properly closesocket(clientSocket); // a quick message printf("client on %d disconnected\n", clientSocket); // move on to the next client continue; } // At this point, the rest is just like tutorial #4. Just receive the rest of the message and print it out. // convert the message size to host ordering messageSize = ntohl(messageSize); // receive the reset of the message nBytes = recv(clientSocket, buffer, messageSize, 0); // check for error if (nBytes == SOCKET_ERROR) { printf("Recv Failed!\n"); gQuitFlag = true; break; } // remember this is a string, so terminate the buffer so we can print it buffer[messageSize] = ''\0''; // print the message printf("Message Received from client on %d : %s\n", clientSocket, buffer); } } // cleanup shutdownServer(serverSocket); printf("Press Enter to Exit ...\n"); getchar(); // That''s just about it. You now have all the tools you need to handle connections from multiple // hosts and a send and receive data between them and the server. This also concludes the basic // networking tutorials. Part 2 of this tutorial does not cover any new material. It has some mild // changes to allow clients to send information they type in, but no new material is covered. So, // browse through it if you like; otherwise, I hope these tutorials have been informative enough about // networking and helpful enough with code examples that you may have many joys in experimenting, // and a little less pains than the rest of us. Thanks for enduring my ramblings, and best of luck! // - redklyde } // ----------------------------------------------------------------------------------- // startupServerForListening() - a function to startup winsock, and open a socket for listening int startupServerForListening(unsigned short port) { // an error code we will use to get more information about our errors int error; // the winsock data structure WSAData wsaData; // startup winsock if ((error = WSAStartup(MAKEWORD(2, 2), &wsaData)) == SOCKET_ERROR) { printf("Could Not Start Up Winsock!\n"); return -1; } // create my socket int mySocket = socket(AF_INET, SOCK_STREAM, 0); // make sure nothing bad happened if (mySocket == SOCKET_ERROR) { printf("Error Opening Socket!\n"); return -1; } // the address structure struct sockaddr_in server; // fill the address structure with appropriate data server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = INADDR_ANY; // and now bind my socket if (bind(mySocket, (sockaddr*)&server, sizeof(server)) == SOCKET_ERROR) { printf("Bind Failed!\n"); closesocket(mySocket); return -1; } // mark my socket for listening if (listen(mySocket, 5) == SOCKET_ERROR) { printf("Listen Failed!\n"); closesocket(mySocket); return -1; } printf("Server Started\n"); return mySocket; } // ----------------------------------------------------------------------------------- // shutdownServer() - a function to shutdown a socket and clean up winsock void shutdownServer(int socket) { // kill my thread and my handle WaitForSingleObject(threadHandle, INFINITE); CloseHandle(threadHandle); CloseHandle(mutexHandle); // close our socket closesocket(socket); // shut down winsock WSACleanup(); printf("Server Shutdown\n"); }

This topic is closed to new replies.

Advertisement