Lesson 3:
A Winsock TCP server
Introduction
Now that we’ve got a TCP client, we’ll create a server to which the
client can connect. It waits a client connect, waits for it to send some data,
and responds with a pre-defined message. There are two major differences
between the client code and the server code. The server has to bind itself to an interface on the computer. Then, instead of connecting to a
computer, it puts itself into a listening
mode and accepts connections. I‘ll
discuss all of these topics when we encounter them in the code. Note: This is a
single-threaded server, it can only handle one client at a time. A
multi-threaded server however, can handle multiple clients at a time.
The first part of our code is almost exactly the same as the client’s
code.
// author:
frenchwhale (http://frenchwhales_site.tripod.com) #define MESSAGE "Hello, Client!\n" #include <winsock.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, int
**argv) { WSADATA wsda; // Structure to store information returned from // WSAStartup char szRepMessage[80]; // Store the reply message int iMessageLen; int ret; // Return values char szInBuffer[128]; int iBufferLen; int iPort, iAddrLen;
SOCKET sListen, sClient; // Our TCP socket handle SOCKADDR_IN addr,
// The local interface
remote_addr; // The address of the
connecting host // Check arguments if(argc != 2 || (argc==2 &&
strcmp((char *) &argv[1][0], "/?")==0)) { printf("wsserver port\n"); printf(" port:
the listening port\n"); exit(1); } // Get the remote port iPort = atoi((char *) &argv[1][0]); if(iPort<0 || iPort>65563) // 0 < iPort < 65563 { printf("Invalid port number!
(%s)\n", argv[2]); exit(1); } strcpy(szRepMessage, MESSAGE); iMessageLen = strlen(szRepMessage); // Load version 1.1 of Winsock WSAStartup(MAKEWORD(1,1), &wsda); // Create a TCP socket printf("Creating socket..."); sListen = socket(AF_INET, SOCK_STREAM,
IPPROTO_IP); // Error? if(sListen == SOCKET_ERROR) { printf("Error\nCall to
socket(AF_INET, SOCK_STREAM, IPPROTO_IP); failed with:\n%d\n", WSAGetLastError()); exit(1); } printf("OK\n"); |
I don’t think you’ll have many difficulties understanding the code. First, it checks and initializes the command-line arguments, then it initializes Winsock and creates a socket. (If you are unfamiliar with any of these functions, see Lesson 2).
[Fig. 3-1, a multihomed computer]
Binding, listening and accepting
A server’s main purpose is to listen for client connections and process the client’s commands. In order to listen, you have to bind your listening socket to an interface. Often, a computer has only one interface that it uses to access the network, if a computer is connected to multiple networks (like a router), it is called a multihomed computer. If you want to listen for connections from one of the network, you have to bind it to its corresponding interface. When you bind your listening socket, you specify the IP-address of the interface. If you want to listen on all the interfaces, specify INADDR_ANY as the IP-address, remember that you have to specify the address it in network byte order so it is necessary to call htonl on INADDR_ANY. When a client connects to a server, he needs to know which port he should connect to just like the server needs to know which port it should be listening to. Both the address and the port are specified in a SOCKADDR_IN structure, in the same way as you specify the server’s address and port number when connecting with a client.
The bind function is declared as follows:
int bind(SOCKET s, struct sockaddr FAR *addr,
int addrlen);
Its declaration is pretty
self-explaining, s is the socket handle, addr is a pointer to the address it should be bound
to and finally the addrlen argument is the length of the sockaddr structure
supplied as the second argument.
Now that you have your listening socket bound, you have to put it into listening mode with the function listen:
int listen(SOCKET s, int backlog);
The s argument is the socket you want to put in listening mode, the second argument is the backlog. The backlog is the number of unanswered (unaccepted) connections you can have waiting to be answered.
Once you’ve put the socket into listening mode, you have to call the accept function in order to accept and take care of a client connection. If there aren’t any connections waiting, accept will block until someone connects. The function is declared like this:
SOCKET
accept(SOCKET s, struct sockaddr FAR *addr,
int FAR *addrlen);
Where s is the socket handle, addr
is a pointer to a sockaddr structure, in which the client’s IP-address will be stored. addrlen is a pointer to an int holding the number of bytes allocated for the sockaddr structure, upon return, the int will hold the number of bytes actually used to
store the sockaddr structure.
The sockaddr handle
that the function returns is a new socket handle used to communicate with this
particular client, the socket handle passed as an argument is called the
listening socket, you’ll only use it when calling bind, listen or accept. The new socket handle
however, is used to call send or recv
to communicate with the client.
When you have called accept, you use the returned socket handle to send or receive data to and from
the client and when you’re ready, you call accept to wait for new clients.
Now we’re ready to look at some code:
addr.sin_family = AF_INET; addr.sin_port = htons(iPort); addr.sin_addr.s_addr =
htonl(INADDR_ANY);
// Listen on any interface ret = bind(sListen, (struct
sockaddr *) &addr, sizeof(addr)); // Error? if(ret == SOCKET_ERROR) { printf("Error\nCall
to bind(sListen, (struct sockaddr *) &addr, sizeof(addr)); failed with:\n%d\n",
WSAGetLastError()); exit(1); } printf("OK\n"); printf("Putting socket into
listening mode..."); ret = listen(sListen, 10); //
Backlog 10 // Error?
if(ret == SOCKET_ERROR) { printf("Error\nCall
to listen(sListen, 10); failed with:\n%d\n", WSAGetLastError()); exit(1); } printf("OK\n"); printf("Waiting for connections
(Press Ctrl-C to exit)..."); iAddrLen = sizeof(remote_addr); sClient = accept(sListen, (struct
sockaddr *) &remote_addr, &iAddrLen); // Error? if(sClient == SOCKET_ERROR) { printf("Error\nCall
to accept(sListen, (struct sockaddr *) remote_addr, sizeof(remote_addr)); failed
with:\n%d\n", WSAGetLastError()); exit(1); } |
The only thing this code does is: binding the socket, checking for errors, putting it into listening mode, checking for errors, calling accept to wait for a connection and checking for errors. Notice that the only way to close the server is to press Ctrl-C, this is because when you call accept, the program’s sole task is to wait for client connections, during this time it doesn’t receive any user input. This problem can be corrected using multi-threaded servers with one controlling thread which handles user input, one listening thread which calls accept in a while loop and finally one thread per client connected. A multi-threaded server also allows multiple clients to connect and exchange information simultaneously.
Exchange of data
The next
part is by far the easiest part in this lesson, it displays the IP-address of
the client, receives data from the client and responds with its own
pre-specified message.
printf("%s connected\n",
inet_ntoa(remote_addr.sin_addr)); // Receive data printf("Waiting for a
response..."); ret = recv(sClient, szInBuffer,
sizeof(szInBuffer), 0); if(ret == SOCKET_ERROR) { printf("Error\nCall
to recv(sClient, szInBuffer, sizeof(szInBuffer), 0); failed with:\n%d\n",
WSAGetLastError()); exit(1); } printf("Response recieved\n"); iBufferLen = ret; // recv() returns the
number of bytes read szInBuffer[iBufferLen] = '\0'; // convert to cstring printf("Data recieved:\n%s\n",
szInBuffer); // Ready to send data printf("Sending reply..."); ret = send(sClient, szRepMessage,
iMessageLen, 0); if(ret == SOCKET_ERROR) { printf("Error\nCall
to send(sClient, szRepMessage, iMessageLen, 0) failed with:\n%d\n",
WSAGetLastError()); exit(1); } printf("OK\n"); printf("Closing client socket..."); closesocket(sClient); printf("OK\n"); return 0; } |
First, the code converts the client’s IP-address from a 32-bit INADDR into a string in the form aaa.bbb.ccc.ddd and prints it. After that, the program waits for the client to send dome data, note that when you call recv, you have to pass the socket handle returned from accept (sClient) in order for it to work. When the data is received, the server adds a ‘\0’ and prints it and then it sends its own pre-specified message. The client socket is then closed and the program returns.
Client/Server interaction
Now
that we have both a client and a server, we can start the server and let the
client connect to it. This can be done with only one host using the special
IP-address 127.0.0.1 or the hostname
localhost, these addresses and
hostnames are called loopback
addresses, everything you send to them is looped back to you, so if you set up
a server on your computer, you can connect to it using 127.0.0.1 as the
server’s address.
The telnet program
Windows
includes a simple client application called telnet, although it’s originally
destined to be used with the protocol called telnet (port 23), a protocol used to
access other computers on a network, but this application can be used to
connect to any other port. You can use this program to figure out how different
protocols work or to test your own servers. To use it, choose Run from the
Start menu and write:
telnet
[server] [port]
Where server is the server’s IP-address and port is the port you wish to connect to, when you start the program, it will connect to the server at the specified port. If you leave the parameters out, you will have to specify the server and port in the program when you wish to connect.
Source codes for Lesson 3:
n WSSERVER.C – Listens for connections on the port specified as the argument