It is a fortunate thing that the server and clients share so many properties, it means we'll be able to share a lot of code between them and simplify our lives. To this end cComm will be the base class for both our client and server communications. Let's start by taking a look at what the client and server have to do in order to connect to the internet via TCP/IP. This model is known as a connection-oriented.
Client
Initialize Winsock 2.0.
Obtain an IP and port of a game on the internet. An IP could be an address (www.flipcode.com) or a number (24.112.84.101). A port is any number between 1 and 65535, inclusive.
Create a socket.
Resolve the address using gethostbyname() or gethostbyaddr()
Try to connect() to the server.
send() and recv() data on the open socket
closesocket() and shutdown()
Cleanup Winsock |
Server
Initialize Winsock 2.0.
Obtain the port number the listen socket should be opened on. This is the socket clients will connect to.
Obtain the IP name and number of the machine for the sysadmin's benefit.
Create a socket.
bind() the socket.
listen() on the socket for client connections.
accept() connections on the socket unless there's some reason not to (such as banned IP, hammering or max clients)
send() and recv() data on the client socket.
closesocket() for the client
closesocket() the listen socket
shutdown()
Cleanup Winsock
|
A surprising number of similarities, wouldn't you say? Each needs code to...
...resolve an internet address into something Winsock 2.0 can use.
...create or destroy a given socket.
...send or receive data on a given socket.
Note that a listen socket isn't quite the same as a connection socket. More on that later.
Initializing and cleaning up Winsock is easy as pie. Just ask it what version you'd like (2.0, in our case) and then check to see that there weren't any major errors. The only thing to really pay attention to is that every call to WSAStartup is matched with a call to WSACleanup because the winsock system relies on wsock32.dll. If you've never worked width DLLs, here's a ten second explanation: DLLs contain reusable system code that only take up memory once in the machine. Every time you use a DLL you increment an internal counter. When you finish you decrement the counter and when that counter reaches zero the DLL is unloaded from memory. Simple, effective and very dangerous to available memory if you don't get it right.
int cComm::StartWinsock() {
// WSADATA d_winsockData;
// unsigned short d_winsockVersion;
// #define WINSOCK_MAJOR_VERSION 2
// #define WINSOCK_MINOR_VERSION 0
char buffer[ 128 ];
int error;
d_winsockVersion = MAKEWORD( WINSOCK_MAJOR_VERSION, WINSOCK_MINOR_VERSION );
error = WSAStartup( d_winsockVersion, &d_winsockData );
if( error == SOCKET_ERROR ) {
if( error == WSAVERNOTSUPPORTED ) {
sprintf( buffer,
"WSAStartup error.\nRequested Winsock v%d.%d, found v%d.%d.",
WINSOCK_MAJOR_VERSION, WINSOCK_MINOR_VERSION,
LOBYTE( d_winsockData.wVersion ), HIBYTE( d_winsockData.wVersion ) );
WSACleanup();
} else sprintf( buffer, "WSAStartup error (%d)", WSAGetLastError() );
// It's not a good idea to try MessageBox
// if you have a DirectX window open...
MessageBox( NULL, buffer, APPLICATION_NAME, MB_OK );
return 1;
}
return 0; // no problems!
}
int cComm::EndWinsock() {
Disconnect(); // cComm method to cleanup any remaining connections
WSACleanup();
return 0;
}
|
The next job is to resolve the IP address of the local machine, in nnn.nnn.nnn.nnn style. This is a little more complicated because winsock 2.0 calls the network subsystem and then returns control to the application. A short while later the window you have running will receive a message indicating that the address has been resolved.
/* cComm::GetHost will return immediately and the window pointed to by
* d_hwnd will get a SM_GETHOST message.
*/
HANDLE cComm::GetHost( char *pAddr, int port ) {
// SOCKADDR_IN d_address;
// HANDLE d_hostHandle;
// char d_hostentbuf[ MAXGETHOSTSTRUCT ];
// HWND d_hwnd;
d_address.sin_addr.s_addr = inet_addr( pAddr );
d_address.sin_port = htons( port );
d_address.sin_family = AF_INET;
if( d_address.sin_addr.s_addr == INADDR_NONE ) {
d_hostHandle = WSAAsyncGetHostByName( d_hwnd, SM_GETHOST,
pAddr,
d_hostentbuf, MAXGETHOSTSTRUCT );
} else {
d_hostHandle = WSAAsyncGetHostByAddr( d_hwnd, SM_GETHOST,
(const char *)&d_address.sin_addr.s_addr, sizeof( IN_ADDR ), AF_INET,
d_hostentbuf, MAXGETHOSTSTRUCT );
}
return d_hostHandle;
}
// When the window receives SM_GETHOST I call cComm::OnGetHost()
int cComm::OnGetHost( WPARAM wparam, LPARAM lparam ) {
// char d_IP[ 64 ];
if( (HANDLE)wparam != d_hostHandle ) return 1;
if( WSAGETASYNCERROR( lparam ) ) { // upper 16 bits of (DWORD?)
Disconnect();
return 2;
}
// Winsock 2.0 says use h_addr_list[ 0 ] for
// inet_ntoa() but it didn't work for me...
LPHOSTENT lphostent;
lphostent = (LPHOSTENT)d_hostentbuf;
strcpy( d_IP, inet_ntoa( *(LPIN_ADDR)lphostent->h_addr ) );
// Here is where we'd open a socket, etc...
return 0;
}
|
Once a valid IP has been obtained our next job is to create or destroy a given socket. Again, this is a fairly simple task. The only problem is that when you want to destroy the socket you have to make sure there's no data left to be read on the socket. Though I've never tested, I bet you'd get a memory leak if you didn't read it in. Note that the following code is only version one, we'll be adding more later.
SOCKET cComm::CreateSocket() {
// SOCKET d_socket;
if( d_socket ) return d_socket; // already exists
// Create a streaming socket.
d_socket = socket( AF_INET, SOCK_STREAM, 0 );
return d_socket;
}
int cComm::Disconnect() {
// SOCKET d_socket;
// shutdown() warns the other machine no more data
// Will be sent on this socket.
shutdown( d_socket, SD_SEND );
// Read in the remaining data pending on this socket
// Shut down the socket.
closesocket( d_socket );
return 0;
}
|
So let's review: We can now initialize Winsock, find the local IP and create a socket. Then when the application ends we destroy the socket and cleanup Winsock. Next week we'll take that open socket and connect to a remote machine.
|