Reliable protocol on top of UDP

1. Demo

I’m drawn towards starting with a program that will provide an easy way to demonstrate the protocol and letting that program guide the construction of the protocol.

I’m thinking something like: let the user type lines into a terminal and send each line over the wire and provide some feedback about acknowledgments, failures, and retries.

That means we’ll have both a “client” and a “server” running in the same “demo” process.

It will be easiest to start with asymmetrical data flow. Our client will send packets containing a header and data. The server will respond with just a header. The header will contain things like sequence numbers and acknowledgment flags.

The overall structure I’m thinking of looks something like this.

/demo.go
package main

@{demo imports}

func parseIp4(ip string) [4]byte {
   netIp := net.ParseIP(ip)
   return [4]byte{netIp[12], netIp[13], netIp[14], netIp[15]}
}

func main() {
     @{handle command line options}
     @{instantiate a connection}
     @{run a "server"}
     @{present the user with a terminal to send/receive}
}

Command line options

I’m going to need two sockets: one for sending data and another for receiving acknowledgments.

Binding to a receive port of 0 will ask the kernel to allocate some random unused port.

handle command line options
sendaddr := flag.String("address", "127.0.0.1", "destination address")
sendport := flag.Int("port", 8888, "destination port")
recvaddr := flag.String("ackaddress", "127.0.0.1", "address for receiving acknowledgments")
recvport := flag.Int("ackport", 0, "port for receiving acknowledgments")
flag.Parse()

Used by 1

Instantiating a connection will involve creating some state.

I’ll store the state in a struct, RdtConnection.

That struct will have sockets for each of sending and receiving. It will also have fields to store things like sequence number and status (idle, awaiting ack, etc…).

instantiate a connection
sendip := parseIp4(*sendaddr)
sendsockaddr := &syscall.SockaddrInet4{Addr: sendip, Port: *sendport}
recvip := parseIp4(*recvaddr)
recvsockaddr := &syscall.SockaddrInet4{Addr: recvip, Port: *recvport}

con, err := rdt.New(sendsockaddr, recvsockaddr)
if err != nil {
   panic(err)
}

Used by 1

The server will receive on the client’s send address/port. We can’t know where the server will send until after the client has created its connection and gotten a random port from the kernel. We’ll eventually get the client’s address from the result of syscall.Recvfrom.

run a "server"
recvsockaddr = &syscall.SockaddrInet4{Addr: sendip, Port: *sendport}
servercon, err := rdt.New(&syscall.SockaddrInet4{}, recvsockaddr)
if err != nil {
   panic(err)
}
go func() {
   for {
       rdtp, from, err := servercon.Recv()
       if err != nil {
          panic(err)
       }
       fmt.Printf("Server recv %v from %v\n", rdtp, from)
       err = servercon.Send([]byte{}, rdt.ACK)
   }
}()

Used by 1

User terminal for demo

present the user with a terminal to send/receive
for {
    @{read line from stdin}
    @{break if input was an empty line}
    @{pack line into an rdt packet}
    @{send the rdt packet over the connection}
    @{wait for and log acknowledgment}
}

Used by 1

I first thought the fmt.Scanln function would be nice for this. But I hadn’t read it carefully enough. It treats space-separated words in the line as tokens that get saved in its arguments. If you have a different number of space-separated words than the number of arguments passed to Scanln, then it errors.

Instead, I’ll use reader.ReadString.

func (*Reader) ReadString ΒΆ
func (b *Reader) ReadString(delim byte) (string, error)

ReadString reads until the first occurrence of delim in the input, returning a string containing the data up to and including the delimiter. If ReadString encounters an error before finding a delimiter, it returns the data read before the error and the error itself (often io.EOF). ReadString returns err != nil if and only if the returned data does not end in delim. For simple uses, a Scanner may be more convenient.
read line from stdin
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
    panic(err)
}

Used by 1

break if input was an empty line
if line == "" {
   break
}

Used by 1

send the rdt packet over the connection
con.Send([]byte(line), 0)

Used by 1

To really demonstrate guaranteed delivery I thought it would be cool to disable terminal input while a packet was in-flight. That looks to be more difficult than it sounds. So I’ll probably let the user continue to type. But don’t let that fool you into thinking what you type is actually getting sent.

wait for and log acknowledgment
ack, from, err := con.Recv()
if err != nil {
    panic(err)
}
fmt.Printf("Received %s from %v", ack, from)

Used by 1

2. Reliable Data Transport (rdt) package

And the rdt package will have:

/rdt/rdt.go
package rdt

@{rdt imports}

@{rdt packet struct}
@{rdt connection struct}
@{new rdt connection constructor}

The client and server will be goroutines with some channels for communication. They’ll basically be infinite loops of sending messages, awaiting responses, and timers that timeout and trigger other actions.

The terminal in that last line of the main function will communicate with the client over a channel.

That’s the high level idea.

In the meantime, let’s get a tiny little wrapper around UDP that we can start hooking into.

3. Stateful wrapper around UDP

My initial idea is for each connection to keep track of a sequence number and status.

The status will be something like: waiting for data from the application above us in the network stack, or data sent and waiting for acknowledgment from remote connection.

The sequence number, for starters, can be just 0 or 1. We’ll start with a real simple protocol that blocks awaiting acknowledgment after every single message. No pipelining or buffering.

Assume Alice and Bob have a connection.

If Bob is expecting a packet with sequence number 0 but receives a packet with sequence number 1, then Bob responds with a “negative acknowledgment”. That’s sufficient as long as Alice will wait to proceed and will resend the packet with sequence number 0 if/when she gets that negative acknowledgment.

Alice will have a timeout and keep resending whatever packet she’s on. If Bob’s [negative] acknowledgment gets lost, then his [negative] acknowledgment will get resent after Alice’s timeout gets resent.

Along with sequence number and status, we’ll maintain information about our connection: the socket and sockaddr. By storing those on this struct, we can have methods that operate on the connection and not have to pass these in as arguments every time.

One last bit of state we’ll want to have is some sort of timer to track how long it’s been between sending a message and receiving a response. If we haven’t received a response by the time the timer expires, then we’d want to resend.

In C, I think we could use something like timer_create. But I don’t think we get any kind of syscall access into Linux timer interrupts from Golang. So instead, I’ll spin off some goroutines to act as timers and I’ll communicate the status of the timers over channels.

rdt connection struct
type Status int

const (
      IDLE Status = iota
      WAIT_ACK
)

type RdtConnection struct {
    sequenceNumber int
    status Status
    sendSocket int
    sendSockaddr syscall.SockaddrInet4
    recvSocket int
    recvSockaddr syscall.SockaddrInet4
    timerChan chan interface{}
}

Used by 1

TODO: I don’t yet know what kinds of messages I’ll want to send to the timer channel. I expect it might be things like start, stop, reset, and maybe a message to set the timer to a new value.

new rdt connection constructor
func New(sendSockaddr, recvSockaddr *syscall.SockaddrInet4) (RdtConnection, error) {
     var rdtcon RdtConnection
     var err error
     @{initialize rdt connection}
     return rdtcon, err
}

Used by 1

initialize rdt connection
@{initialize connection state}
@{bind socket}

Used by 1

initialize connection state
rdtcon.sequenceNumber = 0
rdtcon.status = IDLE
rdtcon.sendSockaddr = *sendSockaddr
rdtcon.sendSocket, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
if err != nil {
   return rdtcon, err
}
rdtcon.recvSockaddr = *recvSockaddr
rdtcon.recvSocket, err = syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
if err != nil {
   return rdtcon, err
}
rdtcon.timerChan = make(chan interface{})

Used by 1

bind socket
err = syscall.Bind(rdtcon.sendSocket, &rdtcon.sendSockaddr)
if err != nil {
   return rdtcon, err
}

Used by 1

4. Client and server

Each of the client and server will have an RdtConnection and a channel for communication. The client channel m

5. Rdt packet struct

How are we going to package up our reliable data transmission packet into a UDP payload? What are the things we need?

I think that’s all.

Before we start packing binary data, let’s create a struct and some helper functions to deal with it.

This is a good time to note that, for now, I’m only considering unidirectional transfer.

Why do I bring this up now?

I’m seeing that this RdtPacket struct has an AckType field that will only be used by the receiver to send [N]ACKs to the sender and a Data field that will only be used by the sender to send data to the receiver. It might make more sense to have two different kinds of packet structures. But I’m going to just use one and leave those fields as some arbitrary default “nil” value when they aren’t used.

rdt packet struct
type AckType int

const (
      ACK AckType = iota
      NACK
)

type RdtPacket struct {
     SequenceNumber int
     AckType AckType
     // TODO: Checksum
     Data []byte
}

@{send packet method}
@{receive packet method}

Used by 1

To populate the RdtPacket, we need some of the state from the RdtConnection; the current SequenceNumber, for example.

It makes sense to create a SendPacket method on the RdtConnection struct.

send packet method
func (con RdtConnection) Send(data []byte, acktype AckType) error {
     var headerByte byte
     headerByte = byte(acktype) << 1 | byte(con.sequenceNumber)
     buf := new(bytes.Buffer)
     binary.Write(buf, binary.BigEndian, headerByte)
     binary.Write(buf, binary.BigEndian, data)
     return syscall.Sendto(con.sendSocket, buf.Bytes(), 0, &con.sendSockaddr)
}

Used by 1

receive packet method
func (con RdtConnection) Recv() (rdt RdtPacket, from syscall.SockaddrInet4, err error) {
    recvbuf := make([]byte, 2048)
    var rdtPacket RdtPacket
    _, sa, err := syscall.Recvfrom(con.recvSocket, recvbuf, 0)
    switch sa := sa.(type) {
    case *syscall.SockaddrInet4:
        if err != nil {
            return rdtPacket, *sa, err
        }
        con.sendSockaddr = *sa
        header := recvbuf[0]
        rdtPacket.SequenceNumber = int(header) & (1)
        ackType := header & (1 << 1)
        if ackType == 1<<1 {
            rdtPacket.AckType = ACK
        } else {
            rdtPacket.AckType = NACK
        }
        rdtPacket.Data = recvbuf[1:]
        return rdtPacket, *sa, err
    }
    return
}

Used by 1

After writing that method to send the packet, I think a struct for the packet is overkill. This packet is pretty basic at the moment. Maybe the struct will be more useful when we add things like checksums and bidirectional communication.

pack line into an rdt packet

Used by 1

demo import
"bufio"
"flag"
"fmt"
"net"
"os"
"owoga.com/rdt/rdt"
"syscall"

Used by 1

demo imports
import (
    @{demo import}
)

Used by 1

rdt import
"bytes"
"encoding/binary"
"syscall"

Used by 1

rdt imports
import (
    @{rdt import}
)

Used by 1