Lecture 24: demos

We played with a bunch of tools:

We also did a demo using sockets in Python.

We played with Wireshark, which let us put the network card into promiscuous mode and observe lots of traffic. You are encouraged to download and play on your own.

We looked at /etc/services, which contains a list of the well-known port numbers

vi /etc/services

We played with traceroute, which lists the routers that a packet traverses while en route to a given destination:

$ traceroute google.com
traceroute to google.com (74.125.228.206), 30 hops max, 60 byte packets
 1  rhodes1-6500-vl2729.net.cornell.edu (10.148.0.1)  3.986 ms  3.971 ms  3.961 ms
 2  core2-6500-te3-3.net.cornell.edu (132.236.222.161)  3.954 ms  3.947 ms  3.944 ms
 3  nat1-3700d-vl25-inside.net.cornell.edu (10.253.34.5)  3.259 ms  3.617 ms  3.751 ms
 4  core1-6500-vl26.net.cornell.edu (128.253.34.34)  4.379 ms  4.374 ms  4.367 ms
 5  cornellnet4-te1-1.net.cornell.edu (128.253.222.10)  4.478 ms  4.844 ms  5.155 ms
 6  te0-0-1-2.rcr11.syr01.atlas.cogentco.com (38.122.120.21)  7.435 ms  4.738 ms  4.649 ms
 7  te0-0-0-16.ccr21.alb02.atlas.cogentco.com (154.54.27.165)  8.045 ms  6.841 ms  8.184 ms
 8  be2106.ccr41.jfk02.atlas.cogentco.com (154.54.3.49)  11.533 ms  12.145 ms  11.771 ms
 9  be2148.ccr41.dca01.atlas.cogentco.com (154.54.31.117)  17.926 ms  19.359 ms  19.488 ms
10  be2171.ccr41.iad02.atlas.cogentco.com (154.54.31.106)  19.074 ms  20.259 ms  20.430 ms
11  38.88.214.50 (38.88.214.50)  18.550 ms  17.633 ms  17.164 ms
12  209.85.252.46 (209.85.252.46)  18.011 ms  18.002 ms  17.809 ms
13  72.14.233.91 (72.14.233.91)  18.482 ms  17.847 ms  18.244 ms
14  iad23s23-in-f14.1e100.net (74.125.228.206)  17.366 ms  18.360 ms  19.814 ms

We showed the netstat tool, which gives information about all currently open sockets.

We played with telnet tool, which creates a TCP connection to a given host and port and simply forwards the users terminal to that connection. We used it to perform HTTP requests from google:

$ telnet google.com 80
GET / HTTP/1.1
Host: google.com
<enter>

Sockets

Sockets are the unix (system call) API for interacting with TCP streams. They are available as an API in many languages.

There are two kinds of sockets:

We did some demos in python. To create a server, one creates a socket and binds it to the well-known port:

python
>>> from socket import *
>>> server = socket()
>>> print(server.bind.__doc__)
bind(address)

Bind the socket to a local address.  For IP sockets, the address is a
pair (host, port); the host must refer to the local host. For raw packet
sockets the address is a tuple (ifname, proto [,pkttype [,hatype]])
>>> server.bind(('localhost', 44100))

Note that the 'localhost' parameter indicates which IP address on the local host to bind to.

At this point, the port has been reserved, but isn't yet accepting connections:

shell$ telnet localhost 44100

python
>>> s2 = socket()
>>> s2.bind(('localhost',44100))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 98] Address already in use

To start accepting connections, we must call listen:

>>> print(server.listen.__doc__)
listen([backlog])

Enable a server to accept connections.  If backlog is specified, it must be
at least 0 (if it is lower, it is set to 0); it specifies the number of
unaccepted connections that the system will allow before refusing new
connections. If not specified, a default reasonable value is chosen.
>>> server.listen()

shell$ telnet localhost 44100
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

At this point, the 3-way TCP handshake between the telnet client and the python server has been performed. Notice that the client has been given an ephemeral port number:

shell$ netstat -t
tcp        0      0 localhost:50822         localhost:44100         ESTABLISHED
tcp        0      0 localhost:44100         localhost:50822         ESTABLISHED

We are using telnet as the client; if we were to write the client in python (or look at the source code of telnet) we would create a client socket and call connect, providing the remote host and port:

>>> s = socket()
>>> print(s.connect.__doc__)
connect(address)

Connect the socket to a remote address.  For IP sockets, the address
is a pair (host, port).
>>> s.connect(('localhost',4410))

Calling connect makes the socket a client socket; had we called bind and listen it would be a server socket.

Returning to the server, we want to get a client socket representing our telnet client's connection. We wait for a connection by calling accept:

>>> print(server.accept.__doc__)
accept() -> (socket object, address info)

    Wait for an incoming connection.  Return a new socket
    representing the connection, and the address of the client.
    For IP sockets, the address info is a pair (hostaddr, port).
    
>>> sock,addr = server.accept()
>>> sock
<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 4410), raddr=('127.0.0.1', 50822)>
>>> addr
('127.0.0.1', 50822)

Accept will block until a new client connects; when it does, it creates and returns a client socket. In this case, we had already connected, so accept returns immediately. If we called accept again, it would block until a second client connected.

At this point, we can call send and recv on sock to send data to the client and receive data from the client.

>>> print(sock.send.__doc__)
send(data[, flags]) -> count

Send a data string to the socket.  For the optional flags
argument, see the Unix manual.  Return the number of bytes
sent; this may be less than len(data) if the network is busy.
>>> print(sock.recv.__doc__)
recv(buffersize[, flags]) -> data

Receive up to buffersize bytes from the socket.  For the optional flags
argument, see the Unix manual.  When no data is available, block until
at least one byte is available or until the remote end is closed.  When
the remote end is closed and all data is read, return the empty string.
>>> sock.send(bytes('hello\n','utf-8'))
6
[in telnet window we see 'hello', and type 'sup?<enter>]
>>> sock.recv(3)
b'sup'
>>> sock.recv(5)
b'?\r\n'

Note that recv does not wait until all requested bytes are available, instead, it blocks until there are any bytes available, and then returns however many bytes are available (to a maximum of the request).

Eventually, when we are done sending data, we can call close; this sends the FIN packet, and then cleans up the send buffer when the FIN has been acknowleged. Note that you can still receive data after calling close, but you cannot send more data.

If we were writing a real server, we would want to simultaneously wait for new connections and wait for data from the connections we have established. A very common pattern is to have a server loop that continuously calls accept and then forks off a new thread to handle each connection.