h o m e | | | m y s o f t w a r e | | | a b o u t m e | | | c o n t a c t m e |
Live now: Watch Artificial Intelligence bots battle each other in StarCraft
|
Other Languages
|
If you've purposely arrived at my Winsock tutorial, you've most likely found the idea of your own applications communicating via the Internet as fascinating a prospect as I have. Or, perhaps someone else has found the prospect equally interesting and you have been entrusted with bringing this vision into reality. In either case, the Winsock network service and this tutorial will assist you in achieving your goals of commercial enterprise, simply exploring the realm of network programming for personal use, or something in between.
Here's what we'll be covering:
Though you may be eager to reach that awe-inspiring point at which your application successfully makes its first connection, be aware of the concepts behind the code. Try to avoid simply manipulating the given code to suit your immediate needs and instead identify the requirements of your application and only then implement what seems to be the best solution. That's enough of my Zen of Software Development advice for now; let's do some network programming...
Applications servicing outside machines are called servers. Server applications listen for clients by initializing one or more listening sockets. When a client connects to one of these listening sockets, the server receives a notification from Winsock, accepts the connection, and begins to dispatch and intercept messages to and from the new client. Perhaps the most simplistic method by which servers handle multiple clients is to spawn a new thread for each client connection. This server model most often utilizes blocking sockets, which pause temporarily to wait for incoming data, a new connection, and other network events. First, let's identify some structures we'll need to initialize a blocking socket:
struct sockaddr_in { short sin_family; // Protocol type u_short sin_port; // Port number of socket struct in_addr sin_addr; // IP address char sin_zero[8]; // Unused };
The first field is the protocol type, which is usually AF_INET (TCP/IP). As a listening socket isn't concerned with the network address of the machine on which it resides, Winsock automatically assigns an IP address and port number to listening sockets upon creation.
We'll build our first listening server with the above structures and a small army of network functions:
#include <windows.h> #include <winsock.h> #include <stdio.h> #define NETWORK_ERROR -1 #define NETWORK_OK 0 void ReportError(int, const char *); int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmd, int nShow) { WORD sockVersion; WSADATA wsaData; int nret; sockVersion = MAKEWORD(1, 1); // We'd like Winsock version 1.1 // We begin by initializing Winsock WSAStartup(sockVersion, &wsaData); // Next, create the listening socket SOCKET listeningSocket; listeningSocket = socket(AF_INET, // Go over TCP/IP SOCK_STREAM, // This is a stream-oriented socket IPPROTO_TCP); // Use TCP rather than UDP if (listeningSocket == INVALID_SOCKET) { nret = WSAGetLastError(); // Get a more detailed error ReportError(nret, "socket()"); // Report the error with our custom function WSACleanup(); // Shutdown Winsock return NETWORK_ERROR; // Return an error value } // Use a SOCKADDR_IN struct to fill in address information SOCKADDR_IN serverInfo; serverInfo.sin_family = AF_INET; serverInfo.sin_addr.s_addr = INADDR_ANY; // Since this socket is listening for connections, // any local address will do serverInfo.sin_port = htons(8888); // Convert integer 8888 to network-byte order // and insert into the port field // Bind the socket to our local server address nret = bind(listeningSocket, (LPSOCKADDR)&serverInfo, sizeof(struct sockaddr)); if (nret == SOCKET_ERROR) { nret = WSAGetLastError(); ReportError(nret, "bind()"); WSACleanup(); return NETWORK_ERROR; } // Make the socket listen nret = listen(listeningSocket, 10); // Up to 10 connections may wait at any // one time to be accept()'ed if (nret == SOCKET_ERROR) { nret = WSAGetLastError(); ReportError(nret, "listen()"); WSACleanup(); return NETWORK_ERROR; } // Wait for a client SOCKET theClient; theClient = accept(listeningSocket, NULL, // Optionally, address of a SOCKADDR_IN struct NULL); // Optionally, address of variable containing // sizeof ( struct SOCKADDR_IN ) if (theClient == INVALID_SOCKET) { nret = WSAGetLastError(); ReportError(nret, "accept()"); WSACleanup(); return NETWORK_ERROR; } // Send and receive from the client, and finally, closesocket(theClient); closesocket(listeningSocket); // Shutdown Winsock WSACleanup(); return NETWORK_OK; } void ReportError(int errorCode, const char *whichFunc) { char errorMsg[92]; // Declare a buffer to hold // the generated error message ZeroMemory(errorMsg, 92); // Automatically NULL-terminate the string // The following line copies the phrase, whichFunc string, and integer errorCode into the buffer sprintf(errorMsg, "Call to %s returned error %d!", (char *)whichFunc, errorCode); MessageBox(NULL, errorMsg, "socketIndication", MB_OK); }
One thing you may immediately notice about the code is the amount of effort put into error checking. Whenever an error occurs, the code obtains a specific error code with WSAGetLastError() and stores the result in nret. The error code is then sent along with a string indicating the name of the failed function to a custom function named ReportError(). There, an error message is constructed and shown to the user with a call to MessageBox(), which is part of the standard WinAPI. For example, had listen() failed with an error code of 10093 (defined as WSANOTINITIALISED), the finished error string would be "Call to listen() returned error 10093!". You, the prudent developer, would then look up the code and discover that the error occurred because a successful call to WSAStartup() hadn't yet been made.
Also included are defines for NETWORK_ERROR and NETWORK_OK. These might be useful when checking the return value of your own networking functions. If your functions returned one of these values, the calling function could perform a simple equality test to reveal any errors: if (myNetworkingFunction() == NETWORK_ERROR) {...}. The calling function could then obtain a specific code with WSAGetLastError() and handle the error accordingly. Ultimately, implementing a good error-handling scheme now will save you many days or weeks of development time as you'll instantly know why your program has failed.
In addition to returning a new client connection, accept() allows the server to extract information about the client rather than through methods requiring extra function calls or time (which may become an issue in game servers, where the speed of the accept loop is especially critical). To take advantage of this functionality, pass in the address of a sockaddr_in struct cast to a sockaddr pointer, i.e. (LPSOCKADDR)&aSockaddrInStructure. Also, declare an integer variable, set the value of the int to the sizeof the sockaddr struct, and pass the address of the integer as the third parameter. If address information is to be returned after the function call, the length parameter must be present.
This isn't much of a server since it waits for only one user to connect and then immediately disconnects, but that is the most basic design. Just to clear things up, a call to WSAStartup() includes a WORD specifying what version you want to load (in this case it's 1.1) and the address of a WSADATA structure. Next, we'll cover how to connect with other computers.
Creating a socket to connect to someone else uses most of the same functions, with the exception of the HOSTENT struct:
So, let's get right to the code:
#include <windows.h> #include <winsock.h> #include <stdio.h> #define NETWORK_ERROR -1 #define NETWORK_OK 0 void ReportError(int, const char *); int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmd, int nShow) { WORD sockVersion; WSADATA wsaData; int nret; sockVersion = MAKEWORD(1, 1); // Initialize Winsock as before WSAStartup(sockVersion, &wsaData); // Store information about the server LPHOSTENT hostEntry; hostEntry = gethostbyname("www.yahoo.com"); // Specifying the server by its name; // another option: gethostbyaddr() if (!hostEntry) { nret = WSAGetLastError(); ReportError(nret, "gethostbyname()"); // Report the error as before WSACleanup(); return NETWORK_ERROR; } // Create the socket SOCKET theSocket; theSocket = socket(AF_INET, // Go over TCP/IP SOCK_STREAM, // This is a stream-oriented socket IPPROTO_TCP); // Use TCP rather than UDP if (theSocket == INVALID_SOCKET) { nret = WSAGetLastError(); ReportError(nret, "socket()"); WSACleanup(); return NETWORK_ERROR; } // Fill a SOCKADDR_IN struct with address information SOCKADDR_IN serverInfo; serverInfo.sin_family = AF_INET; // At this point, we've successfully retrieved vital information about the server, // including its hostname, aliases, and IP addresses. Wait; how could a single // computer have multiple addresses, and exactly what is the following line doing? // See the explanation below. serverInfo.sin_addr = *((LPIN_ADDR)*hostEntry->h_addr_list); serverInfo.sin_port = htons(80); // Change to network-byte order and // insert into port field // Connect to the server nret = connect(theSocket, (LPSOCKADDR)&serverInfo, sizeof(struct sockaddr)); if (nret == SOCKET_ERROR) { nret = WSAGetLastError(); ReportError(nret, "connect()"); WSACleanup(); return NETWORK_ERROR; } // Successfully connected! // Send/receive, then cleanup: closesocket(theSocket); WSACleanup(); } void ReportError(int errorCode, const char *whichFunc) { char errorMsg[92]; // Declare a buffer to hold // the generated error message ZeroMemory(errorMsg, 92); // Automatically NULL-terminate the string // The following line copies the phrase, whichFunc string, and integer errorCode into the buffer sprintf(errorMsg, "Call to %s returned error %d!", (char *)whichFunc, errorCode); MessageBox(NULL, errorMsg, "socketIndication", MB_OK); }
The most complicated line in the listing is the following:
because it performs several operations---one of them relatively hidden---at once. Let's take it apart step by step:
The h_addr_list member of the HOSTENT struct is basically defined as char **h_addr_list, which is an array of strings, or char *'s. gethostbyname() identified and copied all of the known addresses of the server into this list. However, does the concept of multiple addresses fundamentally make sense? Actually, it does. Your computer, in fact, has an array of general networking addresses. Your Internet address may be 205.182.67.96, your LAN address may be 10.0.0.2, and all computers on which Windows is installed naturally have a "loopback" address of 127.0.0.1, used by the computer to refer to itself on the local network. The same concept applies in the realm of Internet addresses or IP's, which is why a list is needed rather than storage space for a single address. Note that the preferred address, that is, the most accessible address, is always copied into the first element of the list, followed by the second preferred or other addresses.
What's *hostEntry->h_addr_list doing? You might guess that the deference operator (*) is being used to access a single address in the list. However, by failing to provide a specific index, the dereference operation automatically reveals the first, preferred address. That particular section is equivalent to *hostEntry->h_addr_list[0], which is guaranteed to exist since the server must have at least one address.
Next, the char * returned by the dereferencing operation is cast into an in_addr * or LPIN_ADDR. Finally, another deference operation is performed to return the in_addr struct referred to by the pointer, which can only hold a single address. The resulting in_addr struct is then assigned to serverInfo.sin_addr. The subsequent connect() takes the one address as a parameter when forming a connection to the server.
If the server's IP address is known, a valid HOSTENT may be obtained through the use of gethostbyaddr() (as opposed to gethostbyname() used in the previous listing):
LPHOSTENT hostEntry; in_addr iaHost; iaHost.s_addr = inet_addr("204.52.135.52"); hostEntry = gethostbyaddr((const char *)&iaHost, sizeof(struct in_addr), AF_INET); if (!hostEntry) { // Handle accordingly }
In this case, inet_addr() is utilized to copy a string denoting the IP address directly into an in_addr struct. Afterwards, the address of the struct is cast into a const char * as required by gethostbyaddr(). Both methods are referred to as resolving the server address since Winsock returns full address records from partial information.
A few additional notes: port 80 was used simply because Internet web page transfers occur over that port. If you were to send a string to a web server requesting a specific file and attempt to receive something back, you'd have a very simple web browser. Of course, that string must include a full HTTP command. It's great that we can listen and connect to other computers, but communication also involves sending and receiving.
Sending is handled, conveniently enough, by the send() function:
int send( SOCKET s, const char * FAR buf, int len, int flags );
Basically you would copy whatever you wanted into a buffer and use the send() function on a connected socket to make the data go to the other end:
char buffer[256]; // Declaring a buffer on the stack char *buffer = new char[256]; // or on the heap ZeroMemory(buffer, 256); strcpy(buffer, "Pretend this is important data."); nret = send(theSocket, buffer, strlen(buffer), // Note that this specifies the length of the string; not // the size of the entire buffer 0); // Most often is zero, but see MSDN for other options delete [] buffer; // If and only if the heap declaration was used if (nret == SOCKET_ERROR) { // Get a specific code // Handle accordingly return NETWORK_ERROR; } else { // nret contains the number of bytes sent }
Receiving is the same process, backwards:
char buffer[256]; // On the stack char *buffer = new char[256]; // or on the heap nret = recv(theSocket, buffer, 256, // Complete size of buffer 0); delete [] buffer; // Manipulate buffer, then delete if and only if // buffer was allocated on heap if (nret == SOCKET_ERROR) { // Get a specific code // Handle accordingly return NETWORK_ERROR; } else { // nret contains the number of bytes received }
What's interesting to note is that there's a button on the toolbar in Microsoft Outlook labeled "Send/Recv." Is "Receive" abbreviated to "Recv" simply to ensure the button looks right, or is it a programmer's habit from typing recv() so many times? Form your own conspiracy theories (again, good for smalltalk at parties).
This is where I ran into a little problem when writing my own Winsock programs. Just using recv() is great when you know exactly how much data you'll be receiving (such as in a game, where the first byte can be a command and the next byte be a parameter, etc.), but when you don't know, what do you do? If the data you're receiving is terminated by a newline character (a common problem with Java clients talking to C servers), you can write a readLine() function to capture everything up to that character. Here's what I used:
char * readLine() { vectortheVector; char buffer; int bytesReceived; while (true) { bytesReceived = recv(theSocket, &buffer, 1, 0); if (bytesReceived <= 0) return NULL; if (buffer == '\n') { char *pChar = new char[theVector.size() + 1]; memset(pChar, 0, theVector.size() + 1); for (int f = 0; f < theVector.size(); f++) pChar[f] = theVector[f]; return pChar; } else { theVector.push_back(buffer); } } }
A vector is utilized instead of an array because its storage space may be increased automatically to suit the length of the line. If recv() returns an error (indicated by bytesReceived being less than zero), NULL is returned. Since this is a possibility, calling functions should ensure that the string returned from readLine() is valid before use. Inside the loop, a single char is received from the socket and, if not a newline character, added to the vector. If it is a newline character, the contents of the vector are copied into a C string and returned. The string is declared to be one char larger than the vector and memset()'ted to zero so that the returned line will be automatically NULL-terminated. Ending strings with NULL prevents unusual errors and is generally good programming practice.
Nor presents this cleverly improved version with support for backspaces and the ability to change the newline character easily:
// Code originally written by Nor. Modified slightly to // support the MessageBox() API, make logic more readable, // align spacing, and add comments. Posted with permission. #define backKey '\b' // To disable backspaces, #define backKey NULL #define newLine '\n' #define endStr '\0' char *readLine(SOCKET s) { vectortheVector; char buffer; char *pChar; int bytesReceived; while (true) { bytesReceived = recv(s, &buffer, 1, 0); if (bytesReceived <= 0) { MessageBox(NULL, "recv() returned nothing.", "socketIndication", MB_OK); return NULL; } switch (buffer) { case backKey: // Handle backspace if (theVector.size() > 0) theVector.pop_back(); break; case endStr: // If end of string char reached, case newLine: // or if end of line char reached, pChar = new char[theVector.size() + 1]; memset(pChar, 0, theVector.size() + 1); for (int f = 0; f < theVector.size(); f++) pChar[f] = theVector[f]; return pChar; break; default: // Any regular char theVector.push_back(buffer); break; } } }
Non-blocking and Asynchronous Sockets
Up until this point we've been talking about blocking sockets, where calling a function such as accept() waits indefinitely for a user to connect. A non-blocking socket returns immediately whenever it is told to do something, either with a successful result, an error, or nothing (indicating that there will be something to receive later). The disadvantage of using this type is that you'll have to manually query the socket to see if a result has come in on every function you call. You can pass a set of sockets to the select() function to see which ones are ready for reading, writing, or have returned errors.
Functions using asynchronous sockets also return immediately, but you can specify a message to send to your window procedure when a specified event has occurred. For example, you can have the socket send an SOCKET_GOTMSG message whenever it receives something. Usually it's smart to check for errors (cumbersome, but necessary) when you get a socket message to prevent causing unnecessary problems later. First, let's define some functions we'll use to set up an asynchronous socket:
So, let's set up an asynchronous socket:
// We begin by creating a flag that Windows will use to contact us when something happens #define THERE_WAS_A_SOCKET_EVENT WM_USER + 100 // WM_USER is a base for custom messages
// Somewhere in our initialization code after CreateWindow (), we call WSAAsyncSelect () WSAAsyncSelect ( theSocket, hwnd, THERE_WAS_A_SOCKET_EVENT, FD_READ | FD_WRITE | FD_CONNECT | ... ); // This translates: Windows, please contact me using the THERE_WAS_A_SOCKET_EVENT flag that I // previously defined whenever there's data to read (FD_READ), or when I'm free to send data // (FD_WRITE), or when I've successfully connected to someone else (FD_CONNECT), or when...etc.
// In our window procedure (the function which handles all the messages that Windows sends to your app) LRESULT WINAPI TheWindowProcedure ( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) { switch ( msg ) { case THERE_WAS_A_SOCKET_EVENT: if ( WSAGETSELECTERROR ( lParam ) ) { // If an error occurred, closesocket ( theSocket ); WSACleanup (); // Shutdown Winsock return NETWORK_ERROR; } switch ( WSAGETSELECTEVENT ( lParam ) ) { // What happened, exactly? case FD_READ: // Receive data break; case FD_WRITE: // Write data break; case FD_CONNECT: // Just connected to server break; case ... // Same setup for other flags break; } break; // other case statements with logic that handles other Windows messages } }
Note that you cannot define one message for each event, like SOCKET_GOTMSG for FD_READ and then SOCKET_CONNECTED for FD_CONNECT. This is because repeated calls to WSAAsyncSelect () to setup each flag will cancel the effects of the last call to WSAAsyncSelect ().
I wrote this tutorial in December of 2000, and the seven or so years since then have seen a constant stream of visitors and improvements. I hope you've enjoyed reading as much as I've enjoyed writing: thank you for using Johnnie's Winsock Tutorial. The above is but a brief overview of the possibilities you can reach through Winsock, and others have done a far better job than me at probing the specifics of this subject:
I agree with Thomas Bleeker (MadWizard) that "network programming seems easier than it is." I cannot impress upon you the importance of practicing the use of these functions along with a debugger so you can see what's going on. You'll ultimately have a much better grasp of how things work if you get it wrong, investigate why you got it wrong and then experience the pleasure of getting it right. Making mistakes, in other words, is how we learn.