From word processors to spreadsheets, databases to
games, an in-creasing number of applications are becoming network-aware. So how do these
programs communicate with each other? What does your Web browser do when it wants to send
a request to the local Internet server? While playing a death match in Quake, how do the
various Quake machines talk to each other?
Berkeley Sockets is what most programs use to
communicate. For instance, when your Web browser opens a connection to the remote HTTP
server, it uses Sockets. Whenever you click a link on a Web page, the browser sends the
HTTP request through this connection. For that matter, every Quake client uses sockets to
communicate with a Quake server.
In this article, we’ll take a look at socket
programming and along the way we’ll build a small client/server application using the
Berkeley Sockets API.
The programs given here have been written and compiled in
Linux, using the Gnu C compiler, gcc. We’ve used Linux since socket is primarily a
Unix API. The Windows series of operating systems use a derivative of the Berkeley sockets
called Windows Sockets, popularly called Winsocks.
What are sockets? COLOR="#000000">
A socket allows a software program (
face="Arial" size="2">process) to communicate with other processes over a network.
Usually one process waits for others to connect to it. This process is called the server.
The connecting processes are clients.
Servers usually come in two forms: iterative or concurrent.
An iterative server gives its entire attention to a client. Other clients wanting to
connect to the server have to wait. Only when the server finishes with the first client
does it service the others.
A concurrent server is capable of handling more than one
client at a time. When a client requests a connection, the server spawns a copy of itself
called a child.
The child then handles the client while the parent (the
server) waits for new connections. Whenever a new client comes along, the server spawns
another child to handle the request.
Different kinds of sockets COLOR="#000000">
There are various types of sockets.
For instance, take two processes running on the same (Unix) system. They can use Unix
Domain Protocol sockets to communicate. These sockets can communicate with other processes
on the same system only. If a process wishes to communicate over the Internet, then it
uses Internet Domain Protocol sockets.
Moreover, a socket can be connection-oriented or
connection-less. When the client and server processes maintain a dedicated link between
themselves, they use connection-oriented sockets. Think of it as a telephone conversation,
where the two parties talking to each other maintain a link throughout.
The connection-less socket is like the postal service. The
client sends a packet of information called a datagram to the server. The server receives
the datagram sometime later and sends a reply to the client.
What is a port? COLOR="#000000">
A port is a 16-bit number used by a server to identify
itself to the clients. Imagine that there are three different servers running on a host.
Each network interface on the host has its own IP address. The client comes along through
one of these interfaces. How does this client decide on which server to contact? The
client can resolve this conflict if it knows the port number of the server it wants to
reach.
Several Internet services (servers) are so popular that
they have been given their own port numbers. The FTP server, for instance, always uses the
port number 21. Whenever an FTP client wants to connect to an FTP server, it connects on
port 21. The Internet community has reserved ports 1 to 1,023 for standard services like
telnet, finger, and FTP. The rest of them have been left free for other user-developed
servers.
The best way to understand the socket architecture is to
see it in action. Look in the accompanying PC Quest CD \cdrom\ sourcecode
directory for the file, server.txt. This file contains installation instructions
for a small client/server program we’ll examine in this article. The server presented
here is concurrent and opens a connection-oriented Internet domain socket for each client
that it services.
The server COLOR="#000000">
Let us examine how the server works.
Look at the file server.c.
if ((sd = socket(AF_INET, SOCK_STREAM, 0))
< 0)
err("server: unable to open socket.");
The line opens a socket. Since we want to open an Internet
Domain Protocol socket, we use the AF_INET define. AF_INET stands for Address Family
Internet.
A connection-oriented socket is also known as a stream
socket since the data is sent in streams. This is opposed to packets of information
(datagrams) sent in connection-less sockets. Here, the SOCK_STREAM defined is used since
we want a connection-oriented socket.
The socket() system call returns an integer by which we can
later identify this socket. This integer is called a socket descriptor. The server then
binds this socket with a port using the bind() system call. We’ve used port number
5,523 for our server. This is an arbitrary number; any unassigned port will do.
if (bind(sd, (struct sockaddr *)
&server_addr,
sizeof(server_addr)) < 0)
err("server: Unable to bind local address.");
Okay, now that we have opened a socket and bound it to a
port, its time to receive connections from clients. The server tells the system that
it’s ready for any clients with the listen() system call.
listen(sd, 5);
Next, the server enters an infinite loop and waits for an
actual connection with a client. Whenever a client connects, the accept() system call,
returns a new socket descriptor with the same properties as the original socket.
newsd = accept(sd, (struct sockaddr *)
&client_addr, &client_size);
A child process is then spawned by the server using the
fork() system call. The child uses the new socket descriptor to communicate with the
client.
if ((cpid = fork()) < 0)
err("server: unable to spawn child process.");
else if (cpid == 0) /* CHILD PROCESS */
{
close(sd); /* close the old socket */
if ((readval = read(newsd, buf, sizeof(buf))) < 0)
err("server: unable to read from socket.");
...
...
Meanwhile, the server waits for other clients. If another
client wants to connect, the server spawns a new child process to handle it.
COLOR="#ff0000">
The client COLOR="#000000">
Now that we have seen how the server
works, let’s have a look at a client. The file client.c
implements a small client program that connects to our server. The client also opens a
socket using the socket() system call.
Meanwhile, the server is waiting for clients on port 5,523.
The client requests a connection with our server using the connect() system call.
if (connect(sd, (struct sockaddr *)
&server_addr,
sizeof(server_addr)) < 0)
err("client: unable to connect to server.");
If the client connects without any snags, it sends a simple
string across to the server and terminates.
char message<> = "Hello World";
...
...
if (write(sd, message, sizeof(message))
< 0)
err("client: unable to write to server.");
The server receives this string from the client and prints
it on the screen.
That’s all that’s there to our simple
client/server implementation. Feel free to modify the code. Try writing a more
sophisticated system, a small time server for instance. The time server
returns the correct time of day to any connecting client. There are some wonderful books
around dealing with network programming. Unix Network Programming by W Richard
Stevens is an old classic that I recommend highly.