Network Programming代写:CS168 Chat

Introduction

代写Python版的聊天应用,在Network Programming里面属于罕见的类型,通常这类作业都是C语言的。
不过Python版直接用socket包就好,也不需要像C语言一样处理大量的I/O异常,也算是比较省事的。

Chat

In this assignment, you’ll build a simple application that connects users over a network: a chat server. Similar to chat programs like Slack and IRC, your finished chat server will allow users to converse in different channels. Users can create and join channels; once a user is in a particular channel, all messages that she sends will be relayed to all other users in that channel.
This assignment (and the rest of the assignments in this class) should be implemented in Python 2. This assignment will introduce you to the socket programming API.

Resources

There’s a demo of the finished project available here.
If you have questions, first take a look at the FAQ section. If your question isn’t answered there, post to Piazza.
The Python Socket Programming HOWTO is a useful introduction to socket programming.
We’ve provided two files to help you test your code:
client_split_messages.py is intended to help you ensure your server is correctly buffering messages, and is described in more detail below.
simple_test.py tests a basic scenario where two clients communicate in a simple channel. simple_test.py represents only a small fraction of the points that we’ll test for when we grade your assignment, and is intended only to help you verify the basic format of your client’s output.

What are sockets?

A socket is an endpoint of a connection between two programs running across a network. Each socket is associated with a particular port number. Sockets are an abstraction provided by operating systems: programs create sockets, read from those sockets, and write to those sockets. When a program writes to a socket, the operating system sends data out a particular port; similarly, with the operating system receives data on a port, that data can be read from the socket corresponding to that port.
In Python, you can create a socket and connect to a remote endpoint by using the socket library as follows:

1
2
3
4
5
import socket
# The socket constructor accepts a few arguments; the defaults are fine for this class.
client_socket = socket.socket()
client_socket.connect(("1.2.3.4", 5678))
client_socket.sendall("Hello World")

The example above created a socket and connected it to port 5678 at IP address 1.2.3.4. Then, it sent a “Hello World” message to the server at 1.2.3.4:5678.
The example above created a client socket that was connected to exactly one remote endpoint. When you create a server, you’ll typically want to allow multiple remote clients to connect, and you don’t usually know the address of those clients when the socket is created. As a result, server sockets work differently:

1
2
3
server_socket = socket.socket()
server_socket.bind(("1.2.3.4", 5678))
server_socket.listen(5)

After creating the socket, rather than connecting to a particular remote destination, the code above bound the socket to a particular IP address and port, which essentially tells the operating system to associate the given IP address and port with the socket. Finally, the listen call listens for connections made to the socket. When a new client connects to the socket, the socket library will create a new socket to use to communicate with that client, so that the server socket can continue to be used to wait for inbound connections from other clients:

1
(new_socket, address) = server_socket.accept()

This call blocks until a client connects (using a connect() call, as in the example above), and then returns a newly created socket, new_socket, that can be used to send and receive data to and from the client. For example, the call

1
message = new_socket.recv(1024)

will block until there is data to receive from the client, and will return up to 1024 bytes of data.
You’ll need to do some reading to understand how all of these API calls work. In particular, be careful when using send and recv! send, for example, will not necessarily send all of the data passed into it. The Python Socket Programming HOWTO will likely be a useful resource.

Part 0 (to be completed in section)

The first part of the assignment will help you get started with the socket programming API and introduce you to basic client-server interaction. This part of the assignment will not be graded, and you’ll complete it.
For this part of the assignment, you’ll write a simple client and server. The client will send a single message from stdin to the server and then disconnect. The server should print messages it receives to stdout. If multiple clients connect to the server, the server should handle them sequentially (i.e., it should print the complete message from one client and close that connection before handling the next client).
The server should accept one command line argument, stating the port that the server should use:

$ python basic_server.py 12345

The client should accept two command line arguments: the hostname (or IP address) of the server to connect to, and the server port:

$ python basic_client.py localhost 12345

Your server should be reachable from any IP address associated with the machine (see the FAQ).

Blocking sockets

In this part of the assignment, it’s fine to use blocking sockets. “Blocking” means that a socket call may not return for a while, until the call completes. Cases when socket calls won’t complete immediately include:

  • send: if the socket’s internal buffer is full so no data can be written
  • recv: if the internal buffer is empty, so there’s no data to read (e.g., if the client has paused sending)
  • accept: if these are no clients currently trying to connect

Example use

Here’s an example of how your client and server should work. Suppose two different clients connected sequentially:

$ python basic_client.py localhost 12345
I am a student in CS168. This class is awesome!
$ python basic_client.py localhost 12345
Why is Shenker so bad at drawing?

If a server had been started on port 12345 before the client was run, it should have printed output as follows:

$ python basic_server.py 12345
I am a student in CS168. This class is awesome!
Why is Shenker so bad at drawing?

Part 1

In the remainder of the assignment, you’ll build on your basic client and server to create a chat server with different channels that clients can communicate on. For a demo of how your server should behave, watch the video here.

Server Functionality

The server should accept a single command line argument that’s the port that the server should run on.
Unlike your server in part 0, your server in this part of the assignment must allow many clients to be connected and sending messages concurrently. Each client should have an associated name (so that other connected clients know who each message is from) and channel that they’re currently subscribed to. When a client first connects, it won’t have an associated name and channel. The first message that the server receives from the client should be used as the client’s name.
Future messages from the client to the server can take one of two forms. The first type of message is a control message; control messages always begin with “/“. There are three different control messages your server should handle from clients:

  • /join [channel] should add the client to the given channel. Clients can only be in one channel at a time, so if the client is already in a channel, this command should remove the client from that channel. When a client is added to a channel, a message should be broadcasted to all existing members of the channel stating that the client has joined. Similarly, if the client left a channel, a message should be broadcasted stating that the client left.
  • /create [channel] should create a new channel with the given name, and add the client to the given channel. As with the /join call, the client should be removed from any existing channels.
  • /list should send a message back to the client with the names of all current channels, separated by newlines.

The second type of message is normal messages to the client’s current channel. All messages that are not preceeded by a / are considered normal messages. These messages should be broadcasted to all other clients in the channel, preceeded by the client’s name in brackets (see the example below). Messages should not be sent to client in different channels. If the client is not currently in a channel, the server should send an error message back to the client.

When a client disconnects, a message should be broadcasted to all members of the client’s channel saying that the client disconnected.
Sockets provide a data stream functionality, but they don’t delineate different messages. When a given recv call returns some data, the socket won’t tell you whether the data returned is a single message, or multiple messages, or part of one message. As a result, you’ll need a way to determine when a message ends. For this assignment, use fixed length messages that have 200 characters for all messages (including messages from the server to the client). If a message is shorter than 200 characters, you should pad the message with spaces (and the receiver should strip any spaces off of the end of the message). You can assume that no messages are longer than 200 characters.
Be sure that your code correctly handles the case where less than one message is available in the socket’s buffer (so a recv call returns fewer than 200 characters of data) and the case where more than one message is available in the socket’s buffer. You should handle partial messages by buffering: if a recv call returns only part of a message, your code should hold on to the part of the message until the remainder of the mesage is received, and then handle the complete message. For example, if a client receives 150 characters from the server, it should hold those 150 characters until 50 more characters are received. The client should only write the message to stdout once all 200 characters have been received. To help you check for your server’s handling of these cases, we’ve provided a special client (client_split_messages.py) that splits messages into many smaller messages before sending them to the server. This client only tests some of the scenarios your server should handle! You’ll likely want to modify this client to test for additional casess.

Error Handling

Your server should handle cases where the client sends an invalid message by returning an appropriate error message to the client. For example, if a client uses the /join command but doesn’t provide the name of a channel to join, the server should send back an error message. The provided utils.py includes format strings for all of the errors you should handle. You can use these format strings using Python’s string formatting operations. For example, utils.py defines the following error message:

1
CLIENT_SERVER_DISCONNECTED = "Server at {0}:{1} has disconnected"

You can use the .format function to replace the brackets with strings as follows:

1
error_message = CLIENT_SERVER_DISCONNECTED.format("localhost", 12345)

You are required to use the error messages defined in utils.py. If you do not use these error messages (with appropriate formatting), you will not get credit for error handling.
When commands lead to an error, the command should not cause any changes at the server. For example, if a client is currently in the cs168_tas channel and then tries to join a channel that doesn’t exist, the client should not be removed from the cs168_tas channel.
We will only test for errors that have associated error messages in utils.py in our testing. You’re welcome to check for additional errors if you’d like, but you will not be graded on them.

Client Functionality

Each client connects to a particular chat server, and is associated with a name. Your client should be started as follows:

$ python client.py Scott 127.0.0.1 55555

This command should connect to the server at the given address and port number, and then send a message with the name Scott.
After being started, the client should listen for messages from the server and from stdin. Messages from the server should be printed to the command line (after being stripped of any spaces at the end) and messages from stdin should be sent to the server (after being padded with spaces, as needed). The client should print [Me] to each new line, to make it clear which messages in the history were typed by the client. When the client gets a message from the server, it should write over the [Me] with the message from the server. For an example of this, take a look at the demo video linked above.
Here’s an example of a client’s interaction with a server that was started locally on port 55555:

python chatv3_client.py Panda localhost 55555
[Me] Hello world!
Not currently in any channel. Must join a channel before sending messages.
[Me] /list
[Me] /create 168_tas
[Me] /list
168_tas
[Me] Hello world!
Alice has joined
[Alice] Hi everyone! Does anyone know what we're doing on the first day of lecture?

After seeing Alice’s message, Panda stopped his client. After Panda created the 168_tas channel, a second client started:

python chatv3_client.py Alice 127.0.0.1 55555
[Me] /list
168_tas
[Me] /join 168_tas
[Me] Hi everyone! Does anyone know what we're doing on the first day of lecture?
Panda has left

Details

We’ve provided a utils.py file that has error messages that you should use. These are intended to make your life easier, and also to enable testing. Be sure you use these messages; otherwise, your code will fail the tests!

Non-blocking sockets

You’ll need to use non-blocking sockets for this part of the assignment, because both your client and server need to receive data from multiple sources, in an unknown order. Consider what would happen if your client used blocking sockets, as in part 0, with a call like:

1
message_from_server = client_socket.recv(200)

Now suppose that the server doesn’t send any messages for a while, but while the client is blocked waiting on the recv call to return, the user types some data into stdin. The client should read the data from stdin and send it to the server – but the client is stuck blocked waiting on data from the server socket! To address this problem, you can use non-blocking sockets.
To use non-blocking sockets, you’ll need to use the select call in the select library. For more about how to use select and a very relevant example, take a look at this page. While you are required to use non-blocking sockets for reading data and accepting connections, it’s fine to use blocking sockets for sending messages (since the messages you’re sending are short and you don’t need to handle sending a large number of messages in quick succession, send and sendall should not block for long periods of time).