Socket programming is the standard way to write programs that communicate over a network. While originally developed for Unix computers programmed in C, the socket abstraction is general and not tied to any specific operating system or programming language. This allows programmers to use the socket mental model to write correct network programs in many contexts.
This part of the assignment will give you experience with basic socket programming. You will write two programs for transmitting text messages over the Internet: 1) a client for sending, and 2) a server for receiving. Both the client and server must be written in Go.
The client and server programs should meet the following specifications. Be sure to read these meticulously before and after programming to make sure your implementation fulfills them:
- The server program should listen on a socket, wait for a client to connect, receive a message from the client, print the message to stdout, and then wait for the next client indefinitely.
- The server should take one command-line argument: the port number to listen on for client connections.
- The server should accept and process client communications in an infinite loop,
allowing multiple clients to send messages to the same server. The server should
only exit in response to an external signal (e.g. SIGINT from pressing
ctrl-c). - The server should gracefully handle error values potentially returned by socket programming library functions (see specifics below). Errors related to handling client connections should not cause the server to exit after handling the error; all others should.
- The server should maintain a client queue and handle multiple client connection
attempts sequentially. The good news is, this is the default behavior if you are
using
net.Listen(). You do not need to do anything extra to satisfy this requirement. In real applications, a TCP server would fork a new process to handle each client connection concurrently, but that is not necessary for this assignment.
- The client program should contact a server, read a message from stdin, send the message, and exit.
- The client should read and send the message exactly as it appears in stdin until reaching an EOF (end-of-file).
- The client should take two command-line arguments: the IP address of the server and the port number of the server.
- The client must be able to handle arbitrarily large messages by iteratively reading and sending chunks of the message, rather than reading the whole message into memory first.
- The client should handle partial sends (when a socket only transmits part of the data given in the last send operation) by attempting to re-send the rest of the data until it has all been sent.
- The client should gracefully handle error values potentially returned by socket programming library functions.
Generally speaking, there are several reasonable actions that a program might take upon realizing that it has encountered an error, and you may sometimes need to take one or more of these actions.
- Attempt to recover: Some errors may arise due to chance events like a busy or noisy network, and in these cases it is possible (and desirable) to try to recover gracefully, perhaps by trying the exact same operation again, or by tweaking some values first and then retrying.
- Crash: On the other hand, some errors cannot be recovered from at runtime. If the user requests access to some resource that is already being used by another process, there is no straightforward way to recover, and crashing (with an informative message) would be an acceptable response.
- Print error message: Especially for fatal errors that cause your program to crash, it is good style to print out a message indicating what has gone wrong. For non-fatal errors, you may find it useful to print messages for debugging purposes. For your final submit, you should make an effort to minimize output by commenting out any debugging statements.
- Do nothing: It is generally poor style to leave potential errors unhandled, as your program might continue executing, believing everything to be OK, only to crash later on in a way that will be much harder to debug. Your program should make an effort to handle all reasonable errors that may arise.
Go has several error handling functions that may be of use to you:
log.Fatal(message string)(docs): Print message toos.Stderrand terminate the program with a return code of 1.log.Print(message string)(docs): Prints message to stderr.log.Panic(message string)(docs): Prints the error message, and then callspanic(), which propagates the error, and prints a stack trace if unhandled.panic()is similar to Java'sthrow. It differs fromlog.Fatal()in that deferred functions are executed before the program exits (perhaps freeing resources or flushing buffers). An interesting note is that callers can recover from a panic usingrecover()(analagous tocatchin Java), but you will not need to make use of therecover()functionality for this assignment. See this blog post for more ondefer(),panic(), andrecover().
Assuming you have completed assignment 0 and have your local environment set up, you can work on this assignment directly on your physical machine and use any editor you have installed (e.g. Vim).
Many modern editors (Emacs, Atom, VS Code, Sublime, ...) provide convenient extensions specifically designed for working with Golang. These extensions provide many useful features, including:
- Automatically adding required import statements
- Automatically vetting your code for compilation errors
- Automatically formatting your code in idiomatic Go style
- Automatically running any unit tests you've written
It is strongly recommended that you install an extension appropriate for your editor of choice to streamline your Go programming experience.
Before jumping into writing code, you will need to clone your code repository
from GitHub to your local machine.
The basic command to do this is git clone https://github.com/cos316/<repository name>.
You may also clone your repository using the GitHub Desktop client.
We have provided scaffolding code in the assignment1 directory.
You should read and understand this code before starting to program.
You should only add code to locations in the provided files marked with TODO
comments. There is one TODO section for the client and one for the server.
You can add functions if you wish, but do not change file names, as they will be used for automated testing.
The following section provides details for implementing the client and server programs in Go.
The documentation for Go socket programming is located here.
The overview at the top and the section on the Conn type will be most relevant. You may also find the buffered Reader and Writer types to be useful, but you aren't required to use them, and you can construct a working solution without them.
The Go language (Golang) documentation can be cryptic, so be sure to familiarize yourself with the language a bit first, especially if you are new to Go. You may find the Tour of Go documentation useful if you have never used Go before.
The files client.go and server.go contain the scaffolding code. You will need
to add socket programming code in the locations marked TODO. The reference
solutions have roughly 40 (well commented and spaced) lines of code in the
TODO sections of each file. Your implementations may be shorter or longer.
You should build your solution by running make in the assignment1 directory.
Your code must build using the provided Makefile.
The server should be run as ./server [port] > [output file].
The client can be run in two ways and should handle both:
Firstly, with ./client [server IP] [server port], your client should wait for
input. You can then type lines of input text into the command line. ctrl-c
should exit the client. Alternatively, with
./client [server IP] [server port] < [message file], the client receives input
text from the [message file]. See "Testing" for more details.
You should test your implementation by attempting to send messages from your
client to your server.
The server can be run in the background (append a & to the command) or in a separate
SSH window.
You should use 127.0.0.1 (i.e. the "localhost", or "loopback", address) as the
server IP and a high server port number between 10000 and 60000.
You can kill a background process with two successive commands: fg to bring it to the foreground,
and then ctrl-c to kill it. Conversely, you can send a foreground process to the background by
hitting ctrl-z to suspend the process, and then typing the command bg to resume the
process in the background.
You should test your implementation by attempting to send several different messages between your client and server. For example:
- The short message "Go Tigers!\n"
- A very long, randomly generated alphanumeric message
- A long, randomly generated binary message
- Several short messages sent sequentially from separate clients to one server
- Several long, random alphaumeric messages sent concurrently from separate clients to one server
Here are some debugging tips. If you are still having trouble, ask a question on Ed or see an instructor during office hours.
- There are defined buffer size constants in the scaffolding code. Use them. If you are not using one of them, either you have hard-coded a value, which is bad style, or you are very likely doing something wrong.
- There are multiple ways to read and write from stdin/stdout in Go. Any method is acceptable as long as it does not read an unbounded amount into memory at once and does not modify the message.
- If you are using buffered I/O to write to stdout, make sure to call
flushor the end of a long message may not write. - Remember to close the socket at the end of the client program.
- When testing, make sure you are using
127.0.0.1as the server IP argument to the client and the same server port for both client and server programs. - If you get "address already in use" errors, make sure you don't already have a server running.
- If you are getting other connection errors, try a different port between 10000 and 60000.
Do I need to handle signals such as SIGINT to clean up the server process when the user presses
ctrl-c?No, it is not necessary in this assignment. The default response to signals is good enough. However, keep in mind it would be good practice to do so in general.
Should I use stream (TCP) or datagram (UDP) sockets?
Please use stream (TCP) sockets to ensure that the exact message is delivered. Streams ensure reliable, in-order packet transmission, whereas datagram packets are not guaranteed to be delivered.
Should I support IPv6?
IPv6 support is not necessary. Your code will be tested using IPv4 only.
Should the client wait to receive a reply from the server?
No, in this assignment it should exit immediately after sending all of the data.
Should the server handle client connections concurrently (in separate processes)?
No, as stated in the client specification, this is not required in this assignment. So no need to use
syscall.ForkExec()or any other system-levelfork()!
To submit your client and server, git commit the changes to your code and
git push them to your GitHub classroom repository.
We will grade your submissions by compiling your client and server, and then sending messages back and forth between each of your submitted programs and a reference server or client, as appropriate. Your code will be scored based on how many different kinds of messages are transmitted correctly, and how well your implementation adheres to various aspects of the specification. Within a couple minutes of submitting your assignment, the GitHub autograder will add a comment to your most recent commit on GitHub, indicating your test results.
You may submit in this way and receive feedback as many times as you like, whenever you like, but a lateness penalty will be applied to submissions received after the deadline.
Code that does not compile is graded harshly; if you want partial credit on code that doesn't compile, comment it out and make sure your file compiles!
Remember that you should submit two files: a server file and a client file, both written in Go!