Linux
by Kamran Husain and Tim Parker
IN THIS CHAPTER
- Ports and Sockets
- Socket Programming
- Record and File Locking
- Interprocess Communications
This chapter looks at the basic concepts you need for network programming:
- Ports and sockets
- Record and file locking
- Interprocess communications
It is impossible to tell you how to program applications for a network in just
a few pages. Indeed, the best available reference to network programming takes almost
800 pages in the first volume alone! If you really want to do network programming,
you need a lot of experience with compilers, TCP/IP, and network operating systems--and
you need a great deal of patience.
For details on TCP/IP, check the book Teach Yourself TCP/IP in 14 Days, by Tim
Parker (Sams Publishing).
Network programming relies on the use of sockets to accept and transmit information.
Although there is a lot of mystique about sockets, the concept is actually simple
to understand.
Most applications that use the two primary network protocols, Transmission Control
Protocol (TCP) and User Datagram Protocol (UDP) have a port number that identifies
the application. A port number is used for each different application the machine
is handling, so it can keep track of those applications by numbers rather than names.
The port number makes it easier for the operating system to know how many applications
are using the system and which services are available.
In theory, port numbers can be assigned on individual machines by the system administrator,
but some conventions have been adopted to allow better communications. These conventions
enable the port number to identify the type of service that one system is requesting
from another. For this reason, most systems maintain a file of port numbers and their
corresponding services.
Port numbers are assigned starting from the number 1. Normally, port numbers above
255 are reserved for the private use of the local machine, but numbers between 1
and 255 are used for processes requested by remote applications or for networking
services.
Each network communications circuit into and out of the host computer's TCP application
layer is uniquely identified by a combination of two numbers, together called the
socket. The socket is composed of the IP address of the machine and the port number
used by the TCP software.
Because at least two machines are involved in network communications, there will
be a socket on both the sending and the receiving machine. Because the IP address
of each machine is unique and the port numbers are unique to each machine, socket
numbers are also unique across the network. This setup enables an application to
talk to another application across the network based entirely on the socket number.
The sending and receiving machines maintain a port table that lists all active
port numbers. The two machines involved have reversed entries for each session between
the two, a process called binding. In other words, if one machine has the source
port number 23 and the destination port number set at 25, the other machine has its
source port number set at 25 and the destination port number set at 23.
Linux supports BSD-style socket programming. Both connection-oriented and connectionless
types of sockets are supported. In connection-oriented communication, the server
and client establish a connection before any data is exchanged. In connectionless
communication, data is exchanged as part of a message. In either case, the server
always starts first, binds itself to a socket, and listens to messages. How the server
attempts to listen depends on the type of connection for which you have programmed
it.
You need to know about a few system calls:
- socket()
- bind()
- listen()
- accept()
- setsockopt() and getsockopt()
- connect()
- sendto()
recvfrom()
We will cover these system calls in the following examples.
The socket() system call creates a socket for the client or the server.
The socket function is defined as shown here:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int family, int type, int protocol)
For Linux, you will have family = AF_UNIX. The type is either SOCK_STREAM
for reliable, though slower, communications or SOCK_DGRAM for faster, but
less reliable, communications. The protocol should be IPPROTO_TCP for SOCK_STREAM
and IPPROTO_UDP for SOCK_DGRAM.
The return value from this function is -1 if there was an error; otherwise,
it's a socket descriptor. You will use this socket descriptor to refer to this socket
in all subsequent calls in your program.
Sockets are created without a name. Clients use the name of the socket to read
or write to it. This is where the bind function comes in.
The bind() system call assigns a name to an unnamed socket. The bind
function is defined like this:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr *saddr, int addrlen)
The first item is a socket descriptor. The second is a structure with the name
to use, and the third item is the size of the structure.
Now that you have bound an address for your server or client, you can connect()
to it or listen on it. If your program is a server, it sets itself up to listen and
accept connections. Let's look at the function available for such an endeavor.
The listen() system call is used by the server. It is defined in the
following way:
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd, int backlog);
The sockfd is the descriptor of the socket. The backlog is the number of connections
that are pending at one time before any are rejected. Use the standard value of 5
for backlog. A returned value of less than 1 indicates an error.
If this call is successful, you can accept connections.
The accept() system call is used by a server to accept any incoming messages
from clients' connect() calls. Be aware that this function does not return
if no connections are received. It is defined like this:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *peeraddr, int addrlen)
The parameters are the same as those for the bind call, with the exception
that the peeraddr points to information about the client that is making a connection
request. Based on the incoming message, the fields in the structure pointed at by
peeraddr are filled out.
The socket libraries provided with Linux include a bug. The symptom of this bug
is that you cannot reuse a port number for a socket even if you closed the socket
properly. For example, say you write your own server that waits on a socket. This
server opens the socket and listens on it with no problems. However, for some reason
(a crash or normal termination), when the program is restarted, you are not able
to bind to the same port. The error codes from the bind() call will always
return an error indicating that the port you are attempting to connect to is already
bound to another process.
The problem is that the Linux kernel never marks the port as unused when the process
bound to a socket terminates. In most other UNIX systems, the port can be used again
by another invocation of the same or even another process.
The way to get around this problem in Linux is to use the setsockopt()
system call to set the options on a socket when it is opened and before a connection
is made on it. The setsockopt() sets options and the getsockopt()call
gets options for a given socket.
Here is the syntax for these calls:
#include<sys/types.h>
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int name, char *value, int *optlen)
int setsockopt(int sockfd, int level, int name, char *value, int *optlen)
The sockfd must be an open socket. The level is the protocol level to use for
the function (IPPROTO_TCP for TCP/IP and SOL_SOCKET for socket
level options), and the name of the option is as defined in the socket's man page.
The *value pointer points to a location where a value is stored for getsockopt()
or when a value is read for setsockopt(). The optlen parameter is a pointer
to an integer containing the length of the parameters in bytes; the value is set
by getsockopt() and must be set by the programmer when making a call via
setsockopt().
The full man page with details of all the options is found in the man page setsockopt(2).
Now back to the bug in Linux. When you open a socket, you must also call the setsockopt()
function with the following segment of code:
#ifdef LINUX
opt = 1; len = sizeof(opt);
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);
#endif
The #ifdef and #endif statements are necessary only if you want
to port the code over to systems other than Linux. Some UNIX systems might not support
or require the SO_REUSEADDR flag.
The connect() system call is used by clients to connect to a server in
a connection-oriented system. This connect() call should be made after the
bind() call. It is defined like this:
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, struct sockaddr *servsaddr, int addrlen)
The parameters are the same as those for the bind call, with the exception
that the servsaddr points to information about the server that the client is connecting
to. The accept call creates a new socket for the server to work with the
request. This way, the server can fork() off a new process and wait for
more connections. On the server side of things, you would have code that looks like
that shown in Listing 59.1.
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <linux/net.h>
#define MY_PORT 6545
main(int argc, char *argv[])
{
int sockfd, newfd;
int cpid; /* child id */
struct sockaddr_in servaddr;
struct sockaddr_in clientInfo;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0) < 0)
{
myabort("Unable to create socket");
}
#ifdef LINUX
opt = 1; len = sizeof(opt);
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);
#endif
bzero((char *)&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_family = htons(MY_PORT);
/*
* The htonl (for a long integer) and htons (for short integer) convert
* a host oriented byte order * into a network order.
*/
if (bind(sockfd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr)) < 0)
{
myabort("Unable to bind socket");
}
listen(sockfd,5);
for (;;)
{
/* wait here */
newfd=accept(sockfd,(struct sockaddr *)&clientInfo,
sizeof(struct sockaddr);
if (newfd < 0)
{
myabort("Unable to accept on socket");
}
if ((cpid = fork()) < 0)
{
myabort("Unable to fork on accept");
}
else if (cpid == 0) { /* child */
close(sockfd); /* no need for original */
do_your_thing(newfd);
exit(0);
}
close(newfd); /* in the parent */
}
}
In the case of connection-oriented protocols, the server performs the following
functions:
- Creates a socket with a call to the socket() function
- Binds itself to an address with the bind() function call
- Listens for connections with the listen() function call
- Accepts any incoming requests with the accept() function call
- Gets incoming messages with the read() function and replies with the
write() call
Now let's look at the client side of things, in Listing 59.2.
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <linux/net.h>
#define MY_PORT 6545
#define MY_HOST_ADDR "204.25.13.1"
int getServerSocketId()
{
int fd, len;
struct sockaddr_in unix_addr;
/* create a Unix domain stream socket */
if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
{
return(-1);
}
#ifdef LINUX
opt = 1; len = sizeof(opt);
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);
#endif
/* fill socket address structure w/our address */
memset(&unix_addr, 0, sizeof(unix_addr));
unix_addr.sin_family = AF_INET;
/* convert internet address to binary value*/
unix_addr.sin_addr.s_addr = inet_addr(MY_HOST_ADDR);
unix_addr.sin_family = htons(MY_PORT);
if (bind(fd, (struct sockaddr *) &unix_addr, len) < 0)
return(-2);
memset(&unix_addr, 0, sizeof(unix_addr));
if (connect(fd, (struct sockaddr *) &unix_addr, len) < 0)
return(-3);
return(fd);
}
The client for connection-oriented communication also takes the following steps:
- Creates a socket with a call to the socket() function
- Attempts to connect to the server with a connect() call
- If a connection is made, requests data with the write() call, and reads
incoming replies with the read() function
Now let's consider the case of a connectionless exchange of information. The principle
on the server side is different from the connection-oriented server side in that
the server calls recvfrom() rather than the listen and accept
calls. Also, to reply to messages, the server uses the sendto() function
call. See Listing 59.3 for the server side.
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <linux/net.h>
#define MY_PORT 6545
#define MAXM 4096
char mesg[MAXM];
main(int argc, char *argv[])
{
int sockfd, newfd;
int cpid; /* child id */
struct sockaddr_in servaddr;
struct sockaddr_in clientInfo;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0) < 0)
{
myabort("Unable to create socket");
}
#ifdef LINUX
opt = 1; len = sizeof(opt);
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);
#endif
bzero((char *)&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_family = htons(MY_PORT);
/*
* The htonl (for a long integer) and htons (for short integer) convert
* a host oriented byte order * into a network order.
*/
if (bind(sockfd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr)) < 0)
{
myabort("Unable to bind socket");
}
for (;;)
{
/* wait here */
n = recvfrom(sockfd, mesg, MAXM, 0,
(struct sockaddr *)&clientInfo,
sizeof(struct sockaddr));
doSomethingToIt(mesg);
sendto(sockfd,mesg,n,0,
(struct sockaddr *)&clientInfo,
sizeof(struct sockaddr));
}
}
As you can see, the two function calls to process each message make this an easier
implementation than a connection-oriented one. You must, however, process each message
one at a time because messages from multiple clients can be multiplexed together.
In a connection-oriented scheme, the child process always knows where each message
originated.
The client does not have to call the connect() system call either. Instead,
the client can call the sendto() function directly. The client side is identical
to the server side, with the exception that the sendto call is made before
the recvfrom()call:
#include <sys/types.h>
#include <sys/socket.h>
int sendto((int sockfd,
const void *message__, /* the pointer to message */
int length, /* of message */
unsigned int type, /* of routing, leave 0 */
const struct sockaddr * client, /* where to send it */
int length ); /* of sockaddr */
-
NOTE: If you are a BSD
user, use the sendto() call, not the sendmsg() call. The sendto()
call is more efficient.
Any errors are indicated by a return value of -1. Only local errors are
detected.
The recvfrom() system call is defined as shown here:
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int sockfd,
const void *message__, /* the pointer to message */
int length, /* of message */
unsigned int flags, /* of routing, leave 0 */
const struct sockaddr * client, /* where to send it */
int length ); /* of sockaddr */
If a message is too long to fit in the supplied buffer, the extra bytes are discarded.
The call might return immediately or wait forever, depending on the type of the flag
being set. You can even set timeout values. Check the man pages for recvfrom
for more information.
There you have it--the very basics of how to program applications to take advantage
of the networking capabilities under Linux. We have not even scratched the surface
of all the intricacies of programming for networks. A good starting point for more
detailed information would be UNIX Network Programming, by W. Richard Stevens (Prentice
Hall, 1990). This book, a classic, is used in most universities and is by far the
most detailed book to date.
When two processes want to share a file, the danger exists that one process might
affect the contents of the file, and thereby affect the other process. For this reason,
most operating systems use a mutually exclusive principle: when one process has a
file open, no other process can touch it. This is called file locking.
This technique is simple to implement. What usually happens is that a "lock
file" is created with the same name as the original file but with the extension
.lock, which tells other processes that the file is unavailable. This is
how many Linux spoolers, such as the print system and UUCP, implement file locking.
It is a brute-force method, perhaps, but effective and easy to program.
Unfortunately, this technique is not good when you must have several processes
access the same information quickly, because the delays waiting for file opening
and closing can grow to be appreciable. Also, if one process doesn't release the
file properly, other processes can hang there, waiting for access.
For this reason, record locking is sometimes implemented. With record locking,
a single part of a larger file is locked to prevent two processes from changing its
contents at the same time. Record locking enables many processes to access the same
file at the same time, each updating different records within the file, if necessary.
The programming necessary to implement record locking is more complex than that for
file locking, of course.
Normally, to implement record locking, you use a file offset, or the number of
characters from the beginning of the file. In most cases, a range of characters is
locked; the program has to note the start of the locking region and the length of
it, and then store that information where other processes can examine it.
Writing either file-locking or record-locking code requires a good understanding
of the operating system but is otherwise not difficult, especially because thousands
of programs are readily available from the Internet, in networking programming books,
and on BBSs to examine for sample code.
Network programming always involves two or more processes talking to each other
(interprocess communications), so the way in which processes communicate is vitally
important to network programmers. Network programming differs from the usual method
of programming in a few important aspects. A traditional program can talk to different
modules (or even other applications on the same machine) through global variables
and function calls. That doesn't work across networks.
A key goal of network programming is to ensure that processes don't interfere
with each other. Otherwise, systems can get bogged down or can lock up. Therefore,
processes must have a clean and efficient method of communicating. UNIX is particularly
strong in this regard, because many of the basic UNIX capabilities, such as pipes
and queues, are used effectively across networks.
Writing code for interprocess communications is quite difficult compared to single
application coding. If you want to write this type of routine, you should study sample
programs from a network programming book or a BBS site to see how this task is accomplished.
Few people need to write network applications, so the details of the process are
best left to those who want them. Experience and lots of examples are the best way
to begin writing network code, and mastering the skills can take many years.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.