Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit

COSC264 Assignment

Dept. of Computer Science and Software Engineering

July 24, 2023

1 Administrivia

This assignment is part of the COSC264 assessment process. It is worth 10% of the final marks.  It centers around socket programming using the Python programming language. Your program should be a command line program that can run from a Linux terminal.

This may seem like a daunting task, so a good place to start is the socket programming slide set.  You may need to investigate some aspects of the Python socket API yourself, so don’t forget to read the Python documentation.

Please use the discussion forum on the Learn platform for raising any unclear technical issues.  Please do not send emails with technical questions directly to the lecturers or the tutors; instead use the Learn forum.  This way other people can benefit from the question (and the answer).

Another piece of advice: socket programming is systems programming, and there can sometimes be subtle differ- ences in the precise operation of socket calls between different (versions of) operating systems, even in a cross- platform API like Python’s socket API. We strongly suggest that you test your programs under Linux before submitting, as this is the OS that we will use to mark your submission.

2 Plagiarism Warning

Your submissions are logged and originality detection software will be used to compare your solution with other solutions. Dishonest practice, which includes

❼ letting another person or a tool like ChatGPT create all or part of an item of work,

❼ copying all or part of an item of work from another person or a tool like ChatGPT with or without modifi- cation, and

❼ allowing someone else to copy all or part of an item of work,

may lead to partial or total loss of marks, no grade being awarded, the failing grade ’X’ (dishonesty) being awarded, or other serious consequences including notification of the University Proctor.

You are encouraged to discuss the general aspects of a problem with others.  However, anything you submit for credit must be entirely your own work and not copied, with or without modification, from any other person or tool like ChatGPT.

If you need help with specific details relating to your work, or are not sure what you are allowed to do, ask your tutor or lecturer for advice.

If you copy someone else’s work or use a tool like ChatGPT or share details of your work with anybody else, you are likely to be in breach of university regulations and the Computer Science and Software Engineering department’s policy. For further information please see

❼ Academic Integrity Guidance for Staff and Students

https://www.canterbury.ac.nz/about/governance/ucpolicy/general/academic-integrity-guidance-for-staff-and-students/

❼ Academic Misconduct Regulations

https://www.canterbury.ac.nz/regulations/general-regulations/academic-misconduct-regulations/

Important note: Any code that a student develops for the assignment must not be published or otherwise made available until after the final exam.  If a student does this nonetheless, we will apply the same penalties to them as we do to students copying code.

You will have to make a plagiarism declaration upon submission of your assignment.

3 Problem Description

Your task is to write two programs in Python.  The first one, called server, will allow the other program, called client, to send and read messages to/from other clients.  The client and the server will communicate through stream / TCP sockets, exchanging both control and actual message data.

3.1 Server

The server is a command line application and will operate as a TCP server.  It will accept one parameter, to be read from the command line (see Section 3.4 for how to use command line arguments).  This parameter is the port number to which the server will bind the socket on which it accepts incoming message requests.  The port number should be between 1,024 and 64,000 (inclusive).  If it is not, then the server should print an appropriate error message and exit. Otherwise, the server will perform the following steps:

❼ The server creates a socket and attempts to bind it to the port number given on the command line.  (In your bind() call, it is probably a good idea to use the "0.0.0.0 " special address as the IP address to bind the socket to.) If this does not work, then the server should print an appropriate error message and exit.

❼ The  server  then  calls  listen()  on the  socket.   If  this  does  not  work,  then the  server  should print  an appropriate error message, close the socket, and exit.

❼ The server enters an infinite loop, in which the server performs the following steps:

It first  accept()s a new incoming connection  (recall that  accept() returns a new socket for that connection). For logging purposes, the server prints a message which indicates the IP address and port number of the client from which the incoming connection originated.

It then attempts to read a MessageRequest record from the connection and checks the validity of that MessageRequest. This process is described in Section 3.3.

If the MessageRequest record is not valid, or if the required number of bytes for the complete request cannot be read from the socket within a maximum of one second after accept() has returned with a socket (see Section 3.5), then the server prints an appropriate error message, closes the socket obtained from accept(), and goes back to the start of the loop.

If the MessageRequest record is correct, the server processes it depending on the type of request:

✯ For create requests, the server will store the message text and sender name under the receiver’s name (note this storage does not have to be persistent; storing them in a Python object will suffice). The server will then close the socket obtained from accept(), print an informational message with the names of the sender and receiver, and go back to the start of the loop.  No response is sent back to the client in this case.

✯ For read requests, the server will send a MessageResponse record back to the client that contains all the messages that the server has for the requesting user (up to 255, on a first-in, first-out basis). The sent messages should be removed from the server’s storage.  If there are more than 255 messages for a user, then this is noted in the MoreMsgs field of the MessageResponse record (see Section 3.3). Once all the messages have been sent, the server should close the socket obtained from accept(), print an informational message regarding the number of messages sent, and go back to the start of the loop.

3.2 Client

The client is a command line application and will operate as a TCP client.  The client will accept four parameters, to be read from the command line:

❼ The first parameter is either a string giving an IP address in dotted-decimal notation (e.g. “130.66.22.212”), or it is the hostname of the computer running the server (e.g.  “fileserver.mydomain.nz”).  Either way, the client will attempt to convert this parameter to an IP address using the getaddrinfo() function.  If this conversion fails (e.g. because the hostname does not exist or an IP address given in dotted-decimal notation is not well-formed), then the client should print an appropriate error message and exit.

❼ The second parameter is the port number to use on the server.  The port number should be between  1,024 and 64,000 (inclusive). If it is not, then the client should print an appropriate error message and exit.

❼ The third parameter is the name of the client’s user, i.e.  the user whose messages are to be retrieved, or the name of the sender of the message that is to be created.  (You may assume that user names are unique and do not need to check or enforce this.) User names must be at least 1 character long, and no longer than 255 bytes (see 3.6 for information about Python strings and bytes).  If it is not, then the client should print an appropriate error message and exit.

❼ The fourth parameter is the type of request the client is making to the server.  This should be either  "read" or "create". If it is not, then the client should print an appropriate error message and exit.

If there are fewer or more than four parameters on the command line, the client prints an appropriate error message and exits. If the parameters are all okay, the client will go through the following steps:

❼ The client prepares a MessageRequest.  Section 3.3 describes the required format.  For a create request, the client must elicit additional information from the user; see the create request additional steps below.

❼ The client creates a socket and connect()s with the server, using the IP address and port number inferred from the command line.

❼ The client sends the MessageRequest to the server over the socket and prints an appropriate informational message.

❼ For a read request, the client then attempts to read and convey the server’s response - see the read request additional steps below.

❼ Finally, the client closes the socket and exits.

If any of these steps do not succeed, the client should print an appropriate error message, close the socket (if open), and exit without attempting the remaining steps.

3.2.1 create request additional steps

For a create request, the client requires additional information in order to prepare the MessageRequest. It should prompt the user for the following information interactively:

❼ The name of the receiver:  This must be at least  1 character and less than 255 bytes.  If it is not, then the client prints an appropriate error message and prompts again until a valid receiver name is provided.

❼ The message contents:  The  message  must be at least 1 character and less than 65,535 bytes.  If it is not, then the client prints an appropriate error message and prompts again until a valid message is provided.

3.2.2 read request additional steps

For a read request, after sending the MessageRequest, the client attempts to read a MessageResponse record from the server. See Section 3.3 for a full description of this process.

If the MessageResponse record is valid:

❼ If the MessageResponse record indicates that there are no messages, then the client prints an appropriate informational message, closes the socket, and exits.

❼ Otherwise, the client attempts to read and print each message’s sender and text.  If the MessageResponse record’s MoreMsgs field indicates that there are more messages to be read from the server, the client will print an appropriate message. Either way, the client will close the socket and exit.

If the MessageResponse record is not valid, or if there is a gap of more than one second while reading any part of the MessageResponse record (see Section 3.5), then the client should print an appropriate error message, close the socket, and exit.

3.3 Record Formats

All records should be created using bytearrays.

3.3.1  The MessageRequest Record

The format of the MessageRequest record is shown in the following figure:

It consists of the following fields:

❼ The first  16-bit field ‘MagicNo’ is taken up by a magic number, which needs to have the value 0xAE73 in network byte order.   This  is  a simple safeguard to check whether the received data could actually be a MessageRequest record.

❼ The next 8-bit field ‘ID’ must contain either ‘1’ for a read request or ‘2’ for a create request

❼ The next two 8-bit fields ‘NameLen’ and ‘ReceiverLen’ contain the length of the user’s name and receiver’s name (relevant only for create requests) in bytes.

❼ The next 16-bit field ‘MessageLen’ contains the length of the message in bytes.  As this is a 2-byte field, it is given network byte order.

❼ Finally, the record contains NameLen + ReceiverLen + MessageLen bytes storing first the user’s name, then (if applicable) the receiver’s name and the message itself, all in bytes, and in that order.

We refer to the first seven bytes of the record (comprising the ‘MagicNo’, ‘ID’, ‘NameLen’, ‘ReceiverLen’, and ‘MessageLen’ fields) as the fixed header.

The MessageRequest record is sent from the client to the server.  To receive and process a MessageRequest record, the server performs the following steps:

❼ First the server attempts to read the seven bytes of the fixed header.  If that is not possible without gap (see 3.5), then the server concludes that the received MessageRequest is erroneous and performs error processing as described in Section 3.1 (i.e. printing an appropriate error message on the terminal, closing the socket obtained from  accept(),  and going back to the start of the loop).   Otherwise,  the  server performs the following checks:

The contents of the ‘MagicNo’ field must equal 0xAE73.

The contents of the ‘ID’ field must be either 1 or 2.

The contents of the ‘NameLen’ field must be at least 1.

The contents of the ‘ReceiverLen’ field must be 0 if ‘ID’ is 1 (read request), or at least 1 if ‘ID’ is 2 (create request).

Similarly, the ‘MessageLen’ field must be 0 if ‘ID’ is 1, or at least 1 if ‘Type’ is 2.

If any of these conditions  are not met,  then the  server  concludes that the received  MessageRequest  is erroneous and performs error processing.

❼ Then the server attempts to read exactly NameLen + ReceiverLen + MessageLen further bytes from the MessageRequest record.  If reading from the socket is not possible without gap or the server reads fewer or more than the expected number of bytes, then the server concludes that the received MessageRequest is erroneous and performs error processing.

3.3.2  The MessageResponse Record

The format of the MessageResponse record is shown in the following figure:

It consists of the following fields:

❼ The first  16-bit field ‘MagicNo’ is taken up by a magic number, which needs to have the value 0xAE73 in network byte order.   This  is  a simple safeguard to check whether the received data could actually be a MessageResponse record.

❼ The next 8-bit field ‘ID’ must contain the fixed value ‘3’ for a MessageResponse record.

❼ The next 8-bit field ‘NumItems’ contains the number of messages that will follow (if any).

❼ The next 8-bit field ‘MoreMsgs’ contains either ‘0’ if there are no more messages for the user on the server, or ‘1’ if there are more messages (i.e. if the server could not send all the messages it had stored for the user because there were more than 255).

❼ Next, if there are any messages, each will contain the following three fields:

First, an 8-bit field ‘SenderLen’ contains the length of the sender’s name in bytes.

Second, a 16-bit field ‘MessageLen’ contains the length of the message in bytes in network byte order.

Finally, the next SenderLen + MessageLen bytes contain the sender’s name, followed by the message text, all in bytes.

❼ The record will repeat the previous three fields for each message contained in the record.

We refer to the first five bytes of the MessageResponse record (comprising the ‘MagicNo’, ‘ID’, and ‘NumItems’) as the fixed header and the first three bytes of each message (comprising the ‘SenderLen’ and ‘MessageLen’) as the message  header.   The MessageResponse record is sent from the server to the client in response to  a MessageRequest record.   To receive and process  a MessageResponse record, the client performs the following steps:

❼ First the client attempts to read the five bytes of the fixed header.  If that is not possible without gap  (see Section 3.5), then the client concludes that the received MessageResponse is erroneous and performs error processing as described in Section 3.2 (i.e. printing an error message on the terminal, closing the socket, and exiting). Otherwise, the client performs the following checks:

The contents of the ‘MagicNo’ field must equal 0xAE73.

The contents of the ‘ID’ field must equal 3.

The contents of the ‘MoreMsgs’ field must be either ‘0’ or ‘1’

If any of these conditions is not met, then the client concludes that the received MessageResponse is erroneous and performs error processing.

❼ Then, for as many messages as denoted by the ‘NumItems’ field, the client performs the following steps:

The client first attempts to read the three bytes of the message header, again without gap.

The ‘SenderLen’ and ‘MessageLen’ fields must be at least 1.  If either is not, the client concludes that the received MessageResponse is erroneous and performs error processing.

Then the client will attempt to read and process the next  SenderLen + MessageLen bytes as the sender’s name and the message text as described in Section 3.2.

❼ If,  at any point, reading from the socket is not possible without gap, then the client concludes that the received MessageResponse is erroneous and performs error processing.

3.4 Running your Code

For this assignment, your server and client programs must both run at the same time. To do this:

❼ Open two terminals on your machine.

❼ Start  the  server  on one of the terminals,  giving the port number to which the server should bind  as a parameter on the command line.

❼ Then start the client on the other terminal.

For the server hostname / IP address parameter, you can use the IP address 127.0.0.1 or hostname localhost, which refers to ‘this host’ without needing to specify the actual IP address of the host.

For the port parameter, use the port number with which you started the server.

❼ You should run the client many times (for different users and request types) to thoroughly test your work.

3.4.1  Using Command  Line Arguments in  Python

Your server and client programs must both take arguments from the command line.

For example, you might write something like the following when running the client on the Linux terminal:

$  python3  client.py  127.0.0.1  5000  "alice  stevens"  read

This will run the program  client.py and pass four command line arguments to it: 127.0.0.1,  5000,  alice stevens, and read.

To access the arguments in Python, you can use the global variable sys.argv, which will contain a Python list of all the arguments. It is important to note that the first item in this list is always the name of the Python file and that Python treats all of these as strings (for quoted arguments, the quotes will not be present in the resulting string). Using the example above:

import   sys

...

filename  =   sys . argv[0]   #   " client . py "

address   =   sys . argv[1]  #   " 127.0.0.1 "

port   =   sys . argv[2]   #   "5000"

name   =   sys . argv[3]   #   " alice   stevens "

type   =   sys . argv[4]   #   " read "

...

3.5 Reading from a Stream/TCP Socket

For a stream (TCP) socket, when a sender sends 50 bytes by writing them in one go into its socket buffer, the underlying TCP protocol implementation has full discretion over when the data is sent, how many  “packets” it sends, and the size of these packets.  Theoretically, the TCP implementation on the sender could choose to send the first three bytes immediately, then wait three seconds, send the next thirteen bytes, then wait another five seconds, and then send the remaining 34 bytes. In reality, such large time gaps are unlikely and probably a result of problems in the underlying network.

In order to detect network issues, you must detect gaps of one second or more and abort operation should this occur.  To achieve this, a socket timeout can be set on each socket when reading or writing data.  This can be done using the socket method settimeout() on the socket. This should be done immediately after creating the socket (i.e. after a socket() or accept() call).  Then, when actually reading from or writing to a socket, e.g.

using recv(), you can check if this operation throws an exception.  If it does, then you will need to handle any exceptions  (a timeout on the socket will throw a specific exception) by printing an appropriate error message, returning resources (closing sockets, files, etc.  as applicable), and exiting (client) or returning to the start of the loop (server).

3.6 Data Representation

There are a few things to consider regarding data representation:

❼ The names and messages that the client reads from the command line or from user input will be treated by Python as strings.  Strings in Python are represented using the UTF-8 character encoding system.  In this system, a printed character may require a variable number of bytes to store in memory. Hence, a string which looks like it has m characters may in fact need a number n ≥ m of bytes to represent it, and your client program needs to transmit all n bytes. To achieve this, you can use the encode method in Python to convert a string to a bytes object. For example:

mystring   =   " hello   world "

mybytes  =  mystring. encode( " utf -8 " )

and then you can use the len function to find out the length n in bytes of mybytes. You should transmit the complete bytes object in the required field of the record, and the value of n should be filled into the length field (’NameLen’, ’MessageLen’, etc.)

❼ You can use the decode method in Python to convert bytes back into a string. For example:

mystring  =  mybytes. decode( " utf -8 " )

print(mystring)  #   " hello  world "

❼ All 16–bit fields in the fixed headers of the MessageRequest and MessageResponse records are encoded in network byte order.  For a  16-bit field, this means that the first or leftmost eight bits are occupied by the highest-valued bits and the last or rightmost eight bits are occupied by the lowest-valued bits.

4 Deliverables

Each student must submit their Python source code using the assignment submission activity on the Learn page of COSC264.

❼ You must submit two Python ( .py) files named server.py and client.py file.  If you wish, you may also submit one additional support file.

❼ Each Python file must contain the student’s name and student ID in the docstring at the top of the file.

❼ In order for an assignment to be marked, the student must press the  Submit button and then make the plagiarism declaration to confirm that the assignment is entirely their own work.

❼ The assignment must be submitted no later than the due date specified on Learn.  Students requiring an extension for special consideration reasons (e.g. illness) must contact the course coordinator.

5 Marking

Marking will be based on the source code. We will consider:

❼ Its ability to produce the right results (e.g. is packet processing correctly implemented, are all the right socket calls there, is the gap detection implemented correctly?)

❼ The  amount  of error  /  consistency checking.   Appropriate  error  messages  are  important to  let the user know exactly what happened and what failed; just letting a library function crash (possibly printing some nondescript message) will result in deductions.  You must also check incoming data / records for correctness.

❼ Whether you have returned all resources (sockets, files) to the operating system, wherever sensible.

❼ Style.  We may apply deductions if your code is particularly ugly or messy.