全部博文(132)
分类: LINUX
2010-04-13 13:35:32
The steps involved in establishing a socket on
the server side are as follows:
When a socket is created, the program has to specify the address domain and the socket type. Two processes can communicate with each other only if their sockets are of the same type and in the same domain.
There are two widely used address domains, the unix domain, in which two processes which share a common file system communicate, and the Internet domain, in which two processes running on any two hosts on the Internet communicate. Each of these has its own address format.
The address of a socket in the Unix domain is a character string which is basically an entry in the file system.
The address of a socket in the Internet domain consists of
the Internet address of the host machine (every computer on the
Internet has a unique 32 bit address, often referred to as its IP
address).
In addition, each socket needs a port number on that host.
Port
numbers are 16 bit unsigned integers.
The lower numbers are
reserved in Unix for standard services. For example, the port number
for the FTP server is 21. It is important that standard services be at
the same port on all computers so that clients will know their
addresses.
However, port numbers above 2000 are generally available.
There are two widely used socket types, stream sockets, and datagram sockets. Stream sockets treat communications as a continuous stream of characters, while datagram sockets have to read entire messages at once. Each uses its own communciations protocol.
Stream sockets use TCP (Transmission Control Protocol), which is a reliable, stream oriented protocol, and datagram sockets use UDP (Unix Datagram Protocol), which is unreliable and message oriented.
The examples in this tutorial will use sockets in the Internet domain using the TCP protocol.
C code for a very simple client and server are provided for you. These communicate using stream sockets in the Internet domain. The code is described in detail below. However, before you read the descriptions and look at the code, you should compile and run the two programs to see what they do.
Download
these into files called server.c
and client.c
and compile them separately into two executables called server
and client
.
They probably won't require any special
compiling flags, but on some solaris systems you may need to link to the
socket library by appending -lsocket
to your compile
command.
Ideally, you should run the client and the server on
separate hosts on the Internet. Start the server first. Suppose the
server is running on a machine called cheerios
. When you
run the server, you need to pass the port number in as an argument. You
can choose any number between 2000 and 65535. If this port is already in
use on that machine, the server will tell you this and exit. If this
happens, just choose another port and try again. If the port is
available, the server will block until it receives a connection from the
client. Don't be alarmed if the server doesn't do anything;
It's
not supposed to do anything until a connection is made.
Here
is a typical command line:
server 51717
To run the client you need to pass in two arguments, the name of the host on which the server is running and the port number on which the server is listening for connections.
Here is the command line to connect to the server described above:
client cheerios 51717
You
can simulate this on a single machine by running the server in one
window and the client in another. In this case, you can use the keyword localhost
as the first argument to the client.
The server code uses a number of ugly programming constructs, and so we will go through it line by line.
#include
This
header file contains declarations used in most input and output and is
typically included in all C programs.
#include
This header file
contains definitions of a number of data types used in system calls.
These types are used in the next two include files.
#include
The header file
socket.h includes a number of definitions of structures needed for
sockets.
#include
The
header file in.h contains constants and structures needed for internet
domain addresses.
void error(char *msg)
{
perror(msg);
exit(1);
}
stderr
and then aborts the program. The
gives more information.int main(int argc, char *argv[])
{
int sockfd, newsockfd, portno, clilen, n;
sockfd
and newsockfd
are file descriptors, i.e. array subscripts
into the . These two variables store the values returned by
the socket system call and the accept system call.portno
stores the port number on which the server accepts connections.
clilen
stores the size of the address of the client. This is needed for the
accept system call.
n
is the return value for the read()
and write()
calls; i.e. it contains the number of
characters read or written.
char buffer[256];
struct sockaddr_in serv_addr, cli_addr;
sockaddr_in
is a structure containing an internet address.
This structure is defined in netinet/in.h
.
Here
is the definition:
struct sockaddr_in
{
short sin_family; /* must be AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8]; /* Not used, must be zero */
};
in_addr
structure, defined in the same header file,
contains only one field, a unsigned long called s_addr
.The
variable serv_addr
will contain the address of the server,
and cli_addr
will contain the address of the client which
connects to the server.
if (argc < 2)
{
fprintf(stderr,"ERROR, no port provided
");
exit(1);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
error("ERROR opening socket");
socket()
system call creates a new socket. It takes three
arguments. The first is the address domain of the socket.Recall that
there are two possible address domains, the unix domain for two
processes which share a common file system, and the Internet domain for
any two hosts on the Internet. The symbol constant AF_UNIX
is used for the former, and AF_INET
for the latter (there
are actually many other options which can be used here for specialized
purposes).
The second argument is the type of socket. Recall that
there are two choices here, a stream socket in which characters are
read in a continuous stream as if from a file or pipe, and a datagram
socket, in which messages are read in chunks. The two symbolic constants
are SOCK_STREAM
and SOCK_DGRAM
.
The third argument is the protocol. If this argument is zero (and it always should be except for unusual circumstances), the operating system will choose the most appropriate protocol. It will choose TCP for stream sockets and UDP for datagram sockets.
The socket system call
returns an entry into the file descriptor table (i.e. a small integer).
This value is used for all subsequent references to this socket. If the
socket call fails, it returns -1.
In this case the
program displays and error message and exits. However, this system call
is unlikely to fail.
This is a simplified description of the socket call; there are numerous other choices for domains and types, but these are the most common. The has more information.
bzero((char *) &serv_addr, sizeof(serv_addr));
bzero()
sets all values in a buffer to zero. It
takes two arguments, the first is a pointer to the buffer and the second
is the size of the buffer. Thus, this line initializes serv_addr
to zeros. ----portno = atoi(argv[1]);
atoi()
function to
convert this from a string of digits to an integer.serv_addr.sin_family = AF_INET;
serv_addr
is a structure of type struct
sockaddr_in
. This structure has four fields. The first field is short
sin_family
, which contains a code for the address family. It
should always be set to the symbolic constant AF_INET
.serv_addr.sin_port = htons(portno);
serv_addr
is unsigned short sin_port
,
which contain the port number. However, instead of simply copying the
port number to this field, it is necessary to convert this to using the function htons()
which converts a port
number in host byte order to a port number in network byte order.serv_addr.sin_addr.s_addr = INADDR_ANY;
sockaddr_in
is a structure of type struct
in_addr
which contains only a single field unsigned long
s_addr
. This field contains the IP address of the host. For
server code, this will always be the IP address of the machine on which
the server is running, and there is a symbolic constant INADDR_ANY
which gets this address.if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
error("ERROR on binding");
bind()
system call binds a socket to an address, in this
case the address of the current host and port number on which the server
will run. It takes three arguments, the socket file descriptor, the
address to which is bound, and the size of the address to which it is
bound. The second argument is a pointer to a structure of type sockaddr
,
but what is passed in is a structure of type sockaddr_in
,
and so this must be cast to the correct type. This can fail for a
number of reasons, the most obvious being that this socket is already in
use on this machine. The has
more information.listen(sockfd,5);
listen
system call allows the process to listen on the socket for connections.
The first argument is the socket file descriptor, and the second is
the size of the backlog queue, i.e., the number of connections that can
be waiting while the process is handling a particular connection. This
should be set to 5, the maximum size permitted by most systems. If the
first argument is a valid socket, this call cannot fail, and so the code
doesn't check for errors. The
has more information.clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
error("ERROR on accept");
accept()
system call causes the process to block until a
client connects to the server. Thus, it wakes up the process when a
connection from a client has been successfully established. It returns a
new file descriptor, and all communication on this connection should be
done using the new file descriptor. The second argument is a reference
pointer to the address of the client on the other end of the connection,
and the third argument is the size of this structure. The
has more information.bzero(buffer,256);
n = read(newsockfd,buffer,255);
if (n < 0) error("ERROR reading from socket");
printf("Here is the message: %s
",buffer);
bzero()
function, and then reads from the socket. Note that the read call uses
the new file descriptor, the one returned by accept()
, not
the original file descriptor returned by socket()
. Note
also that the read()
will block until there is something
for it to read in the socket, i.e. after the client has executed a write()
.It will read either the total number of characters in the socket or 255, whichever is less, and return the number of characters read. The has more information.
n = write(newsockfd,"I got your message",18);
if (n < 0) error("ERROR writing to socket");
return 0;
}
client.c
line by
line. #include
#include
#include
#include
#include
netdb.h
defines the structure hostent
, which
will be used below.void error(char *msg)
{
perror(msg);
exit(0);
}
int main(int argc, char *argv[])
{
int sockfd, portno, n;
struct sockaddr_in serv_addr;
struct hostent *server;
error()
function is identical to that in the server, as
are the variables sockfd, portno
, and n
. The
variable serv_addr
will contain the address of the server
to which we want to connect. It is of type .The
variable server
is a pointer to a structure of type hostent
.
This structure is defined in the header file netdb.h
as
follows:
struct hostent
{
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses from name server */
#define h_addr h_addr_list[0] /* address, for backward compatiblity */
};
h_name Official name of the host.
h_aliases A zero terminated array of alternate
names for the host.
h_addrtype The type of address being returned;
currently always AF_INET.
h_length The length, in bytes, of the address.
h_addr_list A pointer to a list of network addresses
for the named host. Host addresses are
returned in network byte order.
h_addr
is an alias for the first address in the array
of network addresses.char buffer[256];
if (argc < 3)
{
fprintf(stderr,"usage %s hostname port
", argv[0]);
exit(0);
}
portno = atoi(argv[2]);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
error("ERROR opening socket");
server = gethostbyname(argv[1]);
if (server == NULL)
{
fprintf(stderr,"ERROR, no such host
");
exit(0);
}
argv
[1] contains the name of a host on the
Internet, e.g. cs.rpi.edu
. The function:struct hostent *gethostbyname(char *name)
hostent
containing information about that host.The field char *h_addr
contains the IP address.
If this structure is
NULL, the system could not locate a host with this name.
In the
old days, this function worked by searching a system file called /etc/hosts
but with the explosive growth of the Internet, it became impossible for
system administrators to keep this file current. Thus, the mechanism
by which this function works is complex, often involves querying large
databases all around the country. The has more information.
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr,
(char *)&serv_addr.sin_addr.s_addr,
server->h_length);
serv_addr.sin_port = htons(portno);
serv_addr
. Much of it is the same
as in the server. However, because the field server->h_addr
is a character string, we use the function:void bcopy(char *s1, char *s2, int length)
length
bytes from s1
to s2
.
----if (connect(sockfd,&serv_addr,sizeof(serv_addr)) < 0)
error("ERROR connecting");
connect
function is called by the client to establish a
connection to the server. It takes three arguments, the socket file
descriptor, the address of the host to which it wants to connect
(including the port number), and the size of this address. This
function returns 0 on success and -1 if it fails. The
has more information.Notice that the client needs to know the port
number of the server, but it does not need to know its own port number.
This is typically assigned by the system when connect
is
called.
printf("Please enter the message: ");
bzero(buffer,256);
fgets(buffer,255,stdin);
n = write(sockfd,buffer,strlen(buffer));
if (n < 0)
error("ERROR writing to socket");
bzero(buffer,256);
n = read(sockfd,buffer,255);
if (n < 0)
error("ERROR reading from socket");
printf("%s
",buffer);
return 0;
}
fgets
to read the message from stdin, writes
the message to the socket, reads the reply from the socket, and displays
this reply on the screen.The sample server code above has the limitation that it only handles one connection, and then dies. A "real world" server should run indefinitely and should have the capability of handling a number of simultaneous connections, each in its own process. This is typically done by forking off a new process to handle each new connection.
The following code has a dummy function called dostuff(int
sockfd)
.
This function will handle the
connection after it has been established and provide whatever services
the client requests. As we saw above, once a connection is established,
both ends can use read
and write
to send
information to the other end, and the details of the information passed
back and forth do not concern us here.
To write a
"real world" server, you would make essentially no changes to the main()
function, and all of the code which provided the service would be in dostuff()
.
To
allow the server to handle multiple simultaneous connections, we make
the following changes to the code:
fork()
#### to create a
new process.sockfd
####
and call #dostuff
#####, passing the new socket file
descriptor as an argument. When the two processes have completed their
conversation, as indicated by dostuff()
#### returning, this
process simply exits.newsockfd
####.
Because all of this code is in an infinite loop, it will return to the
accept statement to wait for the next connection.Here is the code.
while (1)
{
newsockfd = accept(sockfd,
(struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
error("ERROR on accept");
pid = fork();
if (pid < 0)
error("ERROR on fork");
if (pid == 0)
{
close(sockfd);
dostuff(newsockfd);
exit(0);
}
else
close(newsockfd);
} /* end of while */
The
above code has a problem; if the parent runs for a long time and
accepts many connections, each of these connections will create a zombie
when the connection is terminated. A zombie is a process which has
terminated but but cannot be permitted to fully die because at some
point in the future, the parent of the process might execute a wait
and would want information about the death of the child. Zombies clog
up the process table in the kernel, and so they should be prevented.
Unfortunately, the code which prevents zombies is not consistent across
different architectures. When a child dies, it sends a SIGCHLD signal to
its parent. On systems such as AIX, the following code in main()
is all that is needed.
signal(SIGCHLD,SIG_IGN);
void *SigCatcher(int n)
{
wait3(NULL,WNOHANG,NULL);
}
...
int main()
{
...
signal(SIGCHLD,SigCatcher);
...
SigCatcher()
will be called whenever the parent
receives a SIGCHLD signal (i.e. whenever a child dies). This will in
turn call wait3
which will receive the signal. The WNOHANG
flag is set, which causes this to be a non-blocking wait (one of my
favorite ).This
example showed a stream socket in the Internet domain. This is the
most common type of connection. A second type of connection is a
datagram socket. You might want to use a datagram socket in cases where
there is only one message being sent from the client to the server, and
only one message being sent back. There are several differences between
a datagram socket and a stream socket.
sendto()
#### and receivefrom()
####
rather than the more generic read()
#### and write()
####.for the server code using a datagram socket.
for the client code using a datagram socket.
These two programs can be compiled and run in exactly the same way as the server and client using a stream socket.
Most of the server code is similar to the stream socket code. Here are the differences.
sock=socket(AF_INET, SOCK_DGRAM, 0);
fromlen = sizeof(struct sockaddr_in);
while (1)
{
n = recvfrom(sock,buf,1024,0,(struct sockaddr *)&from,&fromlen);
if (n < 0) error("recvfrom");
listen()
or the accept()
system calls. After a socket has been bound to an address, the program
calls recvfrom()
to read a message. This call will block
until a message is received. The recvfrom()
system call
takes six arguments. The first three are the same as those for the
read()
call, the socket file descriptor, the buffer into which
the message will be read, and the maximum number of bytes. The fourth
argument is an integer argument for flags. This is ordinarily set to
zero. The fifth argument is a pointer to a .
When the call returns, the values of this structure will have been
filled in for the other end of the connection (the client). The size of
this structure will be in the last argument, a pointer to an integer.
This call returns the number of bytes in the message. (or -1 on an
error condition). The
has more information.n = sendto(sock,"Got your message
",17,
0,(struct sockaddr *) &from,fromlen);
if (n < 0)
error("sendto");
}
}
sendto()
is used. This also
takes six arguments. The first three are the same as for a write()
call, the socket file descriptor, the buffer from which the message
will be written, and the number of bytes to write. The fourth argument
is an int argument called flags, which is normally zero. The fifth
argument is a pointer to a sockadd_in
structure. This will
contain the address to which the message will be sent. Notice that in
this case, since the server is replying to a message, the values of this
structure were provided by the recvfrom call. The last argument is the
size of this structure. Note that this is not a pointer to an int, but
an int value itself. The
has more information.The client code for a datagram socket client is
the same as that for a stream socket with the following differences.
connect()
****
system callread
**** and write
****,
the client uses recvfrom
**** and sendto
****
which are described in detail above. Here is the code for a client and server which communicate using a stream socket in the Unix domain.
The only difference between a socket in the Unix domain and a socket in the Internet domain is the form of the address. Here is the address structure for a Unix Domain address, defined in .
struct sockaddr_un
{
short sun_family; /* AF_UNIX */
char sun_path[108]; /* path name (gag) */
};
sun_path
has the form of a path name in the Unix
file system. This means that both client and server have to be running
the same file system. Once a socket has been created, it remain until it
is explicitly deleted, and its name will appear with the ls
command, always with a size of zero. Sockets in the Unix domain are
virtually identical to named pipes (FIFOs).There are a number of different ways to design servers. These models are discussed in detail in a book by Douglas E. Comer and David L. Stevens entiteld Internetworking with TCP/IP Volume III:Client Server Programming and Applications published by in 1996. These are summarized here.
Concurrent, connection oriented servers
The
typical server in the Internet domain creates a stream socket and forks
off a process to handle each new connection that it receives. This
model is appropriate for services which will do a good deal of reading
and writing over an extended period of time, such as a telnet server or
an ftp server. This model has relatively high overhead, because forking
off a new process is a time consuming operation, and because a stream
socket which uses the TCP protocol has high kernel overhead, not only in
establishing the connection but also in transmitting information.
However, once the connection has been established, data transmission is
reliable in both directions.
Iterative, connectionless servers
Servers
which provide only a single message to the client often do not involve
forking, and often use a datagram socket rather than a stream socket.
Examples include a finger daemon or a timeofday server or an echo server
(a server which merely echoes a message sent by the client). These
servers handle each message as it receives them in the same process.
There is much less overhead with this type of server, but the
communication is unreliable. A request or a reply may get lost in the
Internet, and there is no built-in mechanism to detect and handle this.
Single
Process concurrent servers
A server which
needs the capability of handling several clients simultaneous, but where
each connection is I/O dominated (i.e. the server spends most of its
time blocked waiting for a message from the client) is a candidate for a
single process, concurrent server. In this model, one process
maintains a number of open connections, and listens at each for a
message. Whenever it gets a message from a client, it replies quickly
and then listens for the next one. This type of service can be done
with
the select
system call.
========================================================
from:
by David Mertz, Ph.D.
(see also of this tutorial)
IP sockets are the lowest level layer upon which high level internet protocols are built--every thing from HTTP, to SSL, to POP3, to Kerberos, to UDP-Time. To implement custom protocols, or to customize implementation of well-known protocols, a programmer needs a working knowledge of the basic socket infrastructure. A similar API is available in many languages; this tutorial focuses primarily on C programming, but it also uses Python as a representative higher-level language for examples.
Readers of this tutorial will be introduced to the basics of programming custom network tools using the cross-platform Berkeley Sockets Interface. Almost all network tools in Linux and other Unix-based operating systems rely on this interface.
This tutorial requires a minimal level of knowledge of C, and ideally of Python also (mostly for part two). However, readers who are not familiar with either programming language should be able to make it through with a bit of extra effort; most of the underlying concepts will apply equally to other programming languages, and calls will be quite similar in most high-level scripting languages like Ruby, Perl, TCL, etc.
While this tutorial introduces the basic concepts behind IP (internet protocol) networks, it certainly does not hurt readers to have some prior acquaintance with the concept of network protocols and layers.
David Mertz is a writer, a programmer, and a teacher, who always endeavors to improve his communication to readers (and tutorial takers). He welcomes any comments, please direct them to .
David also wrote the book which readers can read online at
What we usually call a computer network is composed of a number of , each providing a different restriction and/or guarantee about the data at that layer. The protocols at each network layer generally have their own packet formats, headers, and layout.
The seven traditional layers of a network are divided into two groups: upper layers and lower layers. The sockets interface provides a uniform API to the lower layers of a network, and allows you to implement upper layers within your sockets application. Further, application data formats may themselves constitute further layers, e.g. SOAP is built on top of XML, and ebXML may itself utilize SOAP. In any case, anything past layer 4 is outside the scope of this tutorial.
While the sockets interface theoretically allows access to protocol families other than IP, in practice, every network layer you use in your sockets application will use IP. For this tutorial we only look at IPv4; in the future IPv6 will become important also, but the principles are the same. At the transport layer, sockets support two specific protocols: TCP (transmission control protocol) and UDP (user datagram protocol).
Sockets cannot be used to access lower (or higher) network layers; for example, a socket application does not know whether it is running over ethernet, token ring, or a dialup connection. Nor does the sockets pseudo-layer know anything about higher-level protocols like NFS, HTTP, FTP, and the like (except in the sense that you might yourself write a sockets application that implements those higher-level protocols).
At times, the sockets interface is not your best choice for a network programming API. Specifically, many excellent libraries exist (in various languages) to use higher-level protocols directly, without having to worry about the details of sockets--the libraries handle those details for you. While there is nothing wrong with writing your own SSH client, for example, there is not need to do so simply to let an application transfer data securely. Lower-level layers than those sockets address fall pretty much in the domain of device driver programming.
As the last panel indicated, when you program a sockets application, you have a choice to make between using TCP and using UDP. Each has its own benefits and disadvantages.
TCP is a stream protocol, while UDP is a datagram protocol. That is to say, TCP establishes a continuous open connection between a client and a server, over which bytes may be written--and correct order guaranteed--for the life of the connection. However, bytes written over TCP have no built-in structure, so higher-level protocols are required to delimit any data records and fields within the transmitted bytestream.
UDP, on the other hand, does not require that any connection be established between client and server, it simply transmits a message between addresses. A nice feature of UDP is that its packets are self-delimiting--each datagram indicates exactly where it begins and ends. A possible disadvantage of UDP, however, is that it provides no guarantee that packets will arrive in-order, or even at all. Higher-level protocols built on top of UDP may, of course, provide handshaking and acknowledgements.
A useful analogy for understanding the difference between TCP and UDP is the difference between a telephone call and posted letters. The telephone call is not active until the caller "rings" the receiver and the receiver picks up. The telephone channel remains alive as long as the parties stay on the call--but they are free to say as much or as little as they wish to during the call. All remarks from either party occur in temporal order. On the other hand, when you send a letter, the post office starts delivery without any assurance the recipient exists, nor any strong guarantee about how long delivery will take. The recipient may receive various letters in a different order than they were sent, and the sender may receive mail interspersed in time with those she sends. Unlike with the USPS, undeliverable mail always goes to the dead letter office, and is not returned to sender.
Beyond the protocol--TCP or UDP--there are two things a peer (a
client or server) needs to know about the machine it
communicates
with: An IP address and a port. An IP address is a 32-bit data
value, usually represented for humans in "dotted quad"
notation,
e.g., 64.41.64.172
. A port is a 16-bit data
value,
usually simply represented as a number less than 65536--most
often
one in the tens or hundreds range. An IP address gets a packet
to a machine, a port lets the machine decide which
process/service (if any) to direct it to. That is a slight
simplification, but the idea is correct.
The above description is almost right, but it misses something.
Most of the time when humans think about an internet host
(peer),
we do not remember a number like 64.41.64.172
,
but
instead a name like gnosis.cx
. To find the IP
address
associated with a particular host name, usually you use a
Domain
Name Server, but sometimes local lookups are used first (often
via
the contents of /etc/hosts
). For this tutorial,
we
will generally just assume an IP address is available, but the
next
panel discusses coding name/address lookups.
The command-line utility nslookup
can be used to
find a host IP address from a symbolic name. Actually, a
number
of common utilities, such as ping
or network
configuration tools, do the same thing in passing. But it is
simple to do the same thing programmatically.
In Python or other very-high-level scripting languages, writing a utility program to find a host IP address is trivial:
#!/usr/bin/env python
"USAGE: nslookup.py"
import socket, sys
print socket.gethostbyname(sys.argv[1])
The trick is using a wrapped version of the same
gethostbyname())
function we also find in C.
Usage
is as simple as:
$ ./nslookup.py gnosis.cx
64.41.64.172
In C, that standard library call gethostbyname()
is used for name lookup. The below is a simple implementation
of
nslookup
as a command-line tool; adapting it
for use
in a larger application is straightforward. Of course, C is a
bit more finicky than Python is.
/* Bare nslookup utility (w/ minimal error checking) */
#include/* stderr, stdout */
#include/* hostent struct, gethostbyname() */
#include/* inet_ntoa() to format IP address */
#include/* in_addr structure */
int main(int argc, char **argv) {
struct hostent *host; /* host information */
struct in_addr h_addr; /* internet address */
if (argc != 2) {
fprintf(stderr, "USAGE: nslookup\n");
exit(1);
}
if ((host = gethostbyname(argv[1])) == NULL) {
fprintf(stderr, "(mini) nslookup failed on '%s'\n", argv[1]);
exit(1);
}
h_addr.s_addr = *((unsigned long *) host->h_addr_list[0]);
fprintf(stdout, "%s\n", inet_ntoa(h_addr));
exit(0);
}
Notice that the returned value from gethostbyname()
is a hostent
structure that describes the names
host.
The member host->h_addr_list
contains a list
of
addresses, each of which is a 32-bit value in "network byte
order"--i.e. the endianness may or may not be machine native
order. In order to convert to dotted-quad form, use the
function
inet_ntoa()
.
My examples for both clients and servers will use one of the
simplest possible applications: one that sends data and
receives
the exact same thing back. In fact, many machines run an "echo
server" for debugging purposes; this is convenient for our
initial
client, since it can be used before we get to the server
portion
(assuming you have a machine with echod
running).
I would like to acknowledge the book TCP/IP Sockets in C by Donahoo and Calvert (see Resources). I have adapted several examples that they present. I recommend the book--but admittedly, echo servers/clients will come early in most presentations of sockets programming.
The steps involved in writing a client application differ slightly between TCP and UDP clients. In both cases, you first create the socket; in the TCP case only, you next establish a connection to the server; next you send some data to the server; then receive data back; perhaps the sending and receiving alternates for a while; finally, in the TCP case, you close the connection.
First we will look at a TCP client, in the second part of the tutorial we will make some adjustments to do (roughly) the same thing with UDP. Let's look at the first few lines--some includes, and creating the socket:
#include
#include
#include
#include
#include
#include
#include
#define BUFFSIZE 32
void Die(char *mess) { perror(mess); exit(1); }
There is not too much to the setup. A particular buffer size is defined, which limits the amount of data echo'd at each pass (but we loop through multiple passes, if needed). . A small error function is also defined.
The arguments to the socket()
call decide the type
of socket: PF_INET
just means it uses IP (which
you
always will); SOCK_STREAM
and IPPROTO_TCP
go together for a TCP socket.
int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in echoserver;
char buffer[BUFFSIZE];
unsigned int echolen;
int received = 0;
if (argc != 4) {
fprintf(stderr, "USAGE: TCPecho\n");
exit(1);
}
/* Create the TCP socket */
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
Die("Failed to create socket");
}
The value returned is a socket handle, which is similar to a file handle; specifically, if the socket creation fails, it will return -1 rather than a positive numbered handle.
Now that we have created a socket handle, we need to establish a
connection with the server. A connection requires a
sockaddr
structure that describes the server.
Specifically, we need to specify the server and port to
connect to
using echoserver.sin_addr.s_addr
and
echoserver.sin_port
. The fact we are using an
IP
address is specified with echoserver.sin_family
,
but
this will always be set to AF_INET
.
/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = inet_addr(argv[1]); /* IP address */
echoserver.sin_port = htons(atoi(argv[3])); /* server port */
/* Establish connection */
if (connect(sock,
(struct sockaddr *) &echoserver,
sizeof(echoserver)) < 0) {
Die("Failed to connect with server");
}
As with creating the socket, the attempt to establish a connection will return -1 if the attempt fails. Otherwise, the socket is now ready to accept sending and receiving data.
Now that the connection is established, we are ready to send
and receive data. A call to send()
takes as
arguments the socket handle itself, the string to send, the
length of the sent string, and a flag argument. Normally the
flag is the default value 0. The return value of the
send()
call is the number of bytes successfully
sent.
/* Send the word to the server */
echolen = strlen(argv[2]);
if (send(sock, argv[2], echolen, 0) != echolen) {
Die("Mismatch in number of sent bytes");
}
/* Receive the word back from the server */
fprintf(stdout, "Received: ");
while (received < echolen) {
int bytes = 0;
if ((bytes = recv(sock, buffer, BUFFSIZE-1, 0)) < 1) {
Die("Failed to receive bytes from server");
}
received += bytes;
buffer[bytes] = '\0'; /* Assure null terminated string */
fprintf(stdout, buffer);
}
The rcv()
call is not guaranteed to get
everything in-transit on a particular call--it simply blocks
until
it gets something . Therefore, we loop until we have
gotten
back as many bytes as were sent, writing each partial string
as we
get it. Obviously, a different protocol might decide when to
terminate receiving bytes in a different manner (perhaps a
delimiter within the bytestream).
Calls to both send()
and recv()
block
by default, but it is possible to change socket options to
allow
non-blocking sockets. However, this tutorial will not cover
details
of creating non-blocking sockets, nor such other details used
in
production servers as forking, threading, or general
asynchronous
processing (built on non-blocking sockets).
At the end of the process, we want to call close()
on the socket, much as we would with a file handle
fprintf(stdout, "\n");
close(sock);
exit(0);
}
A socket server is a bit more complicated than is a client, mostly because a server usually needs to be able to handle multiple client requests. Basically, there are two aspects to a server: handling each established connection, and listening for connections to establish.
In our example, and in most cases, you can split the handling
of a particular connection into support function--which looks
quite a bit like a TCP client application does. We name that
function HandleClient()
.
Listening for new connections is a bit different from client
code. The trick is that the socket you initially create and
bind
to an address and port is not the actually connected socket.
This
initial socket acts more like a socket factory, producing new
connected sockets as needed. This arrangement has an advantage
in
enabling fork'd, threaded, or asynchronously dispatched
handlers
(using select()
); however, for this first
tutorial we
will only handle pending connected sockets in synchronous
order.
Our echo server starts out with pretty much the same few
#include
s as the client did, and defines some
constants and an error handling function:
#include
#include
#include
#include
#include
#include
#include
#define MAXPENDING 5 /* Max connection requests */
#define BUFFSIZE 32
void Die(char *mess) { perror(mess); exit(1); }
The BUFFSIZE
constant limits the data sent per
loop. The MAXPENDING
constant limits the number
of
connections that will be queued at a time (only one will be
serviced at a time in our simple server). The
Die()
function is the same as in our client.
The handler for echo connections is straightforward. All it
does is receive any initial bytes available, then cycle
through
sending back data and receiving more data. For short echo
strings
(particularly if less than BUFFSIZE
) and
typical
connections, only one pass through the while
loop
will occur. But the underlying sockets interface (and TCP/IP)
does not make any guarantees about how the bytestream will be
split between calls to recv()
.
void HandleClient(int sock) {
char buffer[BUFFSIZE];
int received = -1;
/* Receive message */
if ((received = recv(sock, buffer, BUFFSIZE, 0)) < 0) {
Die("Failed to receive initial bytes from client");
}
/* Send bytes and check for more incoming data in loop */
while (received > 0) {
/* Send back received data */
if (send(sock, buffer, received, 0) != received) {
Die("Failed to send bytes to client");
}
/* Check for more data */
if ((received = recv(sock, buffer, BUFFSIZE, 0)) < 0) {
Die("Failed to receive additional bytes from client");
}
}
close(sock);
}
The socket that is passed in to the handler function is one that already connected to the requesting client. Once we are done with echoing all the data, we should close this socket; the parent server socket stays around to spawn new children, like the one just closed.
As we outlined before, creating a socket has a bit different
purpose for a server than for a client. Creating the socket
has
the same syntax it did in the client; but the structure
echoserver
is setup with information about the
server
itself, rather than about the peer it wants to connect to.
You usually want to use the special constant
INADDR_ANY
to enable receiving client request
on any
IP address the server supplies; in principle, such as in a
multi-hosting server, you could specify a particular IP
address
instead.
int main(int argc, char *argv[]) {
int serversock, clientsock;
struct sockaddr_in echoserver, echoclient;
if (argc != 2) {
fprintf(stderr, "USAGE: echoserver\n");
exit(1);
}
/* Create the TCP socket */
if ((serversock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
Die("Failed to create socket");
}
/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = htonl(INADDR_ANY); /* Incoming addr */
echoserver.sin_port = htons(atoi(argv[1])); /* server port */
Notice that both IP address and port are converted to network
byte order for the sockaddr_in
structure. The
reverse
functions to return to native byte order are ntohs()
and ntohl()
. These functions are no-ops on some
platforms, but it is still wise to use them for cross-platform
compatibility.
Whereas the client application connect()
'd to a
server's IP address and port, the server bind()
's to
its own address and port:
/* Bind the server socket */
if (bind(serversock, (struct sockaddr *) &echoserver,
sizeof(echoserver)) < 0) {
Die("Failed to bind the server socket");
}
/* Listen on the server socket */
if (listen(serversock, MAXPENDING) < 0) {
Die("Failed to listen on server socket");
}
Once the server socket is bound, it is ready to
listen()
. As with most socket functions, both
bind()
and listen()
return -1 if
they
have a problem. Once a server socket is listening, it is
ready to
accept()
client connections, acting as a
factory for
sockets on each connection.
Creating new sockets for client connections is the crux of a
server. The function accept()
does two
important
things: it returns a socket pointer for the new socket; and it
populates the sockaddr_in
structure pointed to,
in our case, by echoclient
.
/* Run until cancelled */
while (1) {
unsigned int clientlen = sizeof(echoclient);
/* Wait for client connection */
if ((clientsock =
accept(serversock, (struct sockaddr *) &echoclient,
&clientlen)) < 0) {
Die("Failed to accept client connection");
}
fprintf(stdout, "Client connected: %s\n",
inet_ntoa(echoclient.sin_addr));
HandleClient(clientsock);
}
}
We can see the populated structure in echoclient
with the fprintf()
call that accesses the
client IP
address. The client socket pointer is passed to
HandleClient()
, which we saw at the start of
this
section.
Python's standard module socket
provides almost
exactly the same range of capabilities you would find in C
sockets. However, the interface is generally more flexible,
largely because of the benefits of dynamic typing. Moreover,
an
object-oriented style is also used. For example, once you
create
a socket object, methods like .bind()
,
.connect()
, and .send()
are
methods of
that object, rather than global functions operating on a
socket
pointer.
At a higher level than socket
, the module
SocketServer
provides a framework for writing
servers. This is still relatively low level, and higher-level
interfaces are available for server of higher-level protocols,
e.g. SimpleHTTPServer
, DocXMLRPCServer
,
or CGIHTTPServer
.
Let us just look at the complete client, then make a few remarks:
#!/usr/bin/env python
"USAGE: echoclient.py"
from socket import * # import *, but we'll avoid name conflict
import sys
if len(sys.argv) != 4:
print __doc__
sys.exit(0)
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((sys.argv[1], int(sys.argv[3])))
message = sys.argv[2]
messlen, received = sock.send(message), 0
if messlen != len(message):
print "Failed to send complete message"
print "Received: ",
while received < messlen:
data = sock.recv(32)
sys.stdout.write(data)
received += len(data)
sock.close()
At first brush, we seem to have left out some of the error
catching code from the C version. But since Python raises
descriptive errors for every situation that we checked for in
the
C echo client, we can let the built-in exceptions do our work
for
us. Of course, if we wanted the precise wording of errors
that we
had before, we would have to add a few
try
/ except
clauses around the
calls to
methods of the socket object.
While shorter, the Python client is somewhat more powerful.
Specifically, the address we feed to a .connect()
call can be either a dotted-quad IP address or a symbolic
name,
without need for extra lookup work, e.g.:
$ ./echoclient.py 192.168.2.103 foobar 7
Received: foobar
$ ./echoclient.py fury.gnosis.lan foobar 7
Received: foobar
We also have a choice between the methods .send()
and .sendall()
. The former sends as many bytes
as it
can at once, the latter sends the whole message (or raises an
exception if it cannot). For this client, we indicate if the
whole message was not sent, but proceed with getting back as
much
as actually was sent.
The simplest way to write a echo server in Python is using the
SocketServer
module. It is so easy as to
almost seem
like cheating. In later panels, we will spell out the
lower-level
version, that follows the C implementation. For now, let us
see
just how quick it can be:
#!/usr/bin/env python
"USAGE: echoserver.py"
from SocketServer import BaseRequestHandler, TCPServer
import sys, socket
class EchoHandler(BaseRequestHandler):
def handle(self):
print "Client connected:", self.client_address
self.request.sendall(self.request.recv(2**16))
self.request.close()
if len(sys.argv) != 2:
print __doc__
else:
TCPServer(('',int(sys.argv[1])), EchoHandler).serve_forever()
The only thing we need to provide is a child of
SocketServer.BaseRequestHandler
that has a
.handle()
method. The self
instance has
some useful attributes, such as .client_address
,
and
.request
which is itself a connected socket
object.
If we wish to do it "the hard way"--and gain a bit more fine-tuned control, we can write almost exactly our C echo server in Python (but in fewer lines):
#!/usr/bin/env python
"USAGE: echoserver.py"
from socket import * # import *, but we'll avoid name conflict
import sys
def handleClient(sock):
data = sock.recv(32)
while data:
sock.sendall(data)
data = sock.recv(32)
sock.close()
if len(sys.argv) != 2:
print __doc__
else:
sock = socket(AF_INET, SOCK_STREAM)
sock.bind(('',int(sys.argv[1])))
sock.listen(5)
while 1: # Run until cancelled
newsock, client_addr = sock.accept()
print "Client connected:", client_addr
handleClient(newsock)
In truth, this "hard way" still isn't very hard. But as in the
C implementation, we manufacture new connected sockets using
.listen()
, and call our handler for each such
connection.
The server and client presented in this tutorial are simple,
but they show everything essential to writing TCP sockets
applications. If the data transmitted is more complicated, or
the
interaction between peers (client and server) is ore
sophisticated
in your application, that is just a matter of additional
application programming. The data exchanged will still follow
the
same pattern of connect()
and bind()
,
then send()
's and recv()
's.
One thing this tutorial did not get to, except in brief summary at the start, is usage of UDP sockets. TCP is more common, but it is important to also understand UDP sockets as an option for your application. Part two of this tutorial will look at UDP, as well as implementing sockets applications in Python, and some other intermediate topics.
A good introduction to sockets programming in C, is Michael J. Donahoo and Kenneth L. Calvert, TCP/IP Sockets in C , Morgan-Kaufmann, 2001; ISBN: 1-55860-826-5.
Please let us know whether this tutorial was helpful to you and how we could make it better. We'd also like to hear about other tutorial topics you'd like to see covered.
For questions about the content of this tutorial, contact the
author, David Mertz, at .
by David Mertz, Ph.D.
(see also of this tutorial)
IP sockets are the lowest level layer upon which high level internet protocols are built--every thing from HTTP, to SSL, to POP3, to Kerberos, to UDP-Time. To implement custom protocols, or to customize implementation of well-known protocols, a programmer needs a working knowledge of the basic socket infrastructure. A similar API is available in many languages; this tutorial uses C programming as a ubiquitous low-level language, and Python as a representative higher-level language for examples.
Readers of the first part of this tutorial were introduced to the basics of programming custom network tools using the widespread and cross-platform Berkeley Sockets Interface. This tutorial picks up with further explanation of User Datagram Protocol (UDP), and continues with discussion of writing scalable socket servers.
This tutorial is best suited for readers with a minimal level of knowledge of C and Python . However, readers who are not familiar with either programming language should be able to make it through with a bit of extra effort; most of the underlying concepts will apply equally to other programming languages, and calls will be quite similar in most high-level scripting languages like Ruby, Perl, TCL, etc.
While this tutorial introduces the basic concepts behind IP (internet protocol) networks, it certainly does not hurt readers to have some prior acquaintance with the concept of network protocols and layers.
David Mertz is a writer, a programmer, and a teacher, who always endeavors to improve his communication to readers (and tutorial takers). He welcomes any comments, please direct them to .
David also wrote the book which readers can read online at
The next couple panels contain a quick recap of the discussion in part I of this tutorial--if you already read that, you can skip forward through the next few panels.
A computer network is composed of a number of , each providing a different restriction and/or guarantee about the data at that layer. The protocols at each network layer generally have their own packet formats, headers, and layout.
The seven traditional layers of a network are divided into two groups: upper layers and lower layers. The sockets interface provides a uniform API to the lower layers of a network, and allows you to implement upper layers within your sockets application. Further, application data formats may themselves constitute further layers.
While the sockets interface theoretically allows access to protocol families other than IP, in practice, every network layer you use in your sockets application will use IP. For this tutorial we only look at IPv4; in the future IPv6 will become important also, but the principles are the same. At the transport layer, sockets support two specific protocols: TCP (transmission control protocol) and UDP (user datagram protocol).
Sockets cannot be used to access lower (or higher) network layers; for example, a socket application does not know whether it is running over ethernet, token ring, 802.11b, or a dialup connection. Nor does the sockets pseudo-layer know anything about higher-level protocols like NFS, HTTP, FTP, and the like (except in the sense that you might yourself write a sockets application that implements those higher-level protocols).
At times, the sockets interface is not your best choice for a network programming API. Many excellent libraries exist (in various languages) to use higher-level protocols directly, without having to worry about the details of sockets. While there is nothing wrong with writing you own SSH client, for example, there is not need to do so simply to let an application transfer data securely. Lower-level layers than those sockets address fall pretty much in the domain of device driver programming.
As the last panel indicated, when you program a sockets application, you have a choice to make between using TCP and using UDP. Each has its own benefits and disadvantages.
TCP is a stream protocol, while UDP is a datagram protocol. That is to say, TCP establishes a continuous open connection between a client and a server, over which bytes may be written--and correct order guaranteed--for the life of the connection. However, bytes written over TCP have no built-in structure, so higher-level protocols are required to delimit any data records and fields within the transmitted bytestream.
UDP, on the other hand, does not require that any connection be established between client and server, it simply transmits a message between addresses. A nice feature of UDP is that its packets are self-delimiting--each datagram indicates exactly where it begins and ends. UDP, however, provides no guarantee that packets will arrive in-order, or even at all. Higher-level protocols built on top of UDP may, of course, provide handshaking and acknowledgements.
A useful analogy for understanding the difference between TCP and UDP is the difference between a telephone call and posted letters. The telephone call is not active until the caller "rings" the receiver and the receiver picks up. On the other hand, when you send a letter, the post office starts delivery without any assurance the recipient exists, nor any strong guarantee about how long delivery will take. The recipient may receive various letters in a different order than they were sent, and the sender may receive mail interspersed in time with those she sends. Unlike with the USPS, undeliverable mail always goes to the dead letter office, and is not returned to sender.
Beyond the protocol--TCP or UDP--there are two things a peer (a
client or server) needs to know about the machine it
communicates
with: An IP address and a port. An IP address is a 32-bit data
value, usually represented for humans in "dotted quad"
notation,
e.g., 64.41.64.172
. A port is a 16-bit data
value,
usually simply represented as a number less than 65536--most
often
one in the tens or hundreds range. An IP address gets a packet
to a machine, a port lets the machine decide which
process/service (if any) to direct it to. That is a slight
simplification, but the idea is correct.
The above description is almost right, but it misses something.
Most of the time when humans think about an internet host
(peer),
we do not remember a number like 64.41.64.172
,
but
instead a name like gnosis.cx
. The first part
of
this tutorial demonstrated the use of DNS and local lookups to
find IP addresses from domain names.
As in the first part of this tutorial, my examples for both
clients and servers will use one of the simplest possible
applications: one that sends data and receives the exact same
thing back. In fact, many machines run an "echo server" for
debugging purposes; this is convenient for our initial client,
since it can be used before we get to the server portion
(assuming you have a machine with echod
running).
I would like to acknowledge the book TCP/IP Sockets in C by Donahoo and Calvert (see Resources). I have adapted several examples that they present. I recommend the book--but admittedly, echo servers/clients will come early in most presentations of sockets programming.
Readers of the first part of the tutorial have already seen a TCP echo client in detail. So let us jump into a similar client based on UDP istead.
We will get to clients and servers in C below. But it is
easier to start with far less verbose versions in Python, and
see
the overall structure. The first thing we need before we can
test
a client UDPecho
application is to get a server
running, for the client to talk to. Python, in fact, gives us
the
high-level SocketServer
module that lets us
write
socket servers with minimal customization needed:
#!/usr/bin/env python
"USAGE: %s"
from SocketServer import DatagramRequestHandler, UDPServer
from sys import argv
class EchoHandler(DatagramRequestHandler):
def handle(self):
print "Client connected:", self.client_address
message = self.rfile.read()
self.wfile.write(message)
if len(argv) != 2:
print __doc__ % argv[0]
else:
UDPServer(('',int(argv[1])), EchoHandler).serve_forever()
The various specialized SocketServer
classes all
require you to provide an appropriate .handle()
method.
But in the case of DatagramRequestHandler
, you
get
convenient pseudo-files self.rfile
and
self.wfile
to read and write, respectively,
from the
connecting client.
Writing a Python client generally involves starting with the
basic socket
module. Fortunately, it is so
easy to
write the client that there would hardly be any purpose in
using a
higher-level starting point. Note, however, that frameworks
like
Twisted include base classes for these sorts of tasks, almost
as a
passing thought. Let us look at a socket
based
UDP
echo client:
#!/usr/bin/env python
"USAGE: %s"
from socket import * # import *, but we'll avoid name conflict
from sys import argv, exit
if len(argv) != 4:
print __doc__ % argv[0]
exit(0)
sock = socket(AF_INET, SOCK_DGRAM)
messout = argv[2]
sock.sendto(messout, (argv[1], int(argv[3])))
messin, server = sock.recvfrom(255)
if messin != messout:
print "Failed to receive identical message"
print "Received:", messin
sock.close()
If you happen to recall the TCP echo client from the first
part, you will notice a few differences here. The socket
created
in this case is of type SOCK_DGRAM
rather than
SOCK_STREAM
. But more interesting is the connectionless
nature of UDP. Rather than make a connection and call the .send()
and .recv()
method repeatedly until the
transmission is complete, for UDP we use just one
.sendto()
and one .recvfrom()
to
send
and fetch a message (a datagram).
Since there is no connection involved, you need to pass the
destination address as part of the .sendto()
call. In
Python, the socket object keeps track of the temporary socket
number over which the message actually passes. We will see
later
that in C you will need to use this number from a variable
returned by sendto()
Running the server and the client are straightforward. The server is launched with a port number:
$ ./UDPechoserver.py 7 &
[1] 23369
The client gets three arguments: server address, string to echo, and the port. Being that Python wraps up more in its standard modules than do roughly equivalent C libraries, you can specify a named address just as well as an IP address. In C you would need to perform a lookup yourself, perhaps first testing whether the argument looked like a dotted quad or a domain name:
$ ./UDPechoclient.py
USAGE: ./UDPechoclient.py
$ ./UDPechoclient.py 127.0.0.1 foobar 7
Client connected: ('127.0.0.1', 51776)
Received: foobar
$ ./UDPechoclient.py localhost foobar 7
Client connected: ('127.0.0.1', 51777)
Received: foobar
There is something else interesting to notice in this client
session. Of course, since I launched the server and client in
the
same terminal, the output of both are interspersed. But more
interesting is the client_address
that is
echo'd.
Each new connection establishes a new socket number (they
could be
reused, but the point is you do not know in advance). Port 7
is
merely used to recognize the request to send a message, a new
ad hoc socket is used for the actual data.
It does not take any more lines of code to write a Python UDP
server using the socket
module than it did with
SocketServer
, but the coding style is much more
imperative (and C-like, actually):
#!/usr/bin/env python
"USAGE: %s"
from socket import * # import *, but we'll avoid name conflict
from sys import argv
if len(argv) != 2:
print __doc__ % argv[0]
else:
sock = socket(AF_INET, SOCK_DGRAM)
sock.bind(('',int(argv[1])))
while 1: # Run until cancelled
message, client = sock.recvfrom(256) # <=256 byte datagram
print "Client connected:", client
sock.sendto(message, client)
Usage and behavior is exactly the same as the prior
UDPechoserver.py
, but we manage the loop and
the
client connections ourselves rather than having a class take
care
of it for us. As before ad hoc ports are used to
transmit
the actual message--the client
returned from
sock.recvfrom()
contains the temporary port
number:
$ ./UDPechoserver2.py 8 &
[2] 23428
$ ./UDPechoclient.py localhost foobar 8
Client connected: ('127.0.0.1', 51779)
Received: foobar
The first few lines of our UDP client are identical to those for the TCP client. Mostly we just use some includes for socket functions, or other basic I/O functions.
#include
#include
#include
#include
#include
#include
#include
#define BUFFSIZE 255
void Die(char *mess) { perror(mess); exit(1); }
There is not too much to the setup. It is worth noticing that the buffer size we allocate is much larger than it was in the TCP version (but still finite in size). TCP can loop through the pending data, sending a bit more over an open socket on each loop. For this UDP version, we want a buffer that is large enough to hold the entire message, which we send in a single datagram (it can be smaller than 255, but not any larger). A small error function is also defined.
At the very start of the main()
function we
allocate two sockaddr_in
structures, a few
integers
to hold string sizes, another int for the socket handle, and a
buffer to hold the returned string. After that, we check that
the command-line arguments look mostly correct.
int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in echoserver;
struct sockaddr_in echoclient;
char buffer[BUFFSIZE];
unsigned int echolen, clientlen;
int received = 0;
if (argc != 4) {
fprintf(stderr, "USAGE: %s\n", argv[0]);
exit(1);
}
A contrast with the Python code comes up already here. For
this C client, you must use a dotted-quad IP address.
In
Python, all the socket
module functions handle
name
resolution behind the scenes. If you wanted to do a lookup in
the
C client, you would need to program a DNS function--such as
the
one presented in the first part of this tutorial.
In fact, it would not be a terrible idea to check that the IP address passed in as the server IP address really looks like a dotted-quad address. If you forgetfully pass in a named address, you will probably receive the somewhat misleading error: "Mismatch in number of sent bytes: No route to host" (see below for where this message is produced). Any named address amounts to the same thing as an unused or reserved IP address (which a simple pattern check could not rule out, of course).
The arguments to the socket()
call decide the type
of socket: PF_INET
just means it uses IP (which
you
always will); SOCK_DGRAM
and IPPROTO_UDP
go together for a UDP socket. In preparation for sending the
message to echo, we populate the intended server's structure,
using the command-line arguments.
/* Create the UDP socket */
if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
Die("Failed to create socket");
}
/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = inet_addr(argv[1]); /* IP address */
echoserver.sin_port = htons(atoi(argv[3])); /* server port */
The value returned in the call to socket()
is a
socket handle is similar to a file handle; specifically, if
the
socket creation fails, it will return -1 rather than a
positive
numbered handle. Support functions inet_addr()
and
htons()
(and atoi()
) are used to
convert
the string arguments into appropriate data structures.
For what it does, this UDP client is a bit simpler than was the
similar TCP echo client presented in the first part of this
tutorial series. As we saw with the Python versions, sending a
message is not based on first establishing a connection. You
simply send it to a specified address using sendto()
,
rather than with send()
on an established
connection.
Of course, this requires an extra couple arguments to indicate
the
intended server address.
/* Send the word to the server */
echolen = strlen(argv[2]);
if (sendto(sock, argv[2], echolen, 0,
(struct sockaddr *) &echoserver,
sizeof(echoserver)) != echolen) {
Die("Mismatch in number of sent bytes");
}
The error checking in this call usually establishes that a route to the server exists. This is the message raised if a named address is used by mistake, but it also occurs for valid-looking but unreachable IP addresses.
Receiving the data back works pretty much the same way as it
did in the TCP echo client. The only real change is a
substitute
a call to recvfrom()
for the TCP call to
recv()
/* Receive the word back from the server */
fprintf(stdout, "Received: ");
clientlen = sizeof(echoclient);
if ((received = recvfrom(sock, buffer, BUFFSIZE, 0,
(struct sockaddr *) &echoclient,
&clientlen)) != echolen) {
Die("Mismatch in number of received bytes");
}
/* Check that client and server are using same socket */
if (echoserver.sin_addr.s_addr != echoclient.sin_addr.s_addr) {
Die("Received a packet from an unexpected server");
}
buffer[received] = '\0'; /* Assure null terminated string */
fprintf(stdout, buffer);
fprintf(stdout, "\n");
close(sock);
exit(0);
}
The structure echoserver
had been configured with
an ad hoc port during the call to sendto()
; in
turn, the echoclient
structure gets similarly
filled
in with the call to recvfrom()
. This lets us
compare
the two addresses--if some other server or port sends a
datagram
while we are waiting to receive the echo. We guard at least
minimally against stray datagrams that do not interest us (we
might have checked the .sin_port
members also,
to be
completely certain).
At the end of the process, we print out the datagram that came back, and close the socket.
Even more than with TCP applications, UDP clients and servers
are quite similar to each other. In essence, either one
consists
mainly of some sendto()
and recvfrom()
calls mixed together. The main difference for a server is
simply
that it usually puts its main body in an indefinite loop to
keep
serving.
Let us start out with the usual includes and error function:
#include
#include
#include
#include
#include
#include
#include
#define BUFFSIZE 255
void Die(char *mess) { perror(mess); exit(1); }
Again, not much is new in the UDP echo server's declarations and usage message. We need a socket structure for the server and client, and a few variables that will be used to verify transmission sizes; and, of course, the buffer to read and write the message.
int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in echoserver;
struct sockaddr_in echoclient;
char buffer[BUFFSIZE];
unsigned int echolen, clientlen, serverlen;
int received = 0;
if (argc != 2) {
fprintf(stderr, "USAGE: %s\n", argv[0]);
exit(1);
}
The first real difference between UDP client and server comes
in the need to bind the socket on the server side. We saw
this
already with the Python example, and the situation is the same
here. The server socket is not the actual socket the message
is
transmitted over; rather, it acts as a factory for an ad
hoc socket which is configured in the recvfrom()
call we will see soon.
/* Create the UDP socket */
if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
Die("Failed to create socket");
}
/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = htonl(INADDR_ANY); /* Any IP address */
echoserver.sin_port = htons(atoi(argv[1])); /* server port */
/* Bind the socket */
serverlen = sizeof(echoserver);
if (bind(sock, (struct sockaddr *) &echoserver, serverlen) < 0) {
Die("Failed to bind server socket");
}
Readers will also notice that the echoserver
structure is configured a bit differently. In order to allow
connection on any IP address the server hosts, we use the
special
constant INADDR_ANY
for the member
.s_addr
.
The heavy lifting--such as it is--in the UDP sever is its main
loop. Basically, we perpetually wait to receive a message in a
recvfrom()
call. When this happens, the
echoclient
structure is populated with relevant
members for the connecting socket. We then use that structure
in
the subsequent sendto()
call.
/* Run until cancelled */
while (1) {
/* Receive a message from the client */
clientlen = sizeof(echoclient);
if ((received = recvfrom(sock, buffer, BUFFSIZE, 0,
(struct sockaddr *) &echoclient,
&clientlen)) < 0) {
Die("Failed to receive message");
}
fprintf(stderr,
"Client connected: %s\n", inet_ntoa(echoclient.sin_addr));
/* Send the message back to client */
if (sendto(sock, buffer, received, 0,
(struct sockaddr *) &echoclient,
sizeof(echoclient)) != received) {
Die("Mismatch in number of echo'd bytes");
}
}
}
And that's it! We can receive and send messages forever, reporting connections to the console as we go along. Of course, as we will see in the next section, this arrangement does only one thing at a time, which might be a problem for a server handling many clients (probably not for this simple echo server, but something more complicated might introduce poor latencies).
The servers we have looked at--that do nothing but echo a message--can handle each client request extremely quickly. But more generally, we might expect servers to perform potentially lengthy actions like database lookups, accessing remote resources, or complex computations in order to determine the response for a client. Our "one thing at a time" model does not scale well to multiple clients.
To demonstrate the point, let us look at a slightly modified Python server--one that takes some time to do its job (just to make the point that it is processing the request, we (trivially) modify the message string along the way too:
#!/usr/bin/env python
from socket import *
from sys import argv
def lengthy_action(sock, message, client_addr):
from time import sleep
print "Client connected:", client_addr
sleep(5)
sock.sendto(message.upper(), client_addr)
sock = socket(AF_INET, SOCK_DGRAM)
sock.bind(('',int(argv[1])))
while 1: # Run until cancelled
message, client_addr = sock.recvfrom(256)
lengthy_action(sock, message, client_addr)
In order to give the server some work to do, we can modify the client to make multiple requests (one per thread) that would like to be serviced as quickly as possible:
#!/usr/bin/env python
from socket import *
import sys, time
from thread import start_new_thread, get_ident
start = time.time()
threads = {}
sock = socket(AF_INET, SOCK_DGRAM)
def request(n):
sock.sendto("%s [%d]" % (sys.argv[2],n),
(sys.argv[1], int(sys.argv[3])))
messin, server = sock.recvfrom(255)
print "Received:", messin
del threads[get_ident()]
for n in range(20):
id = start_new_thread(request, (n,))
threads[id] = None
#print id,
while threads: time.sleep(.1)
sock.close()
print "%.2f seconds" % (time.time()-start)
Run against our new "lengthy action" server, the threaded client gets something like the following (abridged) output; note the time it takes, in particular:
$ ./UDPechoclient2.py localhost "Hello world" 7
Received: HELLO WORLD [7]
Received: HELLO WORLD [0]
...
Received: HELLO WORLD [18]
Received: HELLO WORLD [2]
103.96 seconds
Against one of the earlier servers, this client will run in a few seconds (but will not capitalize the returned string, of course); a version without the threading overhead will be even faster against the earlier servers. Assuming our hypothetical server process is not purely CPU-bound, we should be able to be much more responsive than 100+ seconds. Notice also that the threads are not generally serviced in the same order they are created in.
The way we have set up the "lengthy action" server, we guarantee that it takes at least five seconds to service any given client request. But there is no reason that multiple threads cannot be running during those same five seconds. Again, clearly a CPU bound process is not going to be faster through threading, but more often in a real server, those five seconds are spent doing something like a database query against another machine. In other words, we should be able to parallelize serving the several client threads.
An obvious approach here is to thread the server, just as the client is threaded:
#!/usr/bin/env python
from socket import *
from sys import argv
from thread import start_new_thread
# ...definition of 'lenthy_action()' unchanged...
sock = socket(AF_INET, SOCK_DGRAM)
sock.bind(('',int(argv[1])))
while 1: # Run until cancelled
message, client_addr = sock.recvfrom(256)
start_new_thread(lengthy_action, (sock, message, client_addr))
On my test system (using localhost, as before), this brings the
client runtime down to about 9 seconds--five of those spent in
the
call to sleep()
, the rest with threading and
connection overhead (roughly).
On Unix-like systems, forking is even easier than threading. While processes are nominally "heavier" than threads; on popular Posix systems like Linux, FreeBSD, and Darwin, process creation is still quite efficient.
In Python, a forking version of our "lengthy action" server can be as simple as:
#!/usr/bin/env python
from socket import *
from sys import argv, exit
from os import fork
def lengthy_action(sock, message, client_addr):
from time import sleep
print "Client connected:", client_addr
sleep(5)
sock.sendto(message.upper(), client_addr)
exit()
sock = socket(AF_INET, SOCK_DGRAM)
sock.bind(('',int(argv[1])))
while 1: # Run until cancelled
message, client_addr = sock.recvfrom(256)
if fork():
lengthy_action(sock, message, client_addr)
On my Mac OSX test system, I actually found this forking
version a couple seconds faster than the threaded
server;
to my mild surprise. As a slight difference in behavior,
after
servicing a collection of client threads, the main process in
the
while
loop winds up as a background process,
even if
the server was launched in the foreground. In the usual case
where you launch the server in the background, the difference
is
irrelevant though.
Another technique called asynchronous or
non-blocking sockets is potentially even more
efficient
than are threading or forking approaches. The concept behind
asynchronous programming is to keep execution within a single
thread, but poll each open socket to see if it has more data
waiting to be read or written. However, non-blocking sockets
are
really only useful for I/O bound processes--the simulation of a
CPU-bound server that we created using sleep()
sort
of misses the point. Moreover, non-blocking sockets make a
bit
more sense for TCP connections than for UDP ones, since the
former
retain an open connection than may still have pending data.
In overview, the structure of an asynchronous peer (client
or server) is a polling loop--usually using the
function
select()
or some higher-level wrapper to it
such as
Python's asyncore
. At each pass through the
loop,
you check all the open sockets to see which ones are currently
readable and which ones currently writeable. This is quick to
check, and you can simply ignore any sockets that are not
currently ready for I/O actions. This style of socket
programming
avoids any overhead associated with threads or processes.
To simulate a low bandwidth connection, we can create a client
that introduces artificial delays in sending data, and
dribbles
out its message byte by byte. To simulate many such
connections,
we thread multiple connections (each slow). Generally, this
client is similar to the UDPechoclient2.py
we
saw
above, but in a TCP version:
#!/usr/bin/env python
from socket import *
import sys, time
from thread import start_new_thread, get_ident
threads = {}
start = time.time()
def request(n, mess):
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((sys.argv[1], int(sys.argv[3])))
messlen, received = len(mess), 0
for c in mess:
sock.send(c)
time.sleep(.1)
data = ""
while received < messlen:
data += sock.recv(1)
time.sleep(.1)
received += 1
sock.close()
print "Received:", data
del threads[get_ident()]
for n in range(20):
message = "%s [%d]" % (sys.argv[2], n)
id = start_new_thread(request, (n, message))
threads[id] = None
while threads:
time.sleep(.2)
print "%.2f seconds" % (time.time()-start)
We need a "traditional" TCP server to test our slow client against. In essence, the below is identical to the second (low-level) Python server presented in the first part of this tutorial. The only real difference is that the maximum connections are increased to twenty.
#!/usr/bin/env python
from socket import *
import sys
def handleClient(sock):
data = sock.recv(32)
while data:
sock.sendall(data)
data = sock.recv(32)
newsock.close()
if __name__=='__main__':
sock = socket(AF_INET, SOCK_STREAM)
sock.bind(('',int(sys.argv[1])))
sock.listen(20)
while 1: # Run until cancelled
newsock, client_addr = sock.accept()
print "Client connected:", client_addr
handleClient(newsock)
Let us try running the "slow connection" client against the "one thing at a time" server (abridged output, as before):
$ ./echoclient2.py localhost "Hello world" 7
Received: Hello world [0]
Received: Hello world [1]
Received: Hello world [5]
...
Received: Hello world [16]
37.07 seconds
As with the UDP stress-test client, the threads do not necessarily connect in the order they are launched. Most significantly, however, is to notice that the time it takes to serve all twenty threads is basically the same as the sum of all the introduced delays in writing bytes over the sockets. Nothing is parallelized here, since we need to wait for each individual socket connection to complete.
Now we are ready to see how the function select()
can be used to bypass I/O delays of the sort we just
introduced
(or of the kind that arises "in the wild" because of genuinely
slow connections). A few panels ago, the general concept was
discussed; let us look at the specific code:
#!/usr/bin/env python
from socket import *
import sys, time
from select import select
if __name__=='__main__':
while 1:
sock = socket(AF_INET, SOCK_STREAM)
sock.bind(('',int(sys.argv[1])))
print "Ready..."
data = {}
sock.listen(20)
for _ in range(20):
newsock, client_addr = sock.accept()
print "Client connected:", client_addr
data[newsock] = ""
last_activity = time.time()
while 1:
read, write, err = select(data.keys(), data.keys(), [])
if time.time() - last_activity > 5:
for s in read: s.shutdown(2)
break
for s in read:
data[s] = s.recv(32)
for s in write:
if data[s]:
last_activity = time.time()
s.send(data[s])
data[s] = ""
This server is fragile in that it always waits for exactly 20
client connections before select()
's among
them. But
we still demonstrate the basic concept of using a tight
polling
loop, and reading/writing only when data is available on a
particular socket. The return values of select()
is
a tuple of lists of sockets that are readable, writeable, and
in
error, respectively. Each of these types are handled within
the
loop, as needed.
Using this asynchronous server, by the way, lets the "slow connection" client complete all 20 connections in about 6 seconds, rather than 37 seconds (at least on my test system).
The examples presented for more scaleable servers have all used Python. In truth, the quality of Python's libraries mean that these will not be significantly slower than analogous servers written in C. And for this tutorial, relative brevity of presentation is important.
In presenting the above Python servers, I have stuck to
relatively low-level facilities within Python. Some of the
higher-level modules like asyncore
or
SocketServer
--or even threading
rather
than thread
--might provide more "Pythonic"
techniques. These low-level facilities I utilized, however,
remain quite close in structure to the ways you would program
the
same things in C. Python's dynamic typing and concise syntax
still save quite a few lines, but a C programmer should be
able
to use my examples as outlines for similar C servers.
The server and client presented in this tutorial are simple, but they show everything essential to writing UDP sockets applications in C and in Python. A more sophisticated client or server is, at heart, just one that transmits more interesting data back and forth; the sockets-level code is not much different for these.
The general outlines of performing threading, forking, and asynchronous socket handling are similarly applicable to more advanced servers. Your servers and clients themselves are likely to do more, but your strategies towards scalability will always be one of these three approaches (or a combination of them).
A good introduction to sockets programming in C, is Michael J. Donahoo and Kenneth L. Calvert, TCP/IP Sockets in C , Morgan-Kaufmann, 2001; ISBN: 1-55860-826-5.
The sockets discussed in these tutorials have been unicast sockets, which are certainly more widely used in common internet protocols. However, there is also such a thing as multicast sockets--which connect one transmission to many recipients. Brad Huntting and I have written a series of articles describing issues and technologies around IP multicast
Please let us know whether this tutorial was helpful to you and how we could make it better. We'd also like to hear about other tutorial topics you'd like to see covered.
For questions about the content of this tutorial, contact the
author, David Mertz, at .
==========================================================