Section 17.6. Using UDP Datagrams

   


17.6. Using UDP Datagrams

While most network applications benefit from the streaming TCP protocol, some prefer to use UDP. Here are some reasons that the connectionless datagram model provided by UDP might be useful:

  • Connectionless protocols handle machine restarts more gracefully as there are no connections that need to be reestablished. This can be very attractive for network file systems (like NFS, which is UDP based), since it allows the file server to be restarted without the clients having to be aware of it.

  • Protocols that are very simple may run much faster over a datagram protocol. The Domain Name System (DNS) uses UDP for just this reason (although it optionally supports TCP as well). When a TCP connection is being established, the client machine needs to send a message to the server to establish the connection, get an acknowledgment back from the server saying the connection is available, and then tell the server when the client side has been established.[25] Once this occurs, the client can send its hostname query to the server, which responds. All of this took five messages, ignoring the error checking and sequencing for the actual query and response. By using UDP, hostname queries are sent as the first packet to the server, which then responds with one more UDP packet, reducing the total packet count to five. If the client does not get a response, it simply resends the query.

    [25] This is known as TCP's three-way handshake, which is actually more complicated than described here.

  • When computers are first initialized, they often need to establish an IP address and then download the first part of the operating system over the network.[26] Using UDP for these operations makes the protocol stack that is embedded on such machines much simpler than it would be if a full TCP implementation were required.

    [26] This process is called network booting.

17.6.1. Creating a UDP Socket

Like any other socket, UDP sockets are created through socket(), but the second argument should be SOCK_DGRAM, and the final argument is either IPPROTO_UDP or just zero (as UDP is the default IP datagram protocol).

Once the socket is created, it needs to have a local port number assigned to it. This occurs when a program does one of three things:

  • A port number is explicitly set by calling bind(). Servers that need to receive datagrams on a well-known port number need to do this, and the system call is exactly the same as for TCP servers.

  • A datagram is sent through the socket. The kernel assigns a UDP port number to that socket the first time data is sent through it. Most client programs use this technique, as they do not care what port number is used.

  • A remote address is set for the socket by connect() (which is optional for UDP sockets).

There are also two different ways of assigning the remote port number. Recall that TCP sockets have the remote address assigned by connect(), which can also be used for UDP sockets.[27] While TCP's connect() causes packets to be exchanged to initialize the connection (making connect() a slow system call), calling connect() for UDP sockets simply assigns a remote IP address and port number for outgoing datagrams, and is a fast system call. Another difference is applications can connect() a TCP socket only once; UDP sockets can have their destination address changed by calling connect() again.[28]

[27] UDP sockets that have permanant destinations assigned by connect() are sometimes called connected UDP sockets.

[28] It is also possible to use connect() to make a connected socket an unconnected one, but doing so is not well standardized. See [Stevens, 2004] for information on how to do this if it is needed.

One advantage of using connected UDP sockets is that only the machine and port that are specified as the remote address for the socket can send datagrams to that socket. Any IP address and port can send datagrams to an unconnected UDP socket, which is required in some cases (this is how new clients initially contact servers), but forces programs to keep track of where datagrams are being sent from.

17.6.2. Sending and Receiving Datagrams

Four system calls are normally used for sending and receiving UDP packets,[29] send(), sendto(), recv(), and recvfrom().[30]

[29] These functions can be used to send data across any socket, and occasionally there are reasons to use them for TCP connections.

[30] The sendmsg() and recvmsg() system calls can also be used, but there is rarely a reason to do so.

 #include <sys/types.h> #include <sys/sockets.h> int send(int s, const void * data, size_t len, int flags); int sendto(int s, const void * data, size_t len, int flags,            const struct sockaddr * to, socklen_t toLen); int recv(int s, void * data, size_t maxlen, int flags); int recvfrom(int s, void * data, size_t maxlen, int flags,              struct sockaddr * from, socklen_t * fromLen); 


For our purposes, the flags for all of these are always zero. There are many values it can take, and [Stevens, 2004] discusses them in detail.

The first of the system calls, send(), can be used only for sockets whose destination IP address and port have been specified by calling connect(). It sends the first len bytes pointed to by data to the other end of the socket s. The data is sent as a single datagram. If len is too long to the data to be passed in a single datagram, EMSGSIZE is returned in errno.

The next system call, sendto() works like send() but allows the destination IP address and port number to be specified for sockets that have not been connected. The last two parameters are a pointer to the socket address and the length of the socket address. Using this function does not set a destination address for the socket; it remains unconnected and future sendto() calls can send datagrams to different destinations. If the to argument is NULL, sendto() behaves exactly like send().

The recv() and recvfrom() system calls are analogous to send() and sendto(), but they receive datagrams instead of sending them. Both write a single datagram to data, up to *maxlen bytes, and discard any part of the datagram that does not fit in the buffer. The remote address that sent the datagram is stored in the from parameter of recvmsg() as long as it is no more than fromLen bytes long.

17.6.3. A Simple tftp Server

This simple tftp server illustrates how to send and receive UDP datagrams for both connected and unconnected sockets. The tftp protocol is a very simple file transfer protocol that is built on UDP.[31] It is often used by a computer's firmware to download an initial boot image during a network boot. The server we implement has a number of limitations that make it ill-suited for any real work:

[31] Full descriptions of tftp can be found in both [Stevens, 2004] and [Stevens, 1994].

  • Only a single client can talk to the server at a time (although this is easy to fix).

  • Files can only be sent by the server; it cannot receive them.

  • There are no provisions for restricting what files the server sends to an anonymous remote user.

  • Very little error checking is done, making it quite likely that there are exploitable security problems in the code.

A tftp client starts a tftp session by sending a "read request packet," which contains a file name it would like to receive and a mode. The two primary modes are netascii, which performs some simple translations of the file, and octet, which sends the file exactly as it appears on the disk. This server supports only octet mode, as it is simpler.

Upon receiving the read request, the tftp server sends the file 512 bytes at a time. Each datagram contains the block number (starting from 1), and the client knows that the file has been properly received when it receives a data block containing less than 512 bytes. After each datagram, the client sends a datagram containing the block number, which acknowledges that the block has been received. Once the server sees this acknowledgment, it sends the next block of data.

The basic format of a datagram is defined by lines 17-46. Some constants are defined that specify the type of datagram being sent, along with an error code that is sent if the requested file does not exist (all other errors are handled quite unceremoniously by the server, which simply exits). The struct tftpPacket outlines what a datagram looks like, with an opcode followed by data that depends on the datagram type. The union nested inside of the structure then defines the rest of the datagram format for error, data, and acknowledgment packets.

The first part of main(), in lines 156-169, creates the UDP socket and sets up the local port number (which is either the well-known service for tftp or the port specified as the program's sole command-line argument) by calling bind(). Unlike our TCP server example, there is no need to call either listen() or accept() as UDP is connectionless.

Once the socket has been created, the server waits for a datagram to be sent to by calling recvfrom(). The handleRequest() function is invoked on line 181, which transmits the file requested and then returns. After this call, the server once more calls recvfrom() to wait for another client to make a request. The comment right above the call to handleRequest() notes that it is quite easy to switch this server from an iterative server to a concurrent one by letting each call to handleRequest() run as its own process.

While the main part of the program used an unconnected UDP socket (which allowed any client to connect to it), handleSocket() uses a connected UDP socket to transfer the file.[32] After parsing out the file name that needs to be sent and making sure the transfer mode is correct, line 93 creates a socket of the same family, type, and protocol that the server was contacted on. It then uses connect() to set the remote end of the socket to the address that requested the file, and begins sending the file. After sending each block, the server waits for an acknowledgment packet before continuing. After the last one is received, it closes the socket and goes back to the main loop.

[32] The tftp protocol specification requires servers to send data to a client on a port number other than the port the server is waiting for new requests on. It also makes it easier to write a concurrent server as each server socket is intended for exactly one client.

This server should normally be run with a port number as its only argument. To test it, the normal tftp client program can be used, with the first argument being the hostname to connect to (localhost is probably a good choice) and the second being the port number the server is running on. Once the client is running, the bin command should be issued to it so that it requests files in octet mode rather than the default netascii. Once this has been done, a get command will be able to transfer any file from the server to the client.

   1: /* tftpserver.c */   2:   3: /* This is a partial implementation of tftp. It doesn't ever time   4:    out or resend packets, and it doesn't handle unexpected events   5:    very well. */   6:   7: #include <netdb.h>   8: #include <stdio.h>   9: #include <stdlib.h>  10: #include <string.h>  11: #include <sys/socket.h>  12: #include <unistd.h>  13: #include <fcntl.h>  14:  15: #include "sockutil.h"         /* some utility functions */  16:  17: #define RRQ 1                 /* read request */  18: #define DATA 3                /* data block */  19: #define ACK 4                 /* acknowledgement */  20: #define ERROR 5               /* error occured */  21:  22: /* tftp error codes */  23: #define FILE_NOT_FOUND  1  24:  25: struct tftpPacket {  26:     short opcode;  27:  28:     union {  29:         char bytes[514];         /* largest packet we can handle has 2  30:                                      bytes for block number and 512  31:                                      for data */  32:         struct {  33:             short code;  34:             char message[200];  35:         } error;  36:  37:         struct {  38:             short block;  39:             char bytes[512];  40:         } data;  41:  42:         struct {  43:             short block;  44:         } ack;  45:     } u;  46: };  47:  48: void sendError(int s, int errorCode) {  49:     struct tftpPacket err;  50:     int size;  51:  52:     err.opcode = htons(ERROR);  53:  54:     err.u.error.code = htons(errorCode);   /* file not found */  55:     switch (errorCode) {  56:     case FILE_NOT_FOUND:  57:         strcpy(err.u.error.message, "file not found");  58:         break;  59:     }  60:  61:     /* 2 byte opcode, 2 byte error code, the message and a '\0' */  62:     size = 2 + 2 + strlen(err.u.error.message) + 1;  63:     if (send(s, &err, size, 0) != size)  64:         die("error send");  65: }  66:  67: void handleRequest(struct addrinfo tftpAddr,  68:                    struct sockaddr remote, int remoteLen,  69:                    struct tftpPacket request) {  70:     char * fileName;  71:     char * mode;  72:     int fd;  73:     int s;  74:     int size;  75:     int sizeRead;  76:     struct tftpPacket data, response;  77:     int blockNum = 0;  78:  79:     request.opcode = ntohs(request.opcode);  80:     if (request.opcode != RRQ) die("bad opcode");  81:  82:     fileName = request.u.bytes;  83:     mode = fileName + strlen(fileName) + 1;  84:  85:     /* we only support bin mode */  86:     if (strcmp(mode, "octet")) {  87:         fprintf(stderr, "bad mode %s\n", mode);  88:         exit(1);  89:     }  90:  91:     /* we want to send using a socket of the same family and type  92:        we started with, */  93:     if ((s = socket(tftpAddr.ai_family, tftpAddr.ai_socktype,  94:                     tftpAddr.ai_protocol)) < 0)  95:         die("send socket");  96:  97:     /* set the remote end of the socket to the address which  98:        initiated this connection */  99:     if (connect(s, &remote, remoteLen)) 100:         die("connect"); 101: 102:     if ((fd = open(fileName, O_RDONLY)) < 0) { 103:         sendError(s, FILE_NOT_FOUND); 104:         close(s); 105:         return ; 106:     } 107: 108:     data.opcode = htons(DATA); 109:     while ((size = read(fd, data.u.data.bytes, 512)) > 0) { 110:         data.u.data.block = htons(++blockNum); 111: 112:         /* size is 2 byte opcode, 2 byte block number, data */ 113:         size += 4; 114:         if (send(s, &data, size, 0) != size) 115:             die("data send"); 116: 117:         sizeRead = recv(s, &response, sizeof(response), 0); 118:         if (sizeRead < 0) die("recv ack"); 119: 120:         response.opcode = ntohs(response.opcode); 121:         if (response.opcode != ACK) { 122:             fprintf(stderr, "unexpected opcode in response\n"); 123:             exit(1); 124:         } 125: 126:         response.u.ack.block = ntohs(response.u.ack.block); 127:         if (response.u.ack.block != blockNum) { 128:             fprintf(stderr, "received ack of wrong block\n"); 129:             exit(1); 130:         } 131: 132:         /* if the block we just sent had less than 512 data 133:            bytes, we're done */ 134:         if (size < 516) break; 135:     } 136: 137:     close(s); 138: } 139: 140: int main(int argc, char ** argv) { 141:     struct addrinfo hints, * addr; 142:     char * portAddress = "tftp"; 143:     int s; 144:     int rc; 145:     int bytes, fromLen; 146:     struct sockaddr from; 147:     struct tftpPacket packet; 148: 149:     if (argc > 2) { 150:         fprintf(stderr, "usage: tftpserver [port]\n"); 151:         exit(1); 152:     } 153: 154:     if (argv[1]) portAddress = argv[1]; 155: 156:     memset(&hints, 0, sizeof(hints)); 157: 158:     hints.ai_socktype = SOCK_DGRAM; 159:     hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE; 160:     if ((rc = getaddrinfo(NULL, portAddress, &hints, &addr))) 161:         fprintf(stderr, "lookup of port %s failed\n", 162:                 portAddress); 163: 164:     if ((s = socket(addr->ai_family, addr->ai_socktype, 165:                     addr->ai_protocol)) < 0) 166:         die("socket"); 167: 168:     if (bind(s, addr->ai_addr, addr->ai_addrlen)) 169:         die("bind"); 170: 171:     /* The main loop waits for a tftp request, handles the 172:        request, and then waits for another one. */ 173:     while (1) { 174:         bytes = recvfrom(s, &packet, sizeof(packet), 0, &from, 175:                          &fromLen); 176:         if (bytes < 0) die("recvfrom"); 177: 178:         /* if we forked before calling handleRequest() and had 179:            the child exit after the function returned, this server 180:            would work perfectly well as a concurrent tftp server */ 181:         handleRequest(*addr, from, fromLen, packet); 182:     } 183: } 



       
    top
     


    Linux Application Development
    Linux Application Development (paperback) (2nd Edition)
    ISBN: 0321563220
    EAN: 2147483647
    Year: 2003
    Pages: 168

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net