Network Clients and Servers

CS 321 2007 Lecture, Dr. Lawlor

An osl/socket.h network client looks like this:
  1. skt_ip_t ip=skt_lookup_ip("www.google.com");   // Look up IP address
  2. SOCKET s=skt_connect(ip,80,60); // connect to port 80, 60-second timeout
  3. skt_sendN(s,"hey",3); // Send some data to the server--usually some description of the request.
  4. skt_recvN(s,myString,8); // Receive some binary data from the server--usually a response to our request.
  5. skt_close(s); // stop talking to the server
Once you're connected, you can send and receive arbitrary data in any order.

One program can skt_connect to many servers at once, although you do have to be careful that most of these calls can wait until the server responds.  This means real network programs often have many threads or processes that each talk to one server.  It is possible to ask if data is ready to receive on a particular socket with skt_select1(s,0), which returns 1 if the socket has readable data, or 0 if no data is ready to read yet.

skt_lookup_ip works with either DNS names (like "www.google.com") or dotted-decimal IP addresses (like "127.0.0.1").  Sadly, DNS doesn't work in NetRun due to a missing /etc/resolv.conf file.  On your own machine, you can make up your own local DNS-style names by listing them in a "hosts file" (/etc/hosts on UNIX, C:\Windows\System32\drivers\etc\hosts on Windows).

Network Server

A network server waits for connections from clients.  The calls you make are:
  1. unsigned int port=8888; /* listen on this TCP/IP port (or use 0 to have the OS pick a port) */
  2. SERVER_SOCKET srv=skt_server(&port); /* lay claim to that port number */
  3. SERVER s=skt_accept(srv,0,0); /* wait until a client connects to our port */
  4. skt_sendN and skt_recvN data to and from the client.
  5. skt_close(s); /* stop talking to that client */
  6. skt_close(srv); /* give up our claim on server port */
Again, between accept and close you can send and receive data any way you like.  Your sends make data arrive at client receive calls, and your receives grab data from the client's sends.  It's easy to screw up a network server by trying to receive data that isn't going to arrive!

You usually repeat steps 3-5 again and again to handle all the clients that try to connect.  Many servers are designed as an infinite loop--they keep handling client requests until the machine is turned off.  One thread can even have accepted connections from several different clients, and be sending and receiving data from them at the same time.

High-performance servers, like the Apache web server, often will call fork() either before step 3 (called "preforking", where several processes wait in accept) or before step 4 (one process accepts, then splits off a child process to handle each client).

Only root can open server ports numbered less than 1024 on most UNIX systems.  Two programs can't listen on the same server port--the second program will get a socket error when he tries skt_server.

Here's an example network server that serves exactly one client and then exits.
#include "osl/socket.h"
#include "osl/socket.cpp" /* include body for easy linking */

int foo(void)
{
unsigned int port=8888;
SERVER_SOCKET serv=skt_server(&port);

std::cout<<"Waiting for connections on port "
<<port<<"\n";
skt_ip_t client_ip; unsigned int client_port;
SOCKET s=skt_accept(serv,&client_ip,&client_port);
std::cout<<"Connection from "
<<skt_print_ip(client_ip)
<<":"<<client_port<<"!\n";

/* Receive some data from the client */
std::string buf(3,'?');
skt_recvN(s,(char *)&buf[0],3);
std::cout<<"Client sent data '"<<buf<<"'\n";

/* Send some data back to the client */
skt_sendN(s,"gdaymate\n",9);

skt_close(s);
std::cout<<"Closed socket to client\n";

skt_close(serv);
return 0;
}
(executable NetRun link)

In NetRun, the server will just hang while waiting for connections by default.  If you visit the URL https://lawlor.cs.uaf.edu:8888/ while the program is running, you should see the gdaymate message!

Here's the corresponding client.  Note the receives in the server have to be sent by the client, and vice versa.
#include "osl/socket.h"
#include "osl/socket.cpp" /* include body for easy linking */

int foo(void)
{
skt_ip_t ip=skt_lookup_ip("127.0.0.1");
unsigned int port=8888;
SOCKET s=skt_connect(ip,port,2);

/* Send some data to the server */
skt_sendN(s,"dUd",3);

/* Receive some data from the client */
std::string buf(8,'?');
skt_recvN(s,(char *)&buf[0],8);
std::cout<<"Server sent data '"<<buf<<"'\n";


skt_close(s);
std::cout<<"Closed socket to server\n";
return 0;
}
(executable NetRun link) <- this version connects to port 32170, where I've left a server running.

You can also download this server and client program (directory, .zip, .tar.gz), and run them on your own machine.

It's easier to write network clients, and it's more common.  Network servers are more dangerous, and usually trickier to get right.

Network Data

If you're writing the client and server, you can make up whatever data you want to send and receive.  If you're only writing the client, you have to give the server whatever it expects.

In particular, it's common to send and receive ASCII data, which you can send and receive as bytes like this:
	std::string bar="This string stinks.";
skt_send(s,(char *)&bar[0],bar.size());
std::string baz(16,'x'); /* space for 16 characters, initialized to x's */

skt_recv(s,(char *)&baz[0],baz.size());
If you don't know how many bytes you'll need to receive, you can read an entire ASCII line like this:
	std::string bax=skt_recv_line(s);  /* grab bytes up to the newline */
It's also common to send and receive binary data, like a big-endian 32-bit number.  "osl/socket.h" has a version of the portable "Big32" class, so you can send a big-endian 32-bit binary integer like this:
	Big32 v=7;
skt_sendN(s,(char *)&v,sizeof(v));
Sending and receiving data on the network is *exactly* like writing and reading data from files.

Example Protocol: HTTP

HTTP is the protocol used by web pages (that's why URLs start with "http://").  HTTP servers listen on port 80 by default, but you can actually use the :port syntax to connect to any port you like (for example, "https://lawlor.cs.uaf.edu:8888/some_url").

An HTTP client, like a web browser, starts by doing a DNS lookup on the server name.  That's the "resolving host name" message you see in your browser.  The browser then does a TCP connection to that port on the server ("Connecting to server").

Once connected, the HTTP client usually sends a "GET" request.  Here's the simplest possible GET request:
    "GET / HTTP/1.0\r\n\r\n"

Note the DOS newlines, and the extra newline at the end of the request.  You can list a bunch of optional data in your  GET request, like the languages you're willing to accept ("Accept-Language: en-us\r\n") and so on.  HTTP 1.1 (not 1.0) requires a Host to be listed in the request ("Host: www.foobar.com\r\n"), which is used by virtual hosts.

The HTTP server then sends back some sort of reply.  Officially, this is supposed to be a "HTTP/1.1 200 OK\r\n" followed by another set of line-oriented ASCII optional data, such as the Content-Length in bytes ("Content-Length: 187\r\n").  But many browsers will print out plain ASCII text if you just return that.

Here's an example of a real HTTP exchange between Firefox and Apache:
Firefox connects to server.  Apache accepts the connection.
Firefox, the client, sends this ASCII data, with DOS newlines:
GET /my_name_is_url.html HTTP/1.1
Host: lawlor.cs.uaf.edu:8888
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20070126 Ubuntu/dapper-security Firefox/1.5.0.9
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: __utmz=62224958.1163103248.1.1.utmccn=(direct)|utmcsr=(direct)|utmcmd=(none); __utma=62224958.570638686.1163103248.1163107343.1164832326.3
<- blank line at end of HTTP request headers

Apache, the server, sends this data back:
HTTP/1.1 200 OK
Date: Fri, 06 Apr 2007 20:20:50 GMT
Server: Apache/2.0.55 (Ubuntu)
Accept-Ranges: bytes
Content-Length: 9443
Connection: close
Content-Type: text/html; charset=UTF-8
<- blank line at end of HTTP response headers
<html><head><title>UAF Department of ... rest of web page, total of 9443 bytes after blank line