Serving applications

Warning

This is an unpolished draft of the second edition of this ebook. If you find any error or have suggestions to improve the text, please create an issue via https://github.com/obonaventure/cnp3/issues?milestone=3

Open questions

  1. What are the mechanisms that should be included in a transport protocol that provides an unreliable connectionless transport service that can detect transmission errors but not correct them ?
  2. A reliable connection oriented transport places a 32 bits sequence number inside the segment header to number the segments. This sequence number is incremented for each data segment. The connection starts as shown in the figure below :

msc {
a [label="", linecolour=white],
b [label="Host A", linecolour=black],
z [label="", linecolour=white],
c [label="Host B", linecolour=black],
d [label="", linecolour=white];
a=>b [ label = "CONNECT.req()" ] ,
b>>c [ label = "CR(seq=1341)", arcskip="1"];
c=>d [ label = "CONNECT.ind()" ];
d=>c [ label = "CONNECT.resp()" ],
c>>b [ label = "CR(ack=1341,seq=2141)", arcskip="1"];
b=>a [ label = "CONNECT.conf()" ];
b>>c [ label = "CA(seq=1341,ack=2141)", arcskip="1"];
|||;
a=>b [ label = "DATA.req(a)" ];
}

Continue the connection so that Host B sends Hello as data and Host A replies by sending Pleased to meet you. After havng received the response, Host B closes the connection gracefully and Host A does the same. Discuss on the state that needs to be maintained inside each host.

  1. A transport connection that provides a message-mode service has been active for some time and all data has been exchanged and acknowledged in both directions. As in the exercise above, the sequence number is incremented after the transmission of each segment. At this time, Host A sends two DATA segments as shown in the figure below.

msc {
a [label="", linecolour=white],
b [label="Host A", linecolour=black],
z [label="", linecolour=white],
c [label="Host B", linecolour=black],
d [label="", linecolour=white];
a=>b [ label = "DATA.req(abc)" ] ,
b-x c [ label = "DATA(seq=1123,abc)", arcskip="1"];
a=>b [ label = "DATA.req(def)" ] ,
b>>c [ label = "DATA(seq=1124,def)", arcskip="1"];
a=>b [ label = "DISCONNECT.req(graceful,A->B)" ];
|||;
}

What are the acknowledgements sent by Host B, how does Host A react and how does it terminate the connection ?

  1. Consider a reliable connection-oriented transport protocol that provides the bytestream service. In this transport protocol, the sequence number that is placed inside each DATA segment reflects the position of the bytes in the bytestream. Considering the connection shown below, provide the DATA segments that are sent by Host A in response to the DATA.request, assuming that one segment is sent for each DATA.request.

msc {
a [label="", linecolour=white],
b [label="Host A", linecolour=black],
z [label="", linecolour=white],
c [label="Host B", linecolour=black],
d [label="", linecolour=white];
a=>b [ label = "CONNECT.req()" ] ,
b>>c [ label = "CR(seq=8765)", arcskip="1"];
c=>d [ label = "CONNECT.ind()" ];
d=>c [ label = "CONNECT.resp()" ],
c>>b [ label = "CR(ack=8765,seq=4321)", arcskip="1"];
b=>a [ label = "CONNECT.conf()" ];
b>>c [ label = "CA(seq=8765,ack=4321)", arcskip="1"];
|||;
a=>b [ label = "DATA.req(a)" ];
|||;
a=>b [ label = "DATA.req(bcdefg)" ];
|||;
a=>b [ label = "DATA.req(ab)" ];
|||;
a=>b [ label = "DATA.req(bbbbbbbbbbbb)" ];
}

  1. Same question as above, but consider now that the transport protocol tries to send large DATA segments whenever possible. For this exercise, we consider that a DATA segment can contain up to 8 bytes of data in the payload. Do not forget to show the acknowledgements in your answer.
  2. Consider a transport protocol that provides a reliable connection-oriented bystream service. You observe the segments sent by a host that uses this protocol. Does the time-sequence diagram below reflects a valid implementation of this protocol ? Justify your answer.

msc {
a [label="", linecolour=white],
b [label="Host A", linecolour=black],
z [label="", linecolour=white],
c [label="Host B", linecolour=black],
d [label="", linecolour=white];
a=>b [ label = "DATA.req(abc)" ] ,
b-x c [ label = "DATA(seq=1123,abc)", arcskip="1"];
a=>b [ label = "DATA.req(def)" ] ,
b-x c [ label = "DATA(seq=1126,def)", arcskip="1"];
|||;
b>>c [ label = "DATA(seq=1123,abcdef)", arcskip="1"];
|||;
}

  1. In the above example, the two DATA segments were lost before arriving at the destination. Discuss the following scenario and explain how the receiver should react to the reception of the last DATA segment.

msc {
a [label="", linecolour=white],
b [label="Host A", linecolour=black],
z [label="", linecolour=white],
c [label="Host B", linecolour=black],
d [label="", linecolour=white];
a=>b [ label = "DATA.req(abc)" ] ,
b-x c [ label = "DATA(seq=1123,abc)", arcskip="1"];
a=>b [ label = "DATA.req(def)" ] ,
b>> c [ label = "DATA(seq=1126,def)", arcskip="1"];
|||;
b>>c [ label = "DATA(seq=1123,abcdef)", arcskip="1"];
|||;
}

  1. A network layer service guarantees that network will never live during more than 100 seconds inside the network. A reliable connection-oriented transport protocol places a 32 bits sequence number inside each segment. What is the maximum rate (in segments per second) at which is should sent data segments to prevent having two segments with the same sequence number inside the network ?

Using the socket API

Networked applications were usually implemented by using the socket API. This API was designed when TCP/IP was first implemented in the Unix BSD operating system [Sechrest] [LFJLMT], and has served as the model for many APIs between applications and the networking stack in an operating system. Although the socket API is very popular, other APIs have also been developed. For example, the STREAMS API has been added to several Unix System V variants [Rago1993]. The socket API is supported by most programming languages and several textbooks have been devoted to it. Users of the C language can consult [DC2009], [Stevens1998], [SFR2004] or [Kerrisk2010]. The Java implementation of the socket API is described in [CD2008] and in the Java tutorial. In this section, we will use the python implementation of the socket API to illustrate the key concepts. Additional information about this API may be found in the socket section of the python documentation .

The socket API is quite low-level and should be used only when you need a complete control of the network access. If your application simply needs, for instance, to retrieve data from a web server, there are much simpler and higher-level APIs.

A detailed discussion of the socket API is outside the scope of this section and the references cited above provide a detailed discussion of all the details of the socket API. As a starting point, it is interesting to compare the socket API with the service primitives that we have discussed in the previous chapter. Let us first consider the connectionless service that consists of the following two primitives :

  • DATA.request(destination,message) is used to send a message to a specified destination. In this socket API, this corresponds to the send method.
  • DATA.indication(message) is issued by the transport service to deliver a message to the application. In the socket API, this corresponds to the return of the recv method that is called by the application.

The DATA primitives are exchanged through a service access point. In the socket API, the equivalent to the service access point is the socket. A socket is a data structure which is maintained by the networking stack and is used by the application every time it needs to send or receive data through the networking stack. The socket method in the python API takes two main arguments :

  • an address family that specifies the type of address family and thus the underlying networking stack that will be used with the socket. This parameter can be either socket.AF_INET or socket.AF_INET6. socket.AF_INET, which corresponds to the TCP/IPv4 protocol stack is the default. socket.AF_INET6 corresponds to the TCP/IPv6 protocol stack.
  • a type indicates the type of service which is expected from the networking stack. socket.STREAM (the default) corresponds to the reliable bytestream connection-oriented service. socket.DGRAM corresponds to the connectionless service.

A simple client that sends a request to a server is often written as follows in descriptions of the socket API.

import socket
import sys
HOSTIP=sys.argv[1]
PORT=int(sys.argv[2])
MSG="Hello, World!"
s = socket.socket( socket.AF_INET6, socket.SOCK_DGRAM ) 
s.sendto( MSG, (HOSTIP, PORT,0,0) )

A typical usage of this application would be

python client.py ::1 12345

where ::1 is the IPv6 address of the host (in this case the localhost) where the server is running and 12345 the port of the server.

The first operation is the creation of the socket. Two parameters must be specified while creating a socket. The first parameter indicates the address family and the second the socket type. The second operation is the transmission of the message by using sendto to the server. It should be noted that sendto takes as arguments the message to be transmitted and a tuple that contains the IPv6 address of the server and its port number.

The code shown above supports only the TCP/IPv6 protocol stack. To use the TCP/IPv4 protocol stack the socket must be created by using the socket.AF_INET address family. Forcing the application developer to select TCP/IPv4 or TCP/IPv6 when creating a socket is a major hurdle for the deployment and usage of TCP/IPv6 in the global Internet [Cheshire2010]. While most operating systems support both TCP/IPv4 and TCP/IPv6, many applications still only use TCP/IPv4 by default. In the long term, the socket API should be able to handle TCP/IPv4 and TCP/IPv6 transparently and should not force the application developer to always specify whether it uses TCP/IPv4 or TCP/IPv6.

Another important issue with the socket API as supported by python is that it forces the application to deal with IP addresses instead of dealing directly with domain names. This limitation dates from the early days of the socket API in Unix 4.2BSD. At that time, name resolution was not widely available and only IP addresses could be used. Most applications rely on DNS names to interact with servers and this utilisation of the DNS plays a very important role to scale web servers and content distribution networks. To use domain names, the application needs to perform the DNS resolution by using the getaddrinfo method. This method queries the DNS and builds the sockaddr data structure which is used by other methods of the socket API. In python, getaddrinfo takes several arguments :

  • a name that is the domain name for which the DNS will be queried
  • an optional port number which is the port number of the remote server
  • an optional address family which indicates the address family used for the DNS request. socket.AF_INET (resp. socket.AF_INET6) indicates that an IPv4 (IPv6) address is expected. Furthermore, the python socket API allows an application to use socket.AF_UNSPEC to indicate that it is able to use either IPv4 or IPv6 addresses.
  • an optional socket type which can be either socket.SOCK_DGRAM or socket.SOCK_STREAM

In today’s Internet hosts that are capable of supporting both IPv4 and IPv6, all applications should be able to handle both IPv4 and IPv6 addresses. When used with the socket.AF_UNSPEC parameter, the socket.getaddrinfo method returns a list of tuples containing all the information to create a socket.

import socket
socket.getaddrinfo('www.example.net',80,socket.AF_UNSPEC,socket.SOCK_STREAM)
[ (30, 1, 6, '', ('2001:db8:3080:3::2', 80, 0, 0)),
  (2, 1, 6, '', ('203.0.113.225', 80))]

In the example above, socket.getaddrinfo returns two tuples. The first one corresponds to the sockaddr containing the IPv6 address of the remote server and the second corresponds to the IPv4 information. Due to some peculiarities of IPv6 and IPv4, the format of the two tuples is not exactly the same, but the key information in both cases are the network layer address (2001:db8:3080:3::2 and 203.0.113.225) and the port number (80). The other parameters are seldom used.

socket.getaddrinfo can be used to build a simple client that queries the DNS and contact the server by using either IPv4 or IPv6 depending on the addresses returned by the socket.getaddrinfo method. The client below iterates over the list of addresses returned by the DNS and sends its request to the first destination address for which it can create a socket. Other strategies are of course possible. For example, a host running in an IPv6 network might prefer to always use IPv6 when IPv6 is available [1].


import socket
import sys
HOSTNAME=sys.argv[1]
PORT=int(sys.argv[2])
MSG="Hello, World!"
for a in socket.getaddrinfo(HOSTNAME, PORT, socket.AF_UNSPEC,socket.SOCK_DGRAM,0, socket.AI_PASSIVE) :
    address_family,sock_type,protocol,canonicalname, sockaddr=a
    try:
        s = socket.socket(address_family, sock_type) 
    except socket.error:
        s = None
        print "Could not create socket"
        continue
    if s is not None:
        s.sendto(MSG, sockaddr)
        break

Now that we have described the utilisation of the socket API to write a simple client using the connectionless transport service, let us have a closer look at the reliable byte stream transport service. As explained above, this service is invoked by creating a socket of type socket.SOCK_STREAM. Once a socket has been created, a client will typically connect to the remote server, send some data, wait for an answer and eventually close the connection. These operations are performed by calling the following methods :

  • socket.connect : this method takes a sockaddr data structure, typically returned by socket.getaddrinfo, as argument. It may fail and raise an exception if the remote server cannot be reached.
  • socket.send : this method takes a string as argument and returns the number of bytes that were actually sent. The string will be transmitted as a sequence of consecutive bytes to the remote server. Applications are expected to check the value returned by this method and should resend the bytes that were not send.
  • socket.recv : this method takes an integer as argument that indicates the size of the buffer that has been allocated to receive the data. An important point to note about the utilisation of the socket.recv method is that as it runs above a bytestream service, it may return any amount of bytes (up to the size of the buffer provided by the application). The application needs to collect all the received data and there is no guarantee that some data sent by the remote host by using a single call to the socket.send method will be received by the destination with a single call to the socket.recv method.
  • socket.shutdown : this method is used to release the underlying connection. On some platforms, it is possible to specify the direction of transfer to be released (e.g. socket.SHUT_WR to release the outgoing direction or socket.SHUT_RDWR to release both directions).
  • socket.close: this method is used to close the socket. It calls socket.shutdown if the underlying connection is still open.

With these methods, it is now possible to write a simple HTTP client. This client operates over both IPv6 and IPv4 and writes the main page of the remote server on the standard output. It also reports the number of socket.recv calls that were used to retrieve the homepage [2] . We will provide more details on the HTTP protocol that is used in this example later.

#!/usr/bin/python 
# A simple http client that retrieves the first page of a web site

import socket, sys

if len(sys.argv)!=3 and len(sys.argv)!=2:
    print "Usage : ",sys.argv[0]," hostname [port]"

hostname = sys.argv[1]
if len(sys.argv)==3 :
    port=int(sys.argv[2])
else:
    port = 80

READBUF=16384   # size of data read from web server
s=None

for res in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM): 
    af, socktype, proto, canonname, sa = res
    # create socket
    try:
        s = socket.socket(af, socktype, proto)
    except socket.error:
        s = None
        continue
    # connect to remote host
    try:
        print "Trying "+sa[0]
        s.connect(sa)
    except socket.error, msg:
        # socket failed
        s.close()
        s = None
        continue
    if s :
        print "Connected to "+sa[0]
        s.send('GET / HTTP/1.1\r\nHost:'+hostname+'\r\n\r\n')
        finished=False
        count=0
        while not finished:
            data=s.recv(READBUF)
            count=count+1
            if len(data)!=0:
                print repr(data)
            else:
                finished=True
        s.shutdown(socket.SHUT_WR)        
        s.close()
        print "Data was received in ",count," recv calls"
        break

The second type of applications that can be written by using the socket API are the servers. A server is typically runs forever waiting to process requests coming from remote clients. A server using the connectionless will typically start with the creation of a socket with the socket.socket. This socket can be created above the TCP/IPv4 networking stack (socket.AF_INET) or the TCP/IPv6 networking stack (socket.AF_INET6), but not both by default. If a server is willing to use the two networking stacks, it must create two threads, one to handle the TCP/IPv4 socket and the other to handle the TCP/IPv6 socket. It is unfortunately impossible to define a socket that can receive data from both networking stacks at the same time with the python socket API.

A server using the connectionless service will typically use two methods from the socket API in addition to those that we have already discussed.

  • socket.bind is used to bind a socket to a port number and optionally an IP address. Most servers will bind their socket to all available interfaces on the servers, but there are some situations where the server may prefer to be bound only to specific IP addresses. For example, a server running on a smartphone might want to be bound to the IP address of the WiFi interface but not on the 3G interface that is more expensive.
  • socket.recvfrom is used to receive data from the underlying networking stack. This method returns both the sender’s address and the received data.

The code below illustrates a very simple server running above the connectionless transport service that simply prints on the standard output all the received messages. This server uses the TCP/IPv6 networking stack.

import socket, sys

PORT=int(sys.argv[1])
BUFF_LEN=8192

s=socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.bind(('',PORT,0,0))
while True:
    data, addr = s.recvfrom( BUFF_LEN ) 
    if data=="STOP" :
        print "Stopping server"
        sys.exit(0)
    print "received from ", addr, " message:", data

A server that uses the reliable byte stream service can also be built above the socket API. Such a server starts by creating a socket that is bound to the port that has been chosen for the server. Then the server calls the socket.listen method. This informs the underlying networking stack of the number of transport connection attempts that can be queued in the underlying networking stack waiting to be accepted and processed by the server. The server typically has a thread waiting on the socket.accept method. This method returns as soon as a connection attempt is received by the underlying stack. It returns a socket that is bound to the established connection and the address of the remote host. With these methods, it is possible to write a very simple web server that always returns a 404 error to all GET requests and a 501 errors to all other requests.

# An extremely simple HTTP server

import socket, sys, time

# Server runs on all IP addresses by default
HOST=''
# 8080 can be used without root priviledges
PORT=8080 
BUFLEN=8192 # buffer size

s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
try:
    print "Starting HTTP server on port ", PORT
    s.bind((HOST,PORT,0,0))
except socket.error :
    print "Cannot bind to port :",PORT
    sys.exit(-1)

s.listen(10) # maximum 10 queued connections

while True:
    # a real server would be multithreaded and would catch exceptions
    conn, addr = s.accept()
    print "Connection from ", addr
    data=''
    while not '\n' in data :  # wait until first line has been received
        data = data+conn.recv(BUFLEN) 
    if data.startswith('GET'):
        # GET request
        conn.send('HTTP/1.0 404 Not Found\r\n')
        # a real server should serve files
    else:
        # other type of HTTP request
        conn.send('HTTP/1.0 501 Not implemented\r\n')

    now = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
    conn.send('Date: ' + now +'\r\n')
    conn.send('Server: Dummy-HTTP-Server\r\n')
    conn.send('\r\n')
    conn.shutdown(socket.SHUT_RDWR)
    conn.close()

This server is far from a production-quality web server. A real web server would use multiple threads and/or non-blocking IO to process a large number of concurrent requests [3] . Furthermore, it would also need to handle all the errors that could happen while receiving data over a transport connection. These are outside the scope of this section and additional information on more complex networked applications may be found elsewhere. For example, [RG2010] provides an in-depth discussion of the utilisation of the socket API with python while [SFR2004] remains an excellent source of information on the socket API in C.

Practice

  1. The socket interface allows you to use the UDP protocol that provides the connectionless service on a Unix host. UDP, in theory, allows you to send SDUs of up to 64 KBytes.
  • Implement a small UDP client and a small UDP server in C
  • run the client and the servers on different workstations to determine experimentally the largest SDU that is supported by your language and OS.
  1. The time protocol, defined in RFC 868 allows to read the current time on a remote host. Implement this very simple protocol on top of the UDP and TCP sockets. Compare the time required to retrieve this time information over UDP and TCP.
  2. By using the socket interface, implement on top of the connectionless unreliable service provided by UDP a simple client that sends the message shown in the figure below.

In this message, the bit flags should be set to 01010011b, the value of the 16 bits field must be the square root of the value contained in the 32 bits field, the character string must be an ASCII representation (without any trailing 0) of the number contained in the 32 bits character field. The last 16 bits of the message contain an Internet checksum that has been computed over the entire message.

Upon reception of a message, the server verifies that :

  • the flag has the correct value
  • the 32 bits integer is the square of the 16 bits integer
  • the character string is an ASCII representation of the 32 bits integer
  • the Internet checksum is correct

If the verification succeeds, the server returns a SDU containing 11111111b. Otherwise it returns 01010101b

Your implementation must be able to run on both low endian and big endian machines. If you have access to different types of machines (e.g. x86 laptops and SPARC servers), try to run your implementation on both types of machines.

  1. The socket library is also used to develop applications above the reliable bytestream service provided by TCP. We have implemented in C language a small application that sends information to a server on port 62141. This server should operate as follows :
  • the server listens on port 62141 for a TCP connection

  • upon the establishment of a TCP connection, the server sends an integer by using the following TLV format :

    • the first two bits indicate the type of information (01 for ASCII, 10 for boolean)
    • the next six bits indicate the length of the information (in bytes)
    • An ASCII TLV has a variable length and the next bytes contain one ASCII character per byte. A boolean TLV has a length of one byte. The byte is set to 00000000b for true and 00000001b for false.
  • the client replies by sending the received integer encoded as a 32 bits integer in network byte order

  • the server returns a TLV containing true if the integer was correct and a TLV containing false otherwise and closes the TCP connection

Implement a server in C that interacts with our client available at /exercises/c/tlv_ex.c

  1. The Trivial File Transfer Protocol (TFTP), defined in RFC 1350 is a simple file transfer protocol that runs on top of UDP. Read the specification to answer the following questions :

    • What is the maximum length of the DATA segments exchanged by this protocol ?
    • What is the legnth of the header in DATA segments ?

    • What is the first sequence number for DATA segments ?

    • Do sequence number count the segments or the bytes that are transmitted ?

    • Does this protocol uses a sliding window ?

    • How does the data transfer ends ? Consider two different files. The first one has a length of exactly 1024 bytes, the second 513 bytes. Explain what is the last segment sent in each direction in each case.

Discussion questions

  1. In the transport layer, the receive window advertised by a receiver can vary during the lifetime of the connection. What are the causes for these variations ?
  2. A reliable connection-oriented protocol can provide a message-mode service or a byte stream service. Which of the following usages of the sequence numbers is the best suited for each of these services ?
  1. DATA segments contain a sequence number that is incremented for each byte transmitted
  2. DATA segments contain a sequence number that is incremented for each DATA segment transmitted
  1. Some transport protocols use 32 bits sequence numbers while others use 64 bits sequence number. What are the advantages and drawbacks of each approach ?
  2. Consider a transport protocol that provides the bytestream service and uses 32 bits sequence number to represent the position of the first byte of the payload of DATA segments in the bytestream. How would you modify this protocol so that it can provide a message-mode service ? Consider first short messages that always fit inside a single segment. In a second step, discuss how you could support messages of unlimited size.
  3. What is piggybacking and what are the benefits of this technique ?

Footnotes

[1]Most operating systems today by default prefer to use IPv6 when the DNS returns both an IPv4 and an IPv6 address for a name. See http://ipv6int.net/systems/ for more detailed information.
[2]Experiments with the client indicate that the number of socket.recv calls can vary at each run. There are various factors that influence the number of such calls that are required to retrieve some information from a server. We’ll discuss some of them after having explained the operation of the underlying transport protocol.
[3]There are many production quality web servers software available. apache is a very complex but widely used one. thttpd and lighttpd are less complex and their source code is probably easier to understand.