Programming with Transport Protocools
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:
wget www.sci.brooklyn.cuny.edu/~chen/downloads/teach/cisc3340/transport/images.zip
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.
Unicast Gallery Application
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
Broadcast Gallery Application
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
Multicast Gallery Application
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.
Unicast Gallery Application
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:
- Calculate, compare, and contrast the bytes and the packets transmitted when send the small gallery
to three hosts:
- When using the given UDP unicast application, how many image bytes, how many bytes including headers, and how many packets are transmitted?
- When using the given UDP broadcast application, how many image bytes, how many bytes including headers, and how many packets are transmitted?
- When using the given UDP multicast application, how many image bytes, how many bytes including headers, and how many packets are transmitted?
- 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.
- Design a packet capture experiment to verify your answers to the above question.
- In the examples we demonstrate, we only send the small images. What if we want to send the large images, i.e.,
- 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)
- What are the challenges to implement the modification you propose?
- 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?