程式扎記: [ Network Programming in MS ] Introduction to WinSock (Part2 : Connection-Oriented Communication)

標籤

2011年4月11日 星期一

[ Network Programming in MS ] Introduction to WinSock (Part2 : Connection-Oriented Communication)

Preface : 
In this section, we'll cover the Winsock functions necessary for both receiving connections and establishing connections. We'll first discuss how to develop a server by listening for client connections and explore the process for accepting or rejecting a connection. Then we'll describe how to develop a client by initiating a connection to a server. Finally, we'll discuss how data is transferred in a connection-oriented session. 
In IP, connection-oriented communication is accomplished through the TCP/IP protocol. TCP provides reliable error-free data transmission between two computers. When applications communicate using TCP, a virtual connection is established between the source computer and the destination computer. Once a connection is established, data can be exchanged between the computers as a two-way stream of bytes. 

Server API Functions : 
A server is a process that waits for any number of client connections with the purpose of servicing their requests. A server must listen for connections on a well-known name. In TCP/IP, this name is the IP address of the local interface and a port number. The first step in Winsock is to create a socket with either the socket or WSASocket call and bind the socket of the given protocol to its well-known name, which is accomplished with the bind API call. The next step is to put the socket into listening mode, which is performed (appropriately enough) with the listen API function. Finally, when a client attempts a connection, the server must accept the connection with either the accept or WSAAccept call. In the next few sections, we will discuss each API call that is required for binding, listening, and accepting a client connection. Figure 1-1 illustrates the basic calls a server and a client must perform in order to establish a communication channel : 
 

Binding : 
Once the socket of a particular protocol is created, you must bind it to a well-known address. The bind function associates the given socket with a well-known address. This function is declared as : 

  1. int bind(  
  2.     SOCKET                     s,   
  3.     const struct sockaddr FAR* name,   
  4.     int                        namelen  
  5. );  
The first parameter, s, is the socket on which you want to wait for client connections. The second parameter is of type struct sockaddr, which is simply a generic buffer. You must actually fill out an address buffer specific to the protocol you are using and cast that as a struct sockaddr when calling bind. The Winsock header file defines the type SOCKADDR as struct sockaddr. We'll use this type throughout the chapter for brevity. The third parameter is simply the size of the protocol-specific address structure being passed. For example, the following code illustrates how this is done on a TCP connection : 
  1. SOCKET               s;      
  2. SOCKADDR_IN          tcpaddr;  
  3. int                  port = 5150;  
  4.   
  5. s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
  6.   
  7. tcpaddr.sin_family = AF_INET;  
  8. tcpaddr.sin_port = htons(port);      
  9. tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  10.   
  11. bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));  
From the example, you'll see a stream socket being created, followed by setting up the TCP/IP address structure on which client connections will be accepted. In this case, the socket is being bound to the default IP interface by using a special address, INADDR_ANY, and occupies port number 5150. We could have specified an explicit IP address available on the system, but INADDR_ANY allows us to bind to all available interfaces on the system so that any incoming client connection on any interface (but the correct port) will be accepted by our listening socket. The call to bind formally establishes this association of the socket with the local IP interface and port. 
On error, bind returns SOCKET_ERROR. The most common error encountered with bind is WSAEADDRINUSE. With TCP/IP, the WSAEADDRINUSE error indicates that another process is already bound to the local IP interface and port number or that the IP interface and port number are in the TIME_WAIT state. If you call bind again on a socket that is already bound, WSAEFAULT will be returned. 

Listening : 
The next piece of the equation is to put the socket into listening mode. The bind function merely associates the socket with a given address. The API function that tells a socket to wait for incoming connections is listen, which is defined as : 
  1. int listen(  
  2.     SOCKET s,   
  3.     int    backlog  
  4. );  
Again, the first parameter is a bound socket. The backlog parameter specifies the maximum queue length for pending connections. This is important when several simultaneous requests are made to the server. For example, let's say the backlog parameter is set to two. If three client requests are made at the same time, the first two will be placed in a "pending" queue so that the application can service their requests. The third connection request will fail withWSAECONNREFUSED. Note that once the server accepts a connection, the request is removed from the queue so that others can make a request. The backlog parameter is silently limited to a value that the underlying protocol provider determines. Illegal values are replaced with their nearest legal values. In addition, there is no standard provision for finding the actual backlog value. 
The errors associated with listen are straightforward and the most common is WSAEINVAL, which usually indicates that you forgot to call bind before listen. 

Accepting Connections : 
Now you're ready to accept client connections. This is accomplished with the accept, WSAAccept, or AcceptEx function. The prototype for accept is : 
  1. SOCKET accept(  
  2.     SOCKET s,   
  3.     struct sockaddr FAR* addr,   
  4.     int FAR* addrlen  
  5. );  
Parameter s is the bound socket that is in a listening state. The second parameter should be the address of a valid SOCKADDR_IN structure, while addrlenshould be a reference to the length of the SOCKADDR_IN structure. A call to accept services the first connection request in the queue of pending connections. When the accept function returns, the addr structure contains the IPv4 address information of the client making the connection request, and the addrlenparameter indicates the size of the structure. In addition, accept returns a new socket descriptor that corresponds to the accepted client connection. For all subsequent operations with this client, the new socket should be used. The original listening socket is still open to accept other client connections and is still in listening mode. 
If an error occurs, INVALID_SOCKET is returned. The most common error encountered is WSAEWOULDBLOCK if the listening socket is in asynchronous or non-blocking mode and there is no connection to be accepted. Block, non-blocking, and other socket modes are covered in Chapter 5. Winsock 2 introduced the function WSAAccept, which has the capability to conditionally accept a connection based on the return value of a condition function. Chapter 10 will describe WSAAccept in greater detail. 
At this point, we have described all the necessary elements to construct a simple Winsock TCP/IP server application. The following program fragment demonstrates how to write a simple server that can accept one TCP/IP connection. We did not perform any error checking on the calls to make reading the code less confusing : 
  1. void winsock_accept()  
  2. {  
  3.     WSADATA              wsaData;  
  4.     SOCKET               ListeningSocket;  
  5.     SOCKET               NewConnection;  
  6.     SOCKADDR_IN          ServerAddr;  
  7.     SOCKADDR_IN          ClientAddr;  
  8.     int                  Port = 5150;   
  9.     int                 ClientAddrLen = sizeof(ClientAddr);  
  10.   
  11.     // Initialize Winsock version 2.2  
  12.   
  13.     if(WSAStartup(MAKEWORD(2,2), &wsaData)<0)  
  14.     {  
  15.         printf("Call WSAStartup() failed!\n");  
  16.         return ;  
  17.     }  
  18.   
  19.     // Create a new socket to listen for client connections.  
  20.     ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
  21.   
  22.     // Set up a SOCKADDR_IN structure that will tell bind that we  
  23.     // want to listen for connections on all interfaces using port  
  24.     // 5150. Notice how we convert the Port variable from host byte  
  25.     // order to network byte order.  
  26.     ServerAddr.sin_family = AF_INET;  
  27.     ServerAddr.sin_port = htons(Port);      
  28.     ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  29.   
  30.     // Associate the address information with the socket using bind.  
  31.     if(bind(ListeningSocket, (SOCKADDR *)&ServerAddr,   
  32.         sizeof(ServerAddr))<0)  
  33.     {  
  34.         printf("Call bind() failed!\n");  
  35.         goto CLEAN_UP;  
  36.     }  
  37.   
  38.     // Listen for client connections. We used a backlog of 5, which  
  39.     // is normal for many applications.  
  40.     if(listen(ListeningSocket, 5))  
  41.     {  
  42.         printf("Call listen() failed!\n");  
  43.         goto CLEAN_UP;  
  44.     }  
  45.   
  46.     // Accept a new connection when one arrives.  
  47.     NewConnection = accept(ListeningSocket, (SOCKADDR *)   
  48.         &ClientAddr, &ClientAddrLen);  
  49.     if(NewConnection==NULL)   
  50.     {  
  51.         printf("Call accept() failed!\n");  
  52.         goto CLEAN_UP;  
  53.     }  
  54.     // At this point you can do two things with these sockets. Wait  
  55.     // for more connections by calling accept again on ListeningSocket  
  56.     // and start sending or receiving data on NewConnection. We will  
  57.     // describe how to send and receive data later in the chapter.  
  58.   
  59.     // When you are finished sending and receiving data on the  
  60.     // NewConnection socket and are finished accepting new connections  
  61.     // on ListeningSocket, you should close the sockets using the  
  62.     // closesocket API. We will describe socket closure later in the   
  63.     // chapter.  
  64.     closesocket(NewConnection);  
  65.     closesocket(ListeningSocket);  
  66.     printf("winsock_accept() return success!\n");  
  67.     // When your application is finished handling the connections,   
  68.     // call WSACleanup.  
  69. CLEAN_UP:  
  70.     WSACleanup();  
  71. }  
Now that you understand how to construct a server that can receive a client connection, we will describe how to construct a client. 


Client API Functions : 
The client is much simpler and involves fewer steps to set up a successful connection. There are only three steps for a client : 
1. Create a socket.
2. Set up a SOCKADDR address structure with the name of server you are going to connect to (dependent on underlying protocol). For TCP/IP, this is the server's IP address and port number its application is listening on.
3. Initiate the connection with connect or WSAConnect.

You already know how to create the socket and construct a SOCKADDR structure, so the only remaining step is establishing a connection. If you are using TCP/IP, you may need to know about TCP States. For more information regarding the TCP/IP protocol, consult RFC 793. This RFC and others can be found athttp://www.rfc-editor.org. Below is the TCP closure states : 
 

- connect 
Connecting a socket is accomplished by calling connect, WSAConnect, or ConnectEx. We'll look at the Winsock 1 version, which is defined as : 
  1. int connect(  
  2.     SOCKET s,  
  3.     const struct sockaddr FAR* name,  
  4.     int namelen  
  5. );  
The parameters are fairly self-explanatory: s is the valid TCP socket on which to establish the connection, name is the socket address structure (SOCKADDR_IN) for TCP that describes the server to connect to, and namelen is the length of the name variable. 
If the computer you're attempting to connect to does not have a process listening on the given port, the connect call fails with the WSAECONNREFUSEDerror. The other error you might encounter is WSAETIMEDOUT, which occurs if the destination you're trying to reach is unavailable (either because of a communication-hardware failure on the route to the host or because the host is not currently on the network). The following program fragment demonstrates how to write a simple client that can connect to the server application described earlier : 
  1. void winsock_connect()  
  2. {  
  3.     WSADATA             wsaData;  
  4.     SOCKET                  s;  
  5.     SOCKADDR_IN     ServerAddr;  
  6.     int                             Port = 5150;  
  7.   
  8.     // Initialize Winsock version 2.2  
  9.     if(WSAStartup(MAKEWORD(2,2), &wsaData)<0)  
  10.     {  
  11.         printf("Call WSAStartup() failed!\n");  
  12.         goto CLEAN_UP;  
  13.     }  
  14.   
  15.     // Create a new socket to make a client connection.  
  16.     s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
  17.   
  18.     // Set up a SOCKADDR_IN structure that will be used to connect  
  19.     // to a listening server on port 5150. For demonstration  
  20.     // purposes, let's assume our server's IP address is 136.149.3.29.  
  21.     // Obviously, you will want to prompt the user for an IP address  
  22.     // and fill in this field with the user's data.  
  23.     ServerAddr.sin_family = AF_INET;  
  24.     ServerAddr.sin_port = htons(Port);      
  25.     ServerAddr.sin_addr.s_addr = inet_addr("10.6.7.190");  
  26.   
  27.     // Make a connection to the server with socket s.  
  28.     if(connect(s, (SOCKADDR *) &ServerAddr, sizeof(ServerAddr)))  
  29.     {  
  30.         printf("Call connect() failed\n");  
  31.         int iErr;  
  32.         switch((iErr=WSAGetLastError()))  
  33.         {  
  34.         case WSAECONNREFUSED:  
  35.             {  
  36.                 printf("\tError > No connection could be made because the target machine actively refused it.\n");  
  37.                 break;  
  38.             }  
  39.         case WSAETIMEDOUT:  
  40.             {  
  41.                 printf("\tError > A connection attempt failed because the connected party did not properly respond "  
  42.                             "after a period of time, or established connection failed because connected host has failed to respond.\n");  
  43.                 break;  
  44.             }  
  45.         case WSAEADDRNOTAVAIL:  
  46.             {  
  47.                 printf("\tError > The requested address is not valid in its context.\n");  
  48.                 break;  
  49.             }  
  50.         default:  
  51.             {  
  52.                 printf("\tError > Unknown(%d)\n", iErr);  
  53.             }  
  54.         }  
  55.         goto CLEAN_UP;  
  56.     }  
  57.   
  58.     // At this point you can start sending or receiving data on  
  59.     // the socket s. We will describe sending and receiving data  
  60.     // later in the chapter.  
  61.   
  62.     // When you are finished sending and receiving data on socket s,  
  63.     // you should close the socket using the closesocket API. We will  
  64.     // describe socket closure later in the chapter.  
  65.     closesocket(s);  
  66.   
  67.     // When your application is finished handling the connection, call  
  68.     // WSACleanup.  
  69. CLEAN_UP:  
  70.     WSACleanup();  
  71. }  
Now that you can set up communication for a connection-oriented server and client, you are ready to begin handling data transmission. 

Data Transmission : 
Sending and receiving data is what network programming is all about. For sending data on a connected socket, there are two API functions: send andWSASend. The second function is specific to Winsock 2. Likewise, two functions are for receiving data on a connected socket: recv and WSARecv. An important thing to keep in mind is that all buffers associated with sending and receiving data are of the simple char type which is just simple byte-oriented data. In reality, it can be a buffer with any raw data in it—whether it's binary or string data doesn't matter. 
In addition, the error code returned by all send and receive functions is SOCKET_ERROR. The two most common errors encountered areWSAECONNABORTED and WSAECONNRESET. Both of these deal with the connection being closed—either through a timeout or through the peer closing the connection. Another common error is WSAEWOULDBLOCK, which is normally encountered when either nonblocking or asynchronous sockets are used. This error basically means that the specified function cannot be completed at this time. In Chapter 5, we will describe various Winsock I/O methods that can help you avoid some of these errors. 

- send 
To send data on a connected socket is send, which is prototyped as : 
  1. int send(  
  2.     SOCKET s,   
  3.     const char FAR * buf,   
  4.     int len,   
  5.     int flags  
  6. );  
The parameter s is the connected socket to send the data on. The second parameter, buf, is a pointer to the character buffer that contains the data to be sent. The third parameter, len, specifies the number of characters in the buffer to send. Finally, the flags parameter can be either 0, MSG_DONTROUTE, orMSG_OOB. Alternatively, the flags parameter can be a bitwise OR any of those flags. 
On a good return, send returns the number of bytes sent; otherwise, if an error occurs, SOCKET_ERROR will be returned. A common error is WSAECO-NNABORTED, which occurs when the virtual circuit terminates because of a timeout failure or a protocol error. When this occurs, the socket should be closed, as it is no longer usable. The error WSAECONNRESET occurs when the application on the remote host resets the virtual circuit by executing a hard close or terminating unexpectedly, or when the remote host is rebooted. Again, the socket should be closed after this error occurs. The last common error isWSAETIMEDOUT, which occurs when the connection is dropped because of a network failure or the remote connected system going down without notice. 

- recv 
The recv function is the most basic way to accept incoming data on a connected socket. This function is defined as : 
  1. int recv(  
  2.     SOCKET s,   
  3.     char FAR* buf,   
  4.     int len,   
  5.     int flags  
  6. );  
The first parameter, s, is the socket on which data will be received. The second parameter, buf, is the character buffer that will receive the data, and len is either the number of bytes you want to receive or the size of the buffer, buf. Finally, the flags parameter can be one of the following values: 0, MSG_PEEK, or MSG_OOB. In addition, you can bitwise OR any one of these flags together. Of course, 0 specifies no special actions. MSG_PEEK causes the data that is available to be copied into the supplied receive buffer, but this data is not removed from the system's buffer. The number of bytes pending is also returned. 
Message peeking is bad. Not only does it degrade performance, as you now need to make two system calls (one to peek and one without the MSG_PEEK flag to actually remove the data), but it is also unreliable under certain circumstances. The data returned might not reflect the entire amount available. Also, by leaving data in the system buffers, the system has less space to contain incoming data. As a result, the system reduces the TCP window size for all senders. This prevents your application from achieving the maximum possible throughput. The best thing to do is to copy all the data you can into your own buffer and manipulate it there. 
There are some considerations when using recv on a message- or datagram-based socket such as UDP, which we will describe later. If the data pending is larger than the supplied buffer, the buffer is filled with as much data as it will contain. In this event, the recv call generates the error WSAEMSGSIZE. Note that the message-size error occurs with message-oriented protocols. Stream protocols such as TCP buffer incoming data and will return as much data as the application requests, even if the amount of pending data is greater. Thus, for streaming protocols you will not encounter the WSAEMSGSIZE error. 

Stream Protocols : 
Because most connection-oriented communication, such as TCP, is streaming protocols, we'll briefly describe them here. A streaming protocol is one that the sender and receiver may break up or coalesce data into smaller or larger groups. The main thing to be aware of with any function that sends or receives data on a stream socket is that you are not guaranteed to read or write the amount of data you request. Let's say you have a character buffer with 2048 bytes of data you want to send with the send function. The code to send this is : 
  1. char sendbuff[2048];  
  2. int  nBytes = 2048;  
  3.   
  4. // Fill sendbuff with 2048 bytes of data  
  5.   
  6. // Assume s is a valid, connected stream socket  
  7. ret = send(s, sendbuff, nBytes, 0);  
It is possible for send to return having sent less than 2048 bytes. The ret variable will be set to the number of bytes sent because the system allocates a certain amount of buffer space for each socket to send and receive data. In the case of sending data, the internal buffers hold data to be sent until such time as the data can be placed on the wire. Several common situations can cause this. For example, simply transmitting a huge amount of data will cause these buffers to become filled quickly. Also, for TCP/IP, there is what is known as the window size. The receiving end will adjust this window size to indicate how much data it can receive. If the receiver is being flooded with data, it might set the window size to 0 to catch up with the pending data. This will force the sender to stop until it receives a new window size greater than 0. In the case of our send call, there might be buffer space to hold only 1024 bytes, in which case you would have to resubmit the remaining 1024 bytes. The following code ensures that all your bytes are sent : 
  1. char sendbuff[2048];  
  2. int  nBytes = 2048,  
  3.      nLeft,  
  4.      idx;  
  5.   
  6. // Fill sendbuff with 2048 bytes of data  
  7.   
  8. // Assume s is a valid, connected stream socket  
  9. nLeft = nBytes;  
  10. idx = 0;  
  11.   
  12. while (nLeft > 0)  
  13. {  
  14.     ret = send(s, &sendbuff[idx], nLeft, 0);  
  15.     if (ret == SOCKET_ERROR)  
  16.     {  
  17.         // Error  
  18.     }  
  19.     nLeft -= ret;  
  20.     idx += ret;  
  21. }  
The same principle holds true for receiving data on a stream socket but is less significant. Because stream sockets are a continuous stream of data, when an application reads, it isn't generally concerned with how much data it should read. If your application requires discrete messages over a stream protocol, you might have to do a little work. If all the messages are the same size and the code for reading, say, 512-byte messages would look like this : 
  1. char    recvbuff[1024];  
  2. int     ret,  
  3.         nLeft,  
  4.         idx;  
  5.   
  6. nLeft = 512;  
  7. idx = 0;  
  8.   
  9. while (nLeft > 0)  
  10. {  
  11.     ret = recv(s, &recvbuff[idx], nLeft, 0);  
  12.     if (ret == SOCKET_ERROR)  
  13.     {  
  14.         // Error  
  15.     }  
  16.     idx += ret;  
  17.     nLeft -= ret;  
  18. }  
Things get a little complicated if your message sizes vary. It is necessary to impose your own protocol to let the receiver know how big the forthcoming message will be. For example, the first four bytes written to the receiver will always be the integer size in bytes of the forthcoming message. The receiver will start every read by looking at the first four bytes, converting them to an integer, and determining how many additional bytes that message comprises. 

Breaking the Connection : 
Once you are finished with a socket connection, you must close it and release any resources associated with that socket handle. To actually release the resources associated with an open socket handle, use the closesocket call. Be aware, however, that closesocket can have some adverse effects—depending on how it is called—that can lead to data loss. For this reason, a connection should be gracefully terminated with the shutdown function before a call to the closesocket function. 
- shutdown 
To ensure that all data an application sends is received by the peer, a well-written application should notify the receiver that no more data is to be sent. Likewise, the peer should do the same. This is known as a graceful close and is performed by the shutdown function, defined as : 
  1. int shutdown(  
  2.     SOCKET s,   
  3.     int how  
  4. );  
The how parameter can be SD_RECEIVE, SD_SEND, or SD_BOTH. For SD_RECEIVE, subsequent calls to any receive function on the socket are disallowed. This has no effect on the lower protocol layers. And for TCP sockets, if data is queued for receive or if data subsequently arrives, the connection is reset. However, on UDP sockets incoming data is still accepted and queued (because shutdown has no meaning for connectionless protocols). For SD_SEND, subsequent calls to any send function are disallowed. For TCP sockets, this causes a FIN packet to be generated after all data is sent and acknowledged by the receiver. Finally, specifying SD_BOTH disables both sends and receives. 
Note that not all connection-oriented protocols support graceful closure, which is what the shutdown API performs. For these protocols (such as ATM), onlyclosesocket needs to be called to terminate the session. 

- closesocket 
The closesocket function closes a socket and is defined as : 
  1. int closesocket (SOCKET s);  
Calling closesocket releases the socket descriptor and any further calls using the socket fail with WSAENOTSOCK. If there are no other references to this socket, all resources associated with the descriptor are released. This includes discarding any queued data. 
Pending synchronous calls issued by any thread in this process are canceled without posting any notification messages. Pending overlapped operations are also canceled. Any event, completion routine, or completion port that is associated with the overlapped operation is performed but will fail with the errorWSA_OPERATION_ABORTED. Socket I/O models are discussed in greater depth in Chapter 5.

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!