Handy references:
- Server side:
- Client side:
Lab Audio, day 1
Lab Audio, day 2
For this lab, you'll be designing and implementing a protocol of your own
design to build a networked Jukebox. Since the protocol is your own, you'll be
developing both a client and a server. Thus far, you've seen and implemented
several types of protocols -- now you get to decide how to apply the patterns
you've seen to a new scenario.
Note: You do NOT want to split this up such that one person is
responsible for the client and the other the server. The server will be much
larger than the client (387 vs 113 lines in my solution*), and you'll
need to know the details of both when debugging.
* I didn't optimize my solution for size, but feel free to try making yours
shorter. For this lab, smaller solutions tend to correlate with those that
work better, typically as a result of cleaner / more thought out design.
Requirements
- You must use C to implement your server, and it must use select() (rather
than threading) to provide concurrency. You may use any language you wish for
the client, and you may use threads in the client. I strongly recommend Python
for the client, as it makes playing audio much simpler.
- After it starts up and begins serving clients, your server should
never block on any call other than select(). Blocking on a send(),
recv(), or accept() is a bug!
-
Your client should be interactive and it should know how to handle at least the following commands:
- list: Retrieve a list of songs that are available on the server, along with their ID numbers.
- info [song number]: Retrieve information from the server about the song with the specified ID number.
- play [song number]: Begin playing the song with the specified ID number. If another song is already playing, the client should switch immediately to the new one.
- stop: Stops playing the current song, if there is one playing.
- The client should not cache data. In other words, if the user tells the
client to get info for a song, get a song list, or play a song, the two should
exchange messages to facilitate this. Don't retrieve an item from the server
once and then repeat it back again on subsequent requests. To simplify the
file IO, it's fine for the server to keep its data, including the audio files,
in memory, but the client should not store data that it isn't actively
using.
- One of the parameters to your server should be a path to a directory that
contains audio files and their corresponding information. Within this
directory, you may assume that any file ending in ".mp3" is an mp3 audio file.
For each mp3 file, there may be a corresponding information file that is
identical to the file name with ".info" tacked on to the end. For example, if
there were a file in the directory named "song1.mp3", there may also be a file
named "song1.mp3.info" containing human-readable plain text information about
that song. This info file is what should be supplied when the client issues an
info command. I've made my music directory available at
/home/kwebb/music, and I've provided code to read the names of these files.
Feel free to use that or your own mp3 files for testing. Please do not
submit audio files to GitHub!
- I would like to have a brief (~10 minutes) protocol specification and
progress meeting with each group on or before October 27th. I'll post a sign
up sheet on my door. For this meeting, you should come prepared to discuss the
details of your protocol and the per-client state you intend to keep at the
server. I would strongly suggest that your server be capable of accepting
connections by this point, so that we can also discuss next steps.
- For grading, I'll similarly meet briefly with each group for a demo. We'll
handle the logistics of signing up for that when it gets closer to that time.
You must still submit your code prior to the midnight deadline.
Grading Rubric
This assignment is worth six points.
- 1 point for designing a reasonable protocol to solve the problem. Please
include a very brief protocol reference in your submission.
- 1 point for successfully streaming an audio file over the network.
- 1½ points for the server handling multiple concurrent connections, without
blocking, via select(). Your server should NOT use threading. Using threads
in your client is fine.
- 1½ points for interleaving different messages (play, list, info, stop)
between the client and server. That is, a client should be able to request and
receive info while it is currently playing a song.
- 1 point for server resiliency - the server should be able to cope with
clients joining and departing at any time. Be sure to check that your server
does not crash when trying to both receive from and send to a disconnected
client.
Tips / FAQ
- START EARLY! The earlier you start, the sooner you can ask
questions if you get stuck. Test your code in small increments. It's much
easier to localize a bug when you've only changed a few lines.
- When select() returns, make sure that as you check the FD_SETs, you
differentiate between your server socket (that you accept connections on) and
client sockets. If select tells you that your server socket is ready for
reading, it means you can safely call accept.
- When select() leaves a socket descriptor in your read/write FD_SET after
it returns, it means that you can safely (without blocking) recv/send on that
socket once, but not necessarily more than once.
- Use send() and recv() in as few places as possible. Never call either one
on a socket unless select has told you that it's safe to do so, otherwise,
you'll block (and potentially deadlock).
- The first argument to the select function is (the largest numerical socket
descriptor value in any of the subsequent FD_SET parameters) plus one. For
example, if you're populating your FD_SETs with socket descriptors 7, 9, and
10, your first argument to select should be 11 (10 + 1).
- You may find it useful to set your sockets to non-blocking mode. This is
not required (you should never block regardless because you should never try to
send, recv, or accept unless select tells you it's ok), but it will prevent
deadlocking during your testing. Check their return values and errno for
EAGAIN/EWOULDBLOCK, which indicate that you made a syscall that you shouldn't
have. That's send/recv/accept's way of telling you "I would have blocked on
this call had this socket not been set for non-blocking mode."
- If a client closes a connection while your server is blocked on select(),
select will return that client's socket in the set of file descriptors that are
available for reading (usually named rfds). Then, when your server goes to
recv() on that socket, it will get a return value of 0, which as we saw in lab
1, is recv()'s way of telling us that the connection is closed and no more data
is coming (we've reached the EOF).
Thus, as I've been harping on all semester, you should always check the return
value of your system calls to check for these types of conditions so that your
server can detect and account for disconnecting clients.
It's also possible that a client closes a connection just before your server
attempts to send to it. In this case, by default, two things will happen: 1)
your process will receive the SIGPIPE signal, which by default, kills your
process, and 2) send will return an error and set errno to EPIPE to indicate
the connection (pipe) was broken. Obviously you don't want (1) to occur, since
you still want to service other clients. Luckily, we can easily prevent that
signal by using the that extra "flags" parameter to send that we've been
ignoring thus far. By setting the flags to MSG_NOSIGNAL, the kernel will only
do (2), which is a much more convenient way for you to detect and handle a
client disconnection.
- At the client, you will have multiple forms of IO going on at once: 1)
receiving data from the server, 2) reading commands from user input, and 3)
writing to the sound card (playing audio). Python has a threading
module that will allow you to do these all at once. The module gives you
threads, locks, and condition variables.
I recommend using three threads in the client:
- one thread for calling raw_input in a loop to get commands from the user
(this is already there in the example code)
- one thread for receiving data from the server. This thread sits in a tight
loop that receives until it has gotten a full message, processes the messages,
and then goes back to receiving.
- one thread for playing received music.
Threads 2 and 3 will need to share a buffer (the song data). The receiver
thread will be appending to the end of that buffer, while the player thread
will be removing and playing data from the front of it.
- You can use the Python readline module to get user-friendly command
line behavior for reading commands from the user. (Done for you in provided
client example.)
- If you want your client to begin playing a new file, you need to create a
new MadFile object. This tells mad (our audio library) to interpret the next
bytes as the beginning of a new file (which includes some metadata) rather than
the middle of the previously playing file.
- Wireshark will not help you for this lab, since you're designing the
protocol this time. Wireshark knows nothing about how to decode your
protocol!
If you have any questions about the lab requirements or specification,
please post on Piazza.
Submitting
Please remove any debugging output prior to submitting.
Please do not submit audio files to GitHub!
To submit your code, simply commit your changes locally using git
add and git commit. Then run git push while in your lab
directory.