This is a simple TCP-chat in Rust (a university home assignment). It illustrates the use of both the blocking and the non-blocking approaches to communication implementation.
The chat works as a room that random clients may connect to, and implements:
- Sending messages (with the sender's name & timestamp)
- Notifying clients about events (new client connects, someone disconnects, etc.)
- Uploading files to the server
- Downloading files from the server
cargo build
cargo run -p server
cargo run -p client
The server can be stopped via Ctrl-C
.
The client app supports the following commands:
Stop the client app.
Establishes the connection with the server.
The default address
is localhost
.
The default port
is 6969.
Asks the server to change the name of the current user.
Upload a file to the server.
Ask the server to save it as name
, and locally take it from the local_path
.
The default local_path
equals name
.
Download a file name
from the server.
Save it locally to the local_path
.
The default local_path
equals name
.
Sends a text message to the server.
The protocol assumes communication via messages: short pieces of data with predefined formats.
A single message cannot exceed some fixed number of bytes in size (currently MAXIMUM_MESSAGE_SIZE = 1024
).
If a receiver can't parse a message within this amount of bytes, the connection must be dropped.
Since there is an upper limit for the message size, there're also the upper limits for such things as a user name, a textual message, etc. (currently, MAXIMUM_NAME_SIZE = MAXIMUM_TEXT_SIZE = 486
, MAXIMUM_FILE_NAME_SIZE = 479
).
If the server receives a message containing a field with the size exceeding the corresponding upper limit, it must disconnect the client who sent it.
There're multiple message formats in use. Each one corresponds to some high-level situation (since we design the protocol for a single use case - a chat - it's aware of the context). In general, they are side-specific.
Some messages are self-sufficient, others represent a single step in a more complication communication procedure.
The full list of currently used message formats can be found in messages.rs
.
Represents a piece of contiguous binary data
.
The id
field determines the context (some larger thing this data relates to).
Chunk
messages are used for sending files to and from the server.
A text message a client sends to the server.
It's the server's responsibility to determine the client's name and time details.
Server then broadcasts its own Text
message with all the details.
Notifies the server about the client's intent to leave. The server closes its side of the connection upon receiving a message.
Asks the server to set a new name for the current client.
If the new name has been accepted, the server broadcasts a UserRenamed
message.
Otherwise, a Support
message is sent back with the explanation of what went wrong.
Asks the server if it can accept a file named name
of the specified size
.
If the server can accept it, the id
is used to refer to this file transfer procedure (as opposed to transferring other files if they are sent simultaneously).
In this case, the server sends back an AgreeFileUpload
.
Otherwise, a DeclineFileUpload
is returned.
Asks the server if it can send a file named name
.
If so, the server returns AgreeFileDownload
with the corresponding size
and id
.
Otherwise, DeclineFileDownload
is sent.
Notifies the server that the client is still wiling to accept the file after taking into account its size
.
After this message the server starts to actually send Chunk
s.
Notifies the server that the client is not willing to accept the file anymore (e.g. the size of the file is too big for the client machine or something).
The server cancels the file transfer after this message.
The message the server broadcasts when it wants to send a text message to everyone. The name
and the time
are determined by the server according to the contextual information.
A notification meaning there's a new client in the room.
When a user suddenly disconnects (without sending a Leave
message), the server assumes it's due to some error with the client, and broadcasts this notification.
This means, the client disconnects, but they might have not wanted to do it.
This notification means the client disconnects from the room normally.
A message containing some technical details. Typically sent on a per-client basis to explain something.
A notification meaning a user successfully changed their name.
A notification that means someone has uploaded a new file.
A message the server sends back to the client who have requested a file uploading procedure (see the RequestFileUpload
client message) in case if such a file can be accepted by the server.
After receiving this message, the client starts to actually send the Chunk
s.
A message meaning the server cannot accept the specified file.
Sent by the server after receiving the corresponding RequestFileUpload
client message.
After this message the client cancels the sending procedure.
A message meaning the server can give the client the file they have requested via the corresponding RequestFileDownload
message.
Upon receiving this message, the client has to decide whether the given file information (size
) is fine for them and either return an AgreeFileDownload
or a DeclineFileDownload
(the client-side versions).
A message the server returns if the requested file cannot be returned (e.g. not found or something).
The client cancels the file transfer procedure after this message.
The above message formats are translated into sequences of bytes via a BSON-serializer. These sequences of bytes are then written/read to/from the sockets.
Note. As I realized after having done the first part of the lab, I shouldn't have used a ready-made solution, so if you're building such a tcp-chat yourself, consult the teacher to clear things out.
Let's go once more over how a successful file transfer would look.
Client Uploads File:
Client -> RequestFileUpload -> Server
Client <- AgreeFileUpload <- Server
Client -> Chunk+ -> Server
Client Downloads File:
Client -> RequestFileDownload -> Server
Client <- AgreeFileDownload <- Server
Client -> AgreeFileDownload -> Server
Client <- Chunk+ <- Server
The end of the file transfer is handled by the receiver by the check that the number of already accepted bytes equals the initially sent size
.
Currently, the server works with the sockets in a blocking way, and the client works with them in a non-blocking manner. There's no deep reasoning to it, it's just a way to illustrate that both approaches are possible.
The problem of time wasted during iterations in the non-blocking approach is solved by checking whether there was some work to do during the previous iteration. This allows to save processor time for slow communication, but still utilize maximum performance when put under pressure. On the other hand, there's still a small initial delay after an iteration of doing nothing.
- Formal requirements: https://insysnw.github.io/practice/hw/tcp-chat/