socket to create a IPv4, TCP socket, andconnect to bind the socket to a random
port, and make a TCP connection from that port to a
host+port where we knew there'd be a server running.
Now we're going to consider the server, and see how it's implemented. What it needs to do is a bit different because, as we keep hammering, the roles of client and server are asymmetric: the server must be bound to a previously-decided-upon port, the client's port number is irrelevent; the server must sit and wait, listening for a connection request, whereas the client takes the active role and makes the connection request. Thus, the basic steps for the server are quite different:
socket to create a IPv4, TCP socket,bind to bind the socket to a
specific port number,listen to listen for connection
requests for that port, and accept to accept a connection request
and begin communicating.
/* Set up socket */
int sfd = socket(AF_INET,SOCK_STREAM,0);
if (sfd == -1) { fprintf(stderr,"Socket not created!\n"); exit(1); }
sfd to a
specific host+port.
For our first iteration, we'll hard-code the IP address of our
server. Later, it'll make much more sense (though require a
bit more work) to have the program automatically use get the
IP address of the current host. Hard-coding the port number
is OK, but it'd be better if that could at least be changed
from the command-line.
The bind system call is what we use to
accomplish this step, and its syntax should look hauntingly familiar:
int bind(int s, const struct sockaddr *name, int namelen);You see that specifying a host+port has to be done the same mickey mouse way as we did for calls to
connect.
/* Hardcoded IP address and port number. */
unsigned int ipnum = 2205833693U; // The "U" makes this an usigned int literal
unsigned short pnum = 10000;
/* Set up address structure */
struct sockaddr_in mysa;
mysa.sin_family = AF_INET;
mysa.sin_addr.s_addr = htonl(ipnum);
mysa.sin_port = htons(pnum);
/* Bind socket to address */
if (bind(sfd,(struct sockaddr*)&mysa,sizeof(mysa)) < 0)
{
close(sfd);
fprintf(stderr,"bind failed!\n");
exit(3);
}
Of course the call to bind may fail, and if it
does, -1 is returned. Otherwise 0 is returned. Now, why
might bind fail? Well already at this point we have enough to
see how/why. Let's run the program in its current state
(see ex0.c) and use netstat -a
to see how it's doing:
banshee$ gcc -o ex0 ex0.c -lnsl -lsocket
banshee$ ./ex0 &
[1] 5430
banshee$ netstat -a
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
-------------------- -------------------- ----- ------ ----- ------ -----------
mich302csd21d.32769 nwtime.usna.edu.ldap 130572 0 49640 0 ESTABLISHED
localhost.32776 *.* 0 0 49152 0 LISTEN
*.fs *.* 0 0 49152 0 LISTEN
*.dtspc *.* 0 0 49152 0 LISTEN
*.time *.* 0 0 49152 0 LISTEN
*.ftp *.* 0 0 49152 0 LISTEN
*.ssh *.* 0 0 49152 0 LISTEN
mich302csd21d.1023 chessie.cs.usna.edu.nfsd 49640 0 49640 68 ESTABLISHED
mich302csd21d.ssh 131.122.90.34.38390 64128 0 49232 0 ESTABLISHED
mich302csd21d.10000 *.* 0 0 49152 0 BOUND
We see from netstat that the socket has been bound to port
10000, just like we wanted. Now, what happens if, while the
first ex0 process is still running (remember with
the & it's still going in the background):
banshee$ ./ex0 & [2] 5433 bind failed! [2] Exit 3 ./ex0The call to bind failed! Why? Because only one socket may be bound to a given port, using a given protocol, on a given host at one time. That way a socket, once bound, has a truly unique address: it's protocol + address + port, where "protocol" is something like "IPv6 TCP". Anyway, you can see that it's worth checking to see if bind succeeds! (Better kill that
ex0 process right now.)
listen (man -s 3socket listen).
int listen(int s, int backlog);The first parameter is the descriptor referencing the socket, the second is an int that tells the kernel how many connection requests to queue up before it just starts turning requests away. You can imagine, if your server supplies a popular server, you may get lots of connection requests, and get them far faster than you can handle. The kernel will queue them up for you ... this parameter sets a limit on how long that queue will get. As with bind, a return value of 0 indicates success, while failure is indicated by -1.
/* Listen for connection request */
if (listen(sfd,20) < 0)
{
close(sfd);
fprintf(stderr,"listen failed!\n");
exit(4);
}
If we compile and run our implementation so far (ex1.c), then run netstat -a, we'll see that
the state of your port has changed:
banshee$ gcc -o ex0 ex0.c -lnsl -lsocket
banshee$ ./ex0 &
[1] 5430
banshee$ netstat -a
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
-------------------- -------------------- ----- ------ ----- ------ -----------
mich302csd21d.32769 nwtime.usna.edu.ldap 130572 0 49640 0 ESTABLISHED
localhost.32776 *.* 0 0 49152 0 LISTEN
*.fs *.* 0 0 49152 0 LISTEN
*.dtspc *.* 0 0 49152 0 LISTEN
*.time *.* 0 0 49152 0 LISTEN
*.ftp *.* 0 0 49152 0 LISTEN
*.ssh *.* 0 0 49152 0 LISTEN
mich302csd21d.1023 chessie.cs.usna.edu.nfsd 49640 0 49640 68 ESTABLISHED
mich302csd21d.ssh 131.122.90.34.38390 64128 0 49232 0 ESTABLISHED
mich302csd21d.10000 *.* 0 0 49152 0 LISTEN
See? The state changed from BOUND to LISTEN ... we're making progress.
accept
(man -s 3socket accept):
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);The int s is the descriptor for the socket we're listening on. The other two arguments are there to let the kernel provide us with the address (i.e. host+port) from which the request is coming. If we don't care about that, and for the moment we don't, we can simply set them both to NULL. What
accept returns for us is a file descriptor
for this particular connection we've just accepted.
You see, more requests may be coming in from other clients,
and they'll come on the original descriptor sfd.
This new descriptor provides a private channel for
communicating just with the remote socket we've accepted a
connection from ... as if all the other stuff going on with
sfd wasn't even there.
/* Accept the connection request */
int sessionfd = accept(sfd,NULL,NULL);
if (sessionfd == -1)
{
close(sfd);
fprintf(stderr,"accept failed!\n");
exit(5);
}
Now if we run our server (ex2.c), and launch our client to connect to
it, netstat shows us yet another state:
banshee$ gcc -o ex2 ex2.c -lnsl -lsocket banshee$ ./ex2 & |
fireball$ gcc -o client client.c -lnsl -lsocket fireball$ ./client banshee.cs.usna.edu |
banshee$ netstat -a
TCP: IPv4
Local Address Remote Address Swind Send-Q Rwind Recv-Q State
-------------------- -------------------- ----- ------ ----- ------ -----------
mich302csd21d.32769 nwtime.usna.edu.ldap 130572 0 49640 0 ESTABLISHED
localhost.32776 *.* 0 0 49152 0 LISTEN
*.fs *.* 0 0 49152 0 LISTEN
*.dtspc *.* 0 0 49152 0 LISTEN
*.time *.* 0 0 49152 0 LISTEN
*.ftp *.* 0 0 49152 0 LISTEN
*.ssh *.* 0 0 49152 0 LISTEN
mich302csd21d.1023 chessie.cs.usna.edu.nfsd 49640 0 49640 68 ESTABLISHED
mich302csd21d.ssh 131.122.90.34.38390 64128 0 49232 0 ESTABLISHED
mich302csd21d.10000 fireball.cs.usna.edu.41617 0 0 49152 0 ESTABLISHED
| |
So, we see that a connection has been established, and netstat shows us the host+port that defines the address of the socket that has connected to us. At this point, we can do ... whatever it is we really wanted the server to do. My example is a "Capitalization server". It reads characters and echos them back, changing any lowercase to capitals. Check out ex3.c to see that in action.
sessionfd, we can serve the client without
thinking at all about networks or sockets. We just use
sessionfd like any other descriptor, except that
this one is bidirectional, i.e. we can both read and write,
and the two operations don't conflict with one another. So,
once we get that descriptor, we can fork a process to serve
that connection, or spawn a thread to serve that connection.
Which to do depends on the application. In this case, we have
no data to communicate between the thread/process serving that
client and any other thread/process, so that makes processes a
better choice. So our server is essentially a big loop that
accepts a connection, forks a process to serve that
connection, and goes back to the top.
/* Accept the connection request */
while(1)
{
int sessionfd = accept(sfd,NULL,NULL);
if (sessionfd == -1)
{
close(sfd);
fprintf(stderr,"accept failed!\n");
exit(5);
}
/* fork a child to serve this connection. */
if (fork() == 0)
{
char inc, outc;
while(read(sessionfd,&inc,1) == 1)
{
if ('a' <= inc && inc <= 'z')
outc = inc - 'a' + 'A';
else
outc = inc;
write(sessionfd,&outc,1);
}
close(sessionfd);
exit(1);
}
else
{
close(sessionfd);
}
}
fprintf(stderr,"Server shutting down!\n");
close(sfd);
You can check out the complete server
code to see the process in all of its glory, including
grabbing the current host's name from the HOST
environment variable, and using gethostbyname to
find the IP address from the hostname.