|
Socket streams
Analogous to filestreams are sockets or TCP/IP network
connections. A socket is a two-way (read/write) pseudo-file node. An open
socket stream is like an open file-descriptor. Berkeley sockets are part of the
standard C library.
There are two main kinds of socket: TCP/IP sockets and UNIX domain sockets.
UNIX sockets can be used to provide local interprocess communication using a
filestream communication protocol. TCP/IP sockets open file descriptors across
the network. A TCP/IP socket is a file stream associated with an IP address and
a port number. We write to a socket descriptor just as with a file descriptor,
either with write() or using
send().
When sending binary data over a network we have to be careful about machine
level representations of data. Operating systems (actually the hardware they run
on) fall into two categories known as big endian and little
endian. The names refer to the byte-order of numerical
representations. The names indicate how large integers (which require say 32
bits or more) are stored in memory. Little endian systems store the least
significant byte first, while big endian systems store the most significant byte
first. For example, the representation of the number 34,677,374 has either of
these forms.
-----------------------------------
Big | 2 | 17 | 34 | 126
|
-----------------------------------
-----------------------------------
Little | 126 | 34 | 17 | 2
|
-----------------------------------
Obviously if we are transferring data from one host to another, both hosts
have to agree on the data representation otherwise there would be disastrous
consequences. This means that there has to be a common standard of network
byte ordering. For example, Solaris (SPARC hardware) uses network byte
ordering (big endian), while GNU/Linux (Intel hardware) uses the opposite
(little endian). This means that Intel systems have to convert the format every
time something is transmitted over the network. UNIX systems provide generic
functions for converting between host-byteorder and network-byteorder for small
and long integer data:
htonl, htons, ntohl,
ntohs
Here we list two example programs which show how to make a client-server
pair. The server enters a loop, and listens for connections from any clients
(the generic address ´INADDR_ANY'
is a wildcard for any address on the current local network segment). The client
program sends requests to the server as a protocol in the form of a string of
the type ´a + b'. Normally
´a' and
´b' are numbers, in which case the
server returns their sum to the client. If the message has the special form
´halt + *', where the star is
arbitrary, then the server shuts down. Any other form of message results in an
error, which the server signals to the client.
The basic structure of the client-server components in terms of system
calls is this:
Client:
socket() Create a
socket
connect() Contact a server
socket (IP + port)
while (?)
{
send() Send to
server
recv() Receive from
server
}
Server:
socket() Create a
socket
bind() Associates the
socket with a fixed address
listen() Create a listen
queue
while()
{
reply=accept() Accept a connection
request
recv() Receive from
client
send() Send to
client
}
/**********************************************************************/
/*
*/
/* The client part of a client-server pair.
This simply takes two */
/* numbers and adds them together, returning
the result to the client */
/*
*/
/* Compiled with:
*/
/* cc server.c
*/
/*
*/
/* User types:
*/
/* 3 + 5
*/
/* a + b
*/
/* halt + server
*/
/**********************************************************************/
#include <stdio.h>
#include
<sys/types.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#include <netdb.h>
#define PORT 9000 /*
Arbitrary non-reserved port */
#define HOST
"nexus.iu.hio.no"
#define bufsize 20
/**********************************************************************/
/* Main
*/
/**********************************************************************/
main (argc,argv)
int argc;
char *argv[];
{ struct sockaddr_in cin;
struct hostent *hp;
char buffer[bufsize];
int sd;
if (argc != 4)
{
printf("syntax: client a +
b\n");
exit(1);
}
if ((hp = gethostbyname(HOST)) ==
NULL)
{
perror("gethostbyname:
");
exit(1);
}
memset(&cin,0,sizeof(cin)); /*
Another way to zero memory */
cin.sin_family =
AF_INET;
cin.sin_addr.s_addr = ((struct in_addr
*)(hp->h_addr))->s_addr;
cin.sin_port =
htons(PORT);
printf("Trying to connect to %s =
%s\n",HOST,inet_ntoa(cin.sin_addr));
if ((sd = socket(AF_INET,SOCK_STREAM,0)) ==
-1)
{
perror("socket");
exit(1);
}
if (connect(sd,&cin,sizeof(cin)) ==
-1)
{
perror("connect");
exit(1);
}
sprintf(buffer,"%s +
%s",argv[1],argv[3]);
if (send(sd,buffer,strlen(buffer),0) ==
-1)
{
perror ("send");
exit(1);
}
if (recv(sd,buffer,bufsize,0) ==
-1)
{
perror("recv");
exit (1);
}
printf ("Server responded with
%s\n",buffer);
close (sd);
unlink("./socket");
}
/**********************************************************************/
/*
*/
/* The server part of a client-server pair.
This simply takes two */
/* numbers and adds them together, returning
the result to the client */
/*
*/
/* Compiled with:
*/
/* cc server.c
*/
/*
*/
/**********************************************************************/
#include <stdio.h>
#include
<sys/types.h>
#include
<sys/socket.h>
#include
<netinet/in.h>
#include <netdb.h>
#define PORT 9000
#define bufsize 20
#define queuesize 5
#define true 1
#define false 0
/**********************************************************************/
/* Main
*/
/**********************************************************************/
main ()
{ struct sockaddr_in cin;
struct sockaddr_in
sin;
struct hostent *hp;
char buffer[bufsize];
int sd, sd_client,
addrlen;
memset(&sin,0,sizeof(sin)); /*
Another way to zero memory */
sin.sin_family =
AF_INET;
sin.sin_addr.s_addr = INADDR_ANY; /*
Broadcast address */
sin.sin_port =
htons(PORT);
if ((sd = socket(AF_INET,SOCK_STREAM,0)) ==
-1)
{
perror("socket");
exit(1);
}
if (bind(sd,&sin,sizeof(sin)) == -1) /*
Must have this on server */
{
perror("bind");
exit(1);
}
if (listen(sd,queuesize) ==
-1)
{
perror("listen");
exit(1);
}
while (true)
{
if ((sd_client =
accept(sd,&cin,&addrlen)) == -1)
{
perror("accept");
exit(1);
}
if (recv(sd_client,buffer,sizeof(buffer),0)
== -1)
{
perror("recv");
exit(1);
}
if
(!DoService(buffer))
{
break;
}
if (send(sd_client,buffer,strlen(buffer),0)
== -1)
{
perror("send");
exit(1);
}
close (sd_client);
}
close (sd);
printf("Server closing
down...\n");
}
/**************************************************************/
DoService(buffer)
char *buffer;
/* This is the protocol section. Here we
must */
/* check that the incoming data are sensible
*/
{ int a=0,b=0;
printf("Received:
%s\n",buffer);
sscanf(buffer,"%d +
%d\n",&a,&b);
if (a > 0 && b>
0)
{
sprintf(buffer,"%d + %d =
%d",a,b,a+b);
return true;
}
else
{
if (strncmp("halt",buffer,4) ==
0)
{
sprintf(buffer,"Server closing
down!");
return false;
}
else
{
sprintf(buffer,"Invalid
protocol");
return true;
}
}
}
In the example we use ´streams' to implement a typical input/output
behaviour for C. A stream interface is a so-called reliable protocol. There are
other kinds of sockets too, called unrealiable, or UDP sockets. Features
to notice on the server are that we must bind to a specific address. The client
is always implicitly bound to an address since a socket connection always
originates from the machine on which the client is running. On the server
however we want to know which addresses we shall be receiving requests from. In
the above example we use the generic wildcard address
´INADDR_ANY' which means that any
host can connect to the server. Had we been more specific, we could have limited
communication to two machines only.
By calling ´listen()' we set
up a queue for incoming connections. Rather than forking a separate process to
handle each request we set up a queue of a certain depth. If we exceed this
depth then new clients rtying to connect will be refused connection.
The ´accept' call is the
mechanism which extracts a ´reply handle' from the socket. Using the handle
obtained from this call we can reply to the client without having to open a
special socket explicitly.
An improved server side connection can be setup, reading the service name
from ´/etc/services' and setting
reusable socket options to avoid busy signals, like this:
struct sockaddr_in cin, sin;
struct servent
*server;
int sd, addrlen =
sizeof(cin);
int portnumber, yes=1;
if ((server =
getservbyname(service-name,"tcp")) == NULL)
{
CfLog(cferror,"Couldn't get cfengine
service","getservbyname");
exit (1);
}
bzero(&cin,sizeof(cin));
/* Service returns network byte order
*/
sin.sin_port = (unsigned
short)(server->s_port);
sin.sin_addr.s_addr =
INADDR_ANY;
sin.sin_family = AF_INET;
if ((sd = socket(AF_INET,SOCK_STREAM,0)) ==
-1)
{
CfLog(cferror,"Couldn't open
socket","socket");
exit (1);
}
if (setsockopt (sd, SOL_SOCKET,
SO_REUSEADDR,
(char *) &yes,
sizeof (int)) == -1)
{
CfLog(cferror,"Couldn't set socket
options","sockopt");
exit (1);
}
if (bind(sd,(struct sockaddr
*)&sin,sizeof(sin)) == -1)
{
}
/* etc */
|