We've spend a great deal of time talking about Rendezvous in the previous two columns, but Rendezvous can't exist in a vacuum. Having user-friendly service discovery does us no good unless we can make our applications talk to one another. Indeed, Rendezvous has absolutely no provisions for facilitating general network communications between applications, as it is only a protocol for advertising and discovering services on a network. It is a discovery protocol, not a communications protocol.
Today we shift gears into the communications side of this business; we will have very little to say about Rendezvous. Due to its Unix lineage, Mac OS X is a wonderful platform for learning about networking, since it has such a rich set of APIs to offer; in particular, we can program with the venerable BSD sockets API. Today we'll learn about this API, and in doing so we will write a tiny pair of C applications that demonstrate how clients and servers can be made to talk with one another. In the next column, we will finish RCE with what we learn today by adding some Cocoa.
Most of us have likely heard of sockets in the course of our experiences programming. I had always heard about sockets, but up until about half a year ago, I had never had the pleasure of programming with them. Having heard of something is far from understanding it, which is essential to being able to effectively use a technology. In this column and the next, I hope to spark some interest in the subject to give you a feel for the technology. Hopefully, many of you who had previously shied away from sockets and networking will go on to learn more about this interesting and relevant topic.
Also in Programming With Cocoa:
So just what is a socket? The
man page for the
socket() function, which we use to create sockets, describes this little thing in four words: an endpoint for communication. The analogy often used to relate sockets to everyday experiences is that of a telephone, which, as we will see, is indeed an accurate comparison. A telephone is, after all, an endpoint, or an interface, to a communications network that we use to communicate with other people.
In the same way that we speak into and listen to a phone, applications both send data across a network by writing to a socket, and receive data sent by a remote host by reading from the socket. If you are familiar with the Unix APIs for reading and writing to a file, you will be comfortable with sockets, as the same functions for file I/O are used for socket I/O -- namely,
Like two telephones that facilitate a conversation between two people, network connections exist between a pair of sockets, one for each end of the connection. Sockets are often talked about in pairs: one for the server side of the application, and one for the client side. The networking model that we are accustomed to is that of the relationship between a client and a server. A server is an application that is listening for connection requests from clients, and handling them appropriately. A client is a program that connects to a server. Usually client and servers are two completely different applications, as is the case with a Web client and server: Apache is a Web server, while OmniWeb, Internet Explorer, and Mozilla are all Web clients.
We will see in the next column how this distinction between server and client blurs when we talk about peer-to-peer chat applications like RCE. Sometimes, one application is both a client and a server that allows connections from other like applications. This is especially true of peer-to-peer applications, such as the chat application we're building. We'll get into this more in the next column, but understand as we progress through our discussion today that RCE will have both server functionality and client functionality.
Working With Sockets
Because of the differing tasks of a server and a client, their use of sockets is accordingly different. The role each side takes in establishing a communications link is reflected in the nature of the sockets each side uses. To wit, servers use what are known as passive sockets, and clients use active sockets.
When a server process starts up, it must create a socket; bind it to a local, unused port; tell that socket to listen for new connections from clients; and finally, begin waiting for new connections. This socket is often referred to as a listening socket, or a passive socket, or a server socket. All of these names suggest that the role of the socket is to sit patiently while listening to its assigned port for clients requesting a connection with the server. In the analogy of the telephone, creating the socket is like buying a phone, binding is akin to getting a hookup from the phone company, listening is plugging your phone into the wall, and finally, accepting is the act of answering the phone when it rings.
When a connection is received by the listening socket, the server must accept the connection and return a new connected socket that is used to communicate with the client. This new socket has an established connection to the client's remote socket. By creating a new socket to handle the new connection, the listening socket is free to continue doing its thing, listening for connections from other clients.
Clients use sockets in a different way. A client creates a socket in the same way as a server; however, after the socket is created the use of the socket differs. With a socket in hand, the client uses that socket to attempt to connect to a server. Once the connection has been accepted by the server, the client can begin sending and receiving data from the server. Referring back to our phone analogy, connecting is no different than dialing a phone number for someone you want to talk to.
Our Sockets Toolkit
What are all of these functions that we have been alluding to without mentioning? They are the functions of the BSD Sockets API, which is primarily defined in the header sys/socket.h (header file paths are always referenced relative to the path /usr/include). There are seven functions that we will discuss, three of which are part of the standard library. They are:
int socket( int domain, int type, int protocol )
- Creates a new socket and returns the socket file descriptor. The domain argument is a constant to specify the address family of the socket; we will use
AF_INET, which is IPv4 addressing. The argument type specifies the socket type; we will pass the constant
SOCK_STREAMhere, which is a TCP stream socket. The protocol argument does not concern us here, so we pass
-1if there is an error.
int connect( int s, const struct sockaddr *name, int namelen )
- Connects the socket identified by the file descriptor s to the remote socket specified in the address structure name. Returns
int bind( int s, const struct sockaddr *name, int namelen )
- Binds the socket s to the port specified in the address structure name. Returns
int listen( int s, int backlog )
- Converts the socket s into a passive listening (server) socket. The parameter backlog specifies how many pending connections the kernel will allow before clients who attempt to connect will receive a connection refused error. Returns
int accept( int s, struct sockaddr *addr, int *addrlen )
- This function will return a socket connected to the remote socket with the first connection request in the connection queue. The socket returned is not the same socket as s, but it has the same properties as s. The address structure of the connected socket is returned in the struct addr. Returns
-1if there is an error.
ssize_t read( int d, void *buf, size_t nbytes )
- Attempts to read nbytes of data from the socket d into the array buf. Returns the number of bytes that was actually read.
ssize_t write( int d, const void *buf, size_t nbytes )
- This function writes to the socket d nbytes number of bytes from the array buf.
int close( int s )
- This function closes the socket.
Let's take a moment to look at these functions. We discussed above that servers and clients use sockets in different ways. As such, some of these functions are only appropriate for use by a client and others are used only by servers. First, both clients and servers use
close(). The function
connect() is used by clients, while the remaining three --
accept() -- are used by servers.