Communicating Reliably

Wtf Did You Say?

You might have found it a bit strange that I literally said that the transport layer is all about making sure that we are providing reliable communication, and yet we did actually jack shit in UDP to make it so that our communication is reliable.

How can we have reliable communication though? Lets build up some intuitive ideas, and then use those to describe one of the most famous protocols used across the entire internet. The early designers actually cooked some good stuff with TCP going to be so honest 🧑‍🍳

Normally, this is where I would theory craft the idea of a super cool transport layer protocol that’s reliable, but I’ll leave that to be a discussion

Transmission Control Protocol

The thought process of TCP is that we are working with a continuous byte stream that sometimes shuffles or drops our data. For this, we don’t really care how that data is divided up into packets, when we give sequence numbers, the number does not represent the packet number, rather the sequence and ACK numbers represent the byte numbers that we are on.

TCP works through the idea of Acknowledged data, every time that data is sent, the send expects to receive back a new piece of data that acknowledges that previous piece. Specifically, TCP uses what is called “cumulative acknowledgements” which means that if we ACK all bytes prior to the number provided. For example, if I say ACK 36, this means that I acknowledge all bytes of data prior to byte 36, and am now ready to receive byte 36. As for why we do this we will see some advantages soon.

Should an Acknowledgement not arrive, we will resend after waiting a specified timeout window. Normally I discuss more on how that window is calculated, but I don’t think I will have time this semester.

First, let us provide the packet structure of a TCP packet, so we can observe and refer back to the terminology.

The Three-Way Handshake

In TCP, a “connection” is opened between the two hosts so that the two of them can send data back and forth between each other. In this sense, there is not necessarily a client and a receiver, because in each packet each host can send data while simulataneously acknowledging the previous data that it has received.

For this reason, when we start up a new connection, not only does the initiator of the connection make a request to connect, but the receiver does as well. This is famously called the three-way handshake, and in a conversational way would go as follows between two people

Person A: Hey I wanna call you to talk some gossip Person B: Sounds good, I also want to tell you some gossip Person A: Sounds good, can you believe that ….

Notice that both person A and B are informing each other that they each want to speak on some gossip while also acknowledging the fact that the other person has information they want to speak as well. In the three-way handshake it would technically go like this

  1. Host A selects an initial sequence number $A_0$ and sends a packet to (B_ip, B_port) with $A_0$ as its sequence and the synchronize (SYN) bit set to $1$.
  2. Host B receives the synchronize request, opens a socket and TCP buffer for the connection, chooses an initial sequence number $B_0$, and sends a (A_ip, A_port) with $B_0$ as the sequence number, $A_0+1$1 as the acknowledgement number, and the ACK and SYN flags both set to $1$.
  3. Host A responds to Host B by sending a packet with the SYN flag set to $0$, the ACK flag set to $1$, the sequence number of $A_0+1$, and the ACK number of $B_0+1$. A is also free to begin sending data inside of its data field.

The discussion of how we choose our initial sequence numbers will occur later on this page: its not necessarily $0$!

Sending Data

After the three-way handshake has been established, we need to determine how we send data during a normal communication flow. Before sending though, we need to understand exactly how much data we actually can send.

Congestion Control and Flow Control

For an analogy, imagine my friend Ariana wants to send a video to my other friend Devin over text. The video is an hour long, so in order to make it more manageable she breaks it into $10$ six minute chunks. In theory, she could send every single on of those chunks at the same time, but what if that causes the network to overload? What if Devin runs out of storage midway?

Rather, she should send a little at a time, and as Devin responds back letting her know he got them she can begin to send slightly more and more. This idea is what we use the receive window for.

Definition: The receive window is the amount of data that a receiver can handle, and is sent through TCP packets. The send window is the amount of bytes that the sender can send unacknowledged.

Note that the send window, and the receive window might not necessarily be the same, because the sender may be firing off data slower than the receiver could theoretically handle. Some problems with the receive window can be as follows

  1. If the receive window reaches $0$, then the sender would never want to send more data. As such, if the window ever reaches $0$, the sender will periodically send pings to the receiver in order to get window updates.
  2. If the receive window is very small and the sender continually sends data, then the small amount of data in the packets will not be worth the large TCP overhead (this is apparently called Silly Window Syndrome lol2)

TCP senders use a “sliding window” which means that the distance between the smallest and largest unacknowledged bytes cannot exceed the window. For a simplified example, imagine that we have TCP packets 1,2,3,4,5,6 that we want to send, with a send window of $3$. We send 1,2,3 all at once, but only get back the ACK for 2. We will not fire off 4 yet, because we consider the window of 1,2,3 still not acknowledged. Once we get back the ACK for 1, we can shoot off 4,5 and change our sent packets window to 3,4,5. Of course note that this would not be split via packets in a real case, but rather through bytes.

For the purposes of this class, the above situation can’t really happen, because most TCP utilizes what are called cumulative ACKs, but some psychopathic variations do have scenarios like this occur.

So far, the idea of managing our send window to make sure we do not overload the buffer of the receiver is called flow control, however there is another type of control that we use to make sure that we do not overload the network called congestion control. In essence, we will be keeping track of two different window sizes, one for congestion and one for flow, and then our send window is the smaller of the two.

Congestion control is extremely implementation specific, but I will go over one easy way that is often used

  1. Start with a single TCP packet as the send window and send it
  2. If no problems occur (dropped packets, ICMPs, duplicate ACKs) then double the send window size. Repeat until problem
  3. If problem occurs, halve the send window, and each iteration increase the send window by 1.

Bufferbloat

Cumulative ACKs

In most versions of TCP, we utilize what are called cumulative ACKs. This means that if I send out packets with sequence numbers 100, 200, 300 (remember this means that each packet would have $100$ bytes of data) then even if the ACKs for 100 and 200 get dropped, as long as I get the ACK for 300 (which would have ACK number $301$) I can mark all of that data as acknowledged because ACK 301 means that everything prior to 301 has been received a-ok.

This also allows for senders to perform a “fast retransmit” instead of waiting for a timeout. In the event we receive four copies of the same ACK number, called a “triple duplicate ACK”, we can be sufficiently sure that something is messed up and retransmit accordingly.

Closing Connections

In TCP, there is a similar handshake to close a connection. The sender will initiate a FIN, which will be Acknowledged by the receiver, who will then send their own FIN. Upon receiving the acknowledgements each client will begin closing their connection. In the event the FINACKs are lost, retransmissions will be attempted until the connection is eventually killed and closed.

Choosing Initial Sequence Numbers

TCP Code

TCP Client

import socket

serverName = "127.0.0.1"
serverPort = 12503
clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientSocket.connect((serverName, serverPort))
name = input("Name: ")
try:
    while True:
        message = input("Message: ")
        clientSocket.send((name + ": " + message).encode())
        message = clientSocket.recv(2048)
        print(message.decode())
finally:
    clientSocket.close()
    print("Connection Closed")

TCP Server

import socket

serverPort = 12503
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSocket.bind(("", serverPort))
name = input("Name: ")
serverSocket.listen(1)
print("Starting Server")
connections = []

conn, address = serverSocket.accept()
print("Connected with ", address)
try:
    while True:
        message = input("Message: ")
        conn.send((name + ": " + message).encode())
        message = conn.recv(2048)
        print(message.decode())
finally:
    serverSocket.close()
    print("Socket Closed")

  1. We increase the ACK number by $1$ because we are acknowledging the first packet being sent which contains the syn. ↩︎

  2. https://en.wikipedia.org/wiki/Silly_window_syndrome ↩︎

Previous
Next