Table of Content

Introduction

Through hands-on experience, we are

  • to be able to reproduce the programming patterns for simple applications using a transport protocol (UDP or TCP),
  • to be able to identify a transport protocol (between UDP and UDP) for an application,
  • to be able to explain packets captured for simple programs captured a transport protocol (UDP or TCP), and
  • to be able to develop hypothesis, collect data (packets), and debug a program that uses a transport protocol (UDP or TCP)

Experiment Environment

A few Linux systems that reachable to each other are needed. If using the virtual machines provided by the instructor, make sure you have X-Server set up on your host as discussed before.

UDP Experiments

UDP supports:

  • unicast
  • broadcast
  • multicast

We shall demonstrate the design for each.

Example Application

To demonstrate the programming pattern for each, we use a simple application:

  • the sender scans a directory that contains one or more JPEG images, and send the images
  • the receiver receives the images and displays the images. For convenience, let’s call this application, an online gallery.

Example Images

At beginning, you should use the images the instructor provides to you:

  • http://www.sci.brooklyn.cuny.edu/~chen/downloads/teach/cisc3340/transport/images.zip

On a Linux system command line, you download and extract the image files using the two steps below:

  1. wget www.sci.brooklyn.cuny.edu/~chen/downloads/teach/cisc3340/transport/images.zip
  2. unzip images.zip

If any of these two commands are not present in your Linux sytem, you can install them, e.g., on a Debian Linux, use

  • sudo apt-get install wget unzip

The images come with two versions, small files and large files. You should find them in the small and the large subdirectories once you unzip’ed the files.

The following version is implemented using the Socket API. The application consists of two parts, the receiver (show_gallery.py) and the sender (send_gallery.py). The following is the receiver (show_gallery.py):

"""Receive (via unicast) images sent by a sender using UDP

   show_gallery.py
"""

import argparse
import io
import socket

from matplotlib import animation
from matplotlib import image as mpimg
from matplotlib import pyplot as plt

MAX_PLAYLOD_SIZE = 65535 - 20 - 8


def parse_cmdline():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--bind_ip",
        type=str,
        help="The IP address of the receiver.",
        default="localhost",
        dest="bind_ip",
    )
    parser.add_argument(
        "--bind_port",
        type=int,
        help="The port number of the receiver.",
        default=25000,
        dest="bind_port",
    )
    return parser.parse_args()


class Gallery:
    """A simple gallery to display images in animation."""

    def __init__(self, images, interval=1000):
        self.images = images
        self.interval = interval
        self.fig, self.ax = plt.subplots()

    def display_frame(self, i):
        self.ax.clear()
        self.ax.imshow(self.images[i])

    def display_gallery(self):
        self.anim = animation.FuncAnimation(
            self.fig,
            self.display_frame,
            frames=len(self.images),
            interval=self.interval,
        )
        plt.show()
        self.anim = None


def receive_and_show_images(end_point):
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        sock.bind(end_point)
        print(f"bound to {end_point}")
        images = []
        idx = 0
        while True:
            img_data = sock.recv(MAX_PLAYLOD_SIZE)
            if not img_data:
                print("End of Transmission")
                break
            print(f"Image {idx}: received {len(img_data)} bytes.")
            img = mpimg.imread(io.BytesIO(img_data), "jpg")
            images.append(img)
            idx += 1
    gallery = Gallery(images)
    gallery.display_gallery()


def main():
    args = parse_cmdline()
    end_point = (args.bind_ip, args.bind_port)
    receive_and_show_images(end_point)
    print("Done.")


if __name__ == "__main__":
    main()

The following is the sender (send_gallery.py):

"""Send (via unicast) images in a directory to a receiver using UDP.


   send_gallery.py
"""

import argparse
import glob
import pathlib
import socket
import time

WAIT_INTERVAL = 3


def parse_cmdline():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--img_dir",
        type=str,
        help="The directory containing the images to send.",
        default="images/small",
        dest="img_dir",
    )
    parser.add_argument(
        "--to_ip",
        type=str,
        help="The IP address of the receiver.",
        default="localhost",
        dest="to_ip",
    )
    parser.add_argument(
        "--to_port",
        type=int,
        help="The port number of the receiver.",
        default=25000,
        dest="to_port",
    )
    return parser.parse_args()


def send_images(img_dir, destination):
    """Send images to a receiver using UDP.

    Args:
        img_dir (str): The directory containing the images to send.
        destination (str): the end point of the receiver, a tuple consiting of
                           the IP address and port number.
    """
    for img_idx, img_path in enumerate(
        glob.glob(str(pathlib.Path(img_dir).joinpath("*.jpg")))
    ):
        with open(img_path, "rb") as img_file:
            img_data = img_file.read()

        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
            s.sendto(img_data, destination)
            print(f"Image {img_idx}: sent {len(img_data)} bytes to {destination}.")
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        # Why do we need this? Is it a good way
        time.sleep(WAIT_INTERVAL)
        s.sendto("".encode(), destination)
        print("End of Transmission")


def main():
    args = parse_cmdline()
    destination = (args.to_ip, args.to_port)
    send_images(args.img_dir, destination)
    print("Done.")


if __name__ == "__main__":
    main()

To run the programs, assume that the images you downloaded is at the ./images and the pair of program files are at ./transport/udp/pysocket/unicast directory, we send the images to host at 10.1.1.34 and displays the images there:

python transport/udp/pysocket/unicast/show_gallery.py \
  --bind_ip 10.1.1.34 --bind_port 25000

and then,

python transport/udp/pysocket/unicast/send_gallery.py \
  --img_dir images/small --to_ip 10.1.1.34 --to_port 25000

The following version is implemented using the Socket API. The application consists of two parts, the receiver (show_gallery.py) and the sender (send_gallery.py). The receiver is identical to the unicast receiver. However, we do need to make a two-line but an important change to use broadcast over UDP:

"""Send (via broadcast) images in a directory to a receiver using UDP.

   send_gallery.py
"""

import argparse
import glob
import pathlib
import socket
import time

WAIT_INTERVAL = 3


def parse_cmdline():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--img_dir",
        type=str,
        help="The directory containing the images to send.",
        default="images/small",
        dest="img_dir",
    )
    parser.add_argument(
        "--to_ip",
        type=str,
        help="The IP address of the receiver.",
        default="localhost",
        dest="to_ip",
    )
    parser.add_argument(
        "--to_port",
        type=int,
        help="The port number of the receiver.",
        default=25000,
        dest="to_port",
    )
    return parser.parse_args()


def send_images(img_dir, destination):
    """Send images to a receiver using UDP.

    Args:
        img_dir (str): The directory containing the images to send.
        destination (str): the end point of the receiver, a tuple consiting of
                           the IP address and port number.
    """
    for img_idx, img_path in enumerate(
        glob.glob(str(pathlib.Path(img_dir).joinpath("*.jpg")))
    ):
        with open(img_path, "rb") as img_file:
            img_data = img_file.read()

        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            s.sendto(img_data, destination)
            print(f"Image {img_idx}: sent {len(img_data)} bytes to {destination}.")
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        # Why do we need this? Is it a good way
        time.sleep(WAIT_INTERVAL)
        s.sendto("".encode(), destination)
        print("End of Transmission")


def main():
    args = parse_cmdline()
    destination = (args.to_ip, args.to_port)
    send_images(args.img_dir, destination)
    print("Done.")


if __name__ == "__main__":
    main()

To demonstrate this program, we need minimally three hosts, a sender and two receivers, and the sender will broadcast the images to the receivers. We will also identify the broadcast address for the network. Below is an example run:

Run the receivers on multiple hosts:

python transport/udp/pysocket/unicast/show_gallery.py \
  --bind_ip 192.168.56.255 --bind_port 25000

and then run the sender:

python transport/udp/pysocket/broadcast/send_gallery.py \
  --img_dir images/small --to_ip 192.168.56.255 --to_port 25000

The multicast is more complex than broadcast and unicast. The following version is implemented using the Socket API. The application consists of two parts, the receiver (show_gallery.py) and the sender (send_gallery.py). The following is the receiver:

"""Receive (via multicast) images sent by a sender using UDP.

   show_gallery.py
"""

import argparse
import io
import socket
import struct

from matplotlib import animation
from matplotlib import image as mpimg
from matplotlib import pyplot as plt

MAX_PLAYLOD_SIZE = 65535 - 20 - 8


def parse_cmdline():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--mcast_ip",
        type=str,
        help="The muticast IP address of the receiver.",
        default="localhost",
        dest="mcast_ip",
    )
    parser.add_argument(
        "--mcast_port",
        type=int,
        help="The port number of the receiver.",
        default=25000,
        dest="mcast_port",
    )
    parser.add_argument(
        "--arrival_ip",
        type=str,
        help="The IP address of the NIC to receive the multicast datagrams.",
        default=25000,
        dest="arrival_ip",
    )
    return parser.parse_args()


class Gallery:
    """A simple gallery to display images in animation."""

    def __init__(self, images, interval=1000):
        self.images = images
        self.interval = interval
        self.fig, self.ax = plt.subplots()

    def display_frame(self, i):
        self.ax.clear()
        self.ax.imshow(self.images[i])
        self.ax.set_title(f"Image {i} at host {socket.gethostname()}")

    def display_gallery(self):
        self.anim = animation.FuncAnimation(
            self.fig,
            self.display_frame,
            frames=len(self.images),
            interval=self.interval,
        )
        plt.show()
        self.anim = None


def receive_and_show_images(mcast_end_point, arrival_ip):
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        sock.bind(mcast_end_point)
        print(f"bound to {mcast_end_point}")

        # This joins the socket to the intended multicast group. The implications
        # are two. It specifies the intended multicast group identified by the
        # multicast IP address.  This also specifies from which network interface
        # (NIC) the socket receives the datagrams for the intended multicast group.
        # It is important to note that socket.INADDR_ANY means the default network
        # interface in the system (ifindex = 1 if loopback interface present). To
        # receive multicast datagrams from multiple NICs, we ought to create a
        # socket for each NIC. Also note that we identify a NIC by its assigned IP
        # address.
        if arrival_ip == "0.0.0.0":
            mreq = struct.pack(
                "=4sl", socket.inet_aton(mcast_end_point[0]), socket.INADDR_ANY
            )
        else:
            mreq = struct.pack(
                "=4s4s",
                socket.inet_aton(mcast_end_point[0]),
                socket.inet_aton(arrival_ip),
            )
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

        images = []
        idx = 0
        while True:
            img_data = sock.recv(MAX_PLAYLOD_SIZE)
            if not img_data:
                print("End of Transmission")
                break
            print(f"Image {idx}: received {len(img_data)} bytes.")
            img = mpimg.imread(io.BytesIO(img_data), "jpg")
            images.append(img)
            idx += 1
    gallery = Gallery(images)
    gallery.display_gallery()


def main():
    args = parse_cmdline()
    end_point = (args.mcast_ip, args.mcast_port)
    receive_and_show_images(end_point, args.arrival_ip)
    print("Done.")


if __name__ == "__main__":
    main()

and the following the sender:

"""Send (via multicast) images in a directory to a receiver using UDP.


   send_gallery.py
"""

import argparse
import glob
import pathlib
import socket
import time

WAIT_INTERVAL = 3


def parse_cmdline():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--img_dir",
        type=str,
        help="The directory containing the images to send.",
        default="images/small",
        dest="img_dir",
    )
    parser.add_argument(
        "--mcast_ip",
        type=str,
        help="The muticast IP address of the receiver.",
        default="localhost",
        dest="mcast_ip",
    )
    parser.add_argument(
        "--mcast_port",
        type=int,
        help="The port number of the receiver.",
        default=25000,
        dest="mcast_port",
    )
    parser.add_argument(
        "--outbound_ip",
        type=str,
        help="The IP address of the NIC to send the multicast datagrams.",
        default="localhost",
        dest="outbound_ip",
    )
    return parser.parse_args()


def send_images(img_dir, destination, outbound_ip):
    """Send images to a receiver using UDP.

    Args:
        img_dir (str): The directory containing the images to send.
        destination (str): the multicast end point of the receiver, a tuple consiting of
                           the IP address and port number.
        outbound_ip (str): The IP address of the NIC to send the multicast datagrams.
    """
    for img_idx, img_path in enumerate(
        glob.glob(str(pathlib.Path(img_dir).joinpath("*.jpg")))
    ):
        with open(img_path, "rb") as img_file:
            img_data = img_file.read()

        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
            # This defines how many hops a multicast datagram can travel.
            # The IP_MULTICAST_TTL's default value is 1 unless we set it otherwise.
            s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)

            # This defines to which network interface (NIC) is responsible for
            # transmitting the multicast datagram; otherwise, the socket
            # uses the default interface (ifindex = 1 if loopback is 0)
            # If we wish to transmit the datagram to multiple NICs, we
            # ought to create a socket for each NIC.
            s.setsockopt(
                socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(outbound_ip)
            )

            s.sendto(img_data, destination)
            print(f"Image {img_idx}: sent {len(img_data)} bytes to {destination}.")
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)
        s.setsockopt(
            socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(outbound_ip)
        )
        # Why do we need this? Is it a good way
        time.sleep(WAIT_INTERVAL)
        s.sendto("".encode(), destination)
        print("End of Transmission")


def main():
    args = parse_cmdline()
    destination = (args.mcast_ip, args.mcast_port)
    send_images(args.img_dir, destination, args.outbound_ip)
    print("Done.")


if __name__ == "__main__":
    main()

To run the program, we need to first identify a multicast IP address. The following is an example run:

First, we run the receiver:

python transport/udp/pysocket/multicast/show_gallery.py \
  --mcast_ip 224.1.1.5 --mcast_port 25000 --arrival_ip 192.168.56.104

Second, we run the sender:

python transport/udp/pysocket/multicast/send_gallery.py \
  --img_dir images/small --mcast_ip 224.1.1.5 --mcast_port 25000 --outbound_ip 192.168.56.103

TCP Experiments

With TCP, we can only do unicast.

The following version is implemented using the Socket API. The application consists of two parts, the receiver (show_gallery.py) and the sender (send_gallery.py). The following is the receiver:

"""Receive images sent by a sender using TCP.

   show_gallery.py
"""

import argparse
import io
import socket

from matplotlib import animation
from matplotlib import image as mpimg
from matplotlib import pyplot as plt

MAX_BUF_SIZE = 65536


def parse_cmdline():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--listen_ip",
        type=str,
        help="The IP address of the receiver.",
        default="localhost",
        dest="listen_ip",
    )
    parser.add_argument(
        "--listen_port",
        type=int,
        help="The port number of the receiver.",
        default=25000,
        dest="listen_port",
    )
    return parser.parse_args()


class Gallery:
    """A simple gallery to display images in animation."""

    def __init__(self, images, interval=1000):
        self.images = images
        self.interval = interval
        self.fig, self.ax = plt.subplots()

    def display_frame(self, i):
        self.ax.clear()
        self.ax.imshow(self.images[i])
        self.ax.set_title(f"Image {i} at host {socket.gethostname()}")

    def display_gallery(self):
        self.anim = animation.FuncAnimation(
            self.fig,
            self.display_frame,
            frames=len(self.images),
            interval=self.interval,
        )
        plt.show()
        self.anim = None


def receive_full_image(conn):
    image_data = b""
    while True:
        image_chunk = conn.recv(MAX_BUF_SIZE)
        if not image_chunk:
            break
        image_data += image_chunk

    return image_data

def receive_and_show_images(end_point):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind(end_point)
        sock.listen(1)
        print(f"Listening on {end_point}")
        images = []
        idx = 0
        while True:
            conn, _ = sock.accept()
            with conn:
                # Why not just this?
                # img_data = conn.recv(MAX_BUF_SIZE)
                img_data = receive_full_image(conn)
                if not img_data:
                    print("End of Transmission")
                    break
                print(f"Image {idx}: received {len(img_data)} bytes.")
                img = mpimg.imread(io.BytesIO(img_data), "jpg")
                images.append(img)
                idx += 1
    gallery = Gallery(images)
    gallery.display_gallery()


def main():
    args = parse_cmdline()
    end_point = (args.listen_ip, args.listen_port)
    receive_and_show_images(end_point)
    print("Done.")


if __name__ == "__main__":
    main()

The below is the sender:

"""Send images in a directory to a receiver using TCP.

   send_gallery.py
"""

import argparse
import glob
import pathlib
import socket


def parse_cmdline():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--img_dir",
        type=str,
        help="The directory containing the images to send.",
        default="images/small",
        dest="img_dir",
    )
    parser.add_argument(
        "--to_ip",
        type=str,
        help="The IP address of the receiver.",
        default="localhost",
        dest="to_ip",
    )
    parser.add_argument(
        "--to_port",
        type=int,
        help="The port number of the receiver.",
        default=25000,
        dest="to_port",
    )
    return parser.parse_args()


def send_images(img_dir, destination):
    """Send images to a receiver using TCP.

    Args:
        img_dir (str): The directory containing the images to send.
        destination (str): the end point of the receiver, a tuple consiting of
                           the IP address and port number.
    """
    for img_idx, img_path in enumerate(
        glob.glob(str(pathlib.Path(img_dir).joinpath("*.jpg")))
    ):
        with open(img_path, "rb") as img_file:
            img_data = img_file.read()

        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect(destination)
            print(f"Connected to {destination}")
            s.sendall(img_data)
            print(f"Image {img_idx}: sent {len(img_data)} bytes.")
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(destination)
        s.sendall("".encode())
        print("End of Transmission")


def main():
    args = parse_cmdline()
    destination = (args.to_ip, args.to_port)
    send_images(args.img_dir, destination)
    print("Done.")


if __name__ == "__main__":
    main()

We demonstrate a run of the application as follows:

Run the receiver:

python transport/tcp/pysocket/show_gallery.py \
  --listen_ip 192.168.56.104 --listen_port 25000

And then, run the sender:

python transport/tcp/pysocket/send_gallery.py \
  --img_dir images/small --to_ip 192.168.56.104 --to_port 25000

Exercise. Exercises and Explorations

Answer the following questions and the experiments:

  1. Calculate, compare, and contrast the bytes and the packets transmitted when send the small gallery to three hosts:
    1. When using the given UDP unicast application, how many image bytes, how many bytes including headers, and how many packets are transmitted?
    2. When using the given UDP broadcast application, how many image bytes, how many bytes including headers, and how many packets are transmitted?
    3. When using the given UDP multicast application, how many image bytes, how many bytes including headers, and how many packets are transmitted?
    4. When using the TCP application, how many image bytes, how many bytes including headers, and how many packets are transmitted? When answering these questions, compare and contrast on the data link layer, the network layer, and the transport layer.
  2. Design a packet capture experiment to verify your answers to the above question.
  3. In the examples we demonstrate, we only send the small images. What if we want to send the large images, i.e.,
    1. Do we need to modify the programs? If so, sketch the modified programs (i.e., give the pseudocode or algorithm for the sending and receiving logic)
    2. What are the challenges to implement the modification you propose?
  4. Given the above, discuss in what scenario (i.e., for what kind of applications) you want to use UDP, and in what scenario you want to use TCP to design your network applications?