Sockets Tutorial Introduction to Socket Programming in C InterProcess Communication (IPC) Overview This is intended to be a brief but practical tutorial on implementing socket-based interprocess communication. Introduction This tutorial focuses on a specific type of socket-based interprocess communication: stream-oriented connections in the Internet domain. Addressing As described in class, a socket is a communication endpoint. It enables bidirectional communication between processes located on the same or different machines. Two processes on the same machine, and hence sharing the same file system, can communicate in what is called the UNIX domain. Simple UNIX pathnames can be used to identify sockets. More commonly, and as considered in this tutorial, the two processes reside on different machines. In that case they must communicate using the Internet domain. Internet addresses (i.e. the familiar IP address) and port numbers (a 16-bit unsigned integer) are used to specify sockets. Socket Types There are two commonly used types of sockets. Datagram sockets expect a unit of data; they read the entire message at once. These message-oriented sockets use the UNIX datagram protocol (UTP). More common, and again the subject of this tutorial, are the connection-oriented sockets. These so-called stream sockets use continuous streams of characters, and communicate using the reliable Transmission Control Protocol (TCP). Clients/servers Once two processes have connected to each other, their method of communication is identical. However, processes differ in how they establish sockets and initiate communication. Server processes establish a socket with a known, or published, address. Client processes create an unnamed socket and initiate communication by connecting to a server socket. Creating a server socket A server process creates a socket, names it, and listens for communication requests. Creating a socket The socket() call creates an unnamed socket and returns a file descriptor to the calling process. The function prototype is as follows: int socket(int domain, int type, int protocol); where domain specifies the address domain, type specifies the socket type, and the third argument is the protocol (this is usually set to 0 to automatically select the appropriate default). Here is a typical socket() call: sockfd = socket(AF_INET, SOCK_STREAM, 0); This establishes a socket in the Internet domain, and configures it for stream-oriented communication using the default TCP protocol. Naming - binding the socket to a particular address The bind() system call associates a socket with a particular name or address. In other words, this call specifies the port a server process will listen to. The function prototype is as follows: int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen); where sockfd is the socket file descriptor returned from a previous socket() call, my_addr is a pointer to a structure containing the address to bind to, and addrlen is the size of the structure. The key to using this call is understanding how to fill in the fields of the sockaddr structure. This structure (defined in netinet/in.h) contains fields for the address family (sin_family), the IP address of the host (sin_addr.s_addr), the port number (sin_port), and some padding. Here is a typical bind() call: bind (sockfd, (struct sockaddr *) &server, sizeof(server)); This call binds the socket sockfd to the address contained in the structure named server. Within the structure server, the field family has been set to AF_INET, specifying the Internet address domain. The port number has been set (using the htons() function for portability) to any currently unused number on that system. Finally, since this is a server, the IP address has been set to INADDR_ANY, specifying that connections will be accepted from any other host. Because systems may use different byte-ordering for data objects such as numbers, functions have been created to allow systems to communicate using a standardized network-order format. Here is the function prototype for one of them: uint16_t htons(uint16_t hostshort); where a short is the value being converted into its network-order version. As an example of its use, here is the function call used to convert the integer representation of the desired port number into the correct network format: server.sin_port = htons ((short) 3000 + BlockedOn); In this example, the port number 3002 will be converted and assigned to the appropriate field of the sockaddr structure. Establishing a queue The listen() system call establishes the size of the incoming request queue. Although the listen() call is not strictly necessary, it is commonly used. The function prototype is as follows: int listen(int s, int backlog); where s is the socket and backlog is the size of the queue (i.e. the maximum queued connection requests). Here is a typical listen() call: listen (sockfd, MAXCONNECT); This call specifies that the socket sockfd will queue up to MAXCONNECT requests. Waiting for connections Finally, the accept() system call causes the server process to block until a client attempts to connect. The function prototype is as follows: int accept(int s, struct sockaddr *addr, socklen_t *addrlen); where s is the socket, addr is a pointer to a structure of type sockaddr, and addrlen is a pointer to a variable containing the size of the structure. The address pointer is a reference to an address structure containing information about the connecting client, in case that information is desired. In other words, this call will cause the system to populate the fields of the sockaddr structure with information about the client that has just connected. For example, this information could be used to determine the hostname of the connecting client process. The accept() system call returns a new file descriptor that is the one normally used for subsequent communications. Here is a typical accept() call: tempfd = accept (sockfd, (struct sockaddr *) &client, &len); This will cause the process to wait for a connection request on socket sockfd, then return information about the connecting process in the structure named client, and return a new socket descriptor called tempfd for communications. Handling multiple connections in the server The server connects with each client through the new, unnamed socket returned by accept() and retains the original named socket for subsequent connection requests from other clients. If other clients do connect to the server, then there are several techniques for managing connections and communicating with multiple clients. Using multiple processes The fork() system call spawns a new process. The function prototype is as follows: pid_t fork(void); where a PID (Process ID) of 0 is returned to the child, the PID of the child process is returned to the parent (i.e. fork() returns twice). The fork() system call creates an exact duplicate of the calling process in a separate address space. In the context of networking, a child process would be spawned to handle each new connection while the parent retains the original socket for additional incoming connection requests. Using nonblocking I/O The select() system call allows a process to perform synchronous I/O multiplexing without polling. Its primary use is to provide a type of nonblocking I/O. Basically, a process will suspend itself until I/O is available on any one of a set of file descriptors. The function prototype is: int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); where n is one more than the number of file descriptors to check, followed by a set of file descriptors to be monitored for reading, a set of file descriptors to be monitored for writing, a set of file descriptors to be monitored for exceptional conditions, and a value after which the system call times out. The file descriptor sets are represented as bit fields in an array of integers, with macros provided for platform-independent manipulation of the sets. For example, the FD_ZERO macro clears all bits in the set it is called on. A timeout value of NULL signifies the process will wait indefinitely for I/O. Using threads The pthread_create() function call creates a new thread of execution within the existing process. The function prototype is: int pthread_create(pthread_t *thread, pthread_attr_t *attr, void* (*start_routine)(void*), void *arg); where thread is a pointer to a location that will hold the identifier of the new thread, attr is a pointer to a structure whose fields specify the desired attributes of the newly created thread, start_routine is the name of the function the new thread will execute, and arg is a pointer to any arguments required by the new thread. In the context of socket programming, a thread is spawned to handle each new connection while the main thread retains the original, named socket for additional connection requests. Connecting from a client to a server The client begins by creating an unnamed socket and then requests a connection with a named socket located on a remote server. Creating a socket A client creates a socket in a manner similar to a server, by using the socket system call (see above). Connecting to the server A client connects to a server via the connect() system call, which resembles the bind() system call used in the server. The difference lies in the initialization of the sockaddr structure, which must contain the address of the server to which the client wishes to connect. The function prototype is as follows: int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); where sockfd is the socket, serv_addr specifies the socket the client is connecting to, and addrlen is the size of the structure. Here is a typical use of the function: connect (sockfd, (struct sockaddr*)&server, sizeof(server))) where server contains information describing the host. As before, the sin_family field (in the sockaddr structure) is specified as AF_INET. The sin_addr field is filled in with information necessary to locate the host (i.e. its address). If the name of the host is known, then the gethostbyname() function can be used to obtain this information. The function prototype follows: struct hostent *gethostbyname(const char *name); where name is the name of the host, and the return value is a pointer to a structure of type hostent that contains information about that host. Here is a typical call: hostPtr = gethostbyname(hostname)) where hostname is, for example "eos03". After this call, the hostent structure pointed to by hostPtr contains the required address information about eos03. This host information must now be copied into the address field of the sockaddr structure. To simplify this process, a function called memcpy() can be used to copy information directly into the field. The function prototype is: void *memcpy(void *dest, const void *src, size_t n); which copies n bytes from src (sourse) to dest (destination). A typical call would be: memcpy((char*) &server.sin_addr, hostPtr->h_addr_list[0], hostPtr->h_length); which copies the host information directly from the address field of the hostent structure into the address field of the sockaddr structure. Note that memcpy() can be used in any instance where a fixed number of bytes of memory need to be copied into some other location or data structure. Alternatively, if the name of the host is not known but its IP address is, the gethostbyaddr() function can be used instead to return the same, necessary information. Its prototype is as follows: struct hostent *gethostbyaddr(const char *addr, int len, int type); where addr is the host address of length len and type AF_INET. Communicating Communication over sockets is simple. Socket descriptors look like file descriptors to the system, hence normal file-like communication protocols can be used. However, note that the byte-order may be different for different machines (i.e. the "endianess"), so communicating data other than characters may require using the network-ordering functions described previously. There are many different communication protocol options. The function prototypes for the read() and write() methods follow: ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); In these calls, fd is the file descriptor, buf is typically a array of bytes that contains the message to send (write()) or is where the received message will be placed (read()), and count is the size of the message sent or received. There are also socket-specific communication functions called send() and recv(). Their function prototypes are very similar to read() and write(): int send(int s, const void *msg, size_t len, int flags); int recv(int s, void *buf, size_t len, int flags); where the first three arguments are as for reading and writing above and the flags parameter specifies options relevant to network communications. Here are some example function calls: send (sockfd, buf, strlen(buf), 0); recv (tempfd, buf, MAXCHAR, 0); In this example, the client is sending a message on sockfd, and the server is receiving it on tempfd. Termination Remember that socket descriptors, like file descriptors, are a finite resource. The close() system call must be used to close a file descriptor and free it for subsequent reuse. The function prototype is: int close(int fd); where fd is the file descriptor to close. Final Comments A few last useful suggestions and notes: Read the man pages. As usual, this is the definitive source on how to use these functions. Perform error-checking. In the interests of brevity and clarity I have omitted the normal error-checking that should accompany every system call. Real programs would not omit this vital step. Include the required files. Each of the system calls has different .h files that are required for compilation. Create the appropriate data structures. Most of these system calls require creating, and appropriately using, various data objects (e.g. filling in fields, casting references, etc.). Understand and use the return values. Some of the system calls have very useful return values that can help your program determine how to proceed. For example, you might simply try again if your attempt at connecting to a socket was refused. Oh, and read the man pages!