Servers are persistent programs that provide services to other programs in a
networked environment. Clients are programs that request services from server
programs over a network. By way of example, your interaction with the web is
entirely the result of a client-server computing model, where the web browser
you are running is the client, requesting services (pages) from remote web
servers which exist purely to service these client requests. To better
understand distributed programming, you will write a TCP-based server
that acts as a dealer for the card game Blackjack.
Criteria that your server should satisfy include:
The game is played in "rounds" in which a number of player connections are accepted by the server, and a hand of blackjack played with each in accordance with the rules provided (see link and restrictions above).
The server initiates a round by accepting the first available connection from its master socket. Player processes connect to the server to express their willingness to participate in the current round.
When a player first connects, it sends the server the passcode 0xfacebeef (note that this is effectively a 32-bit integer, just written in hex here as it is amusing --- ensure you take into consideration network byte order when transmitting this value). This value indicates to the server that this is an actual player that knows the rules of the game (and uses the correct protocol). A player that doesn't send this value within 5 seconds should be disconnected (its socket closed) and ignored. Once the first player is authenticated, the server should proceed to accept additional players in the same fashion until:
Once a set of players have been accepted as outlined above, the server should fork off a child process, which executes the dealer process which will play the game with the connected players (note that the dealer process is to be a separate program - so the main server forks a child which calls exec to become the dealer process). The server should wait for the dealer to finish the round (i.e. child process terminates) and the process repeats itself. Note that the master socket is never closed.
The dealer begins by sending each player their cards, followed by the single visible dealer card (see format below). As soon as a player receives this information they may begin playing their hand in accordance with the rules. Specifically, a player may send the message HIT, to which the dealer will respond with another card (same format), or STAND, to indicate they are satisfied with their hand. If a "HIT" results in the player going bust, which the dealer is aware of as it can keep track of the player's hand, the dealer disconnects the appropriate player connection after "dealing" (sending) the card---the dealer automatically wins that hand with that player.
Important: the dealer must process player's requests as soon as they are received; the dealer will have to monitor all connections for data simultaneously rather than process them in a round-robin fashion.
Once all players have either chosen to stand or gone bust, the dealer plays the hand it is holding for each remaining connected player according to the criteria specified in the restricted rule-set we are using for this exercise. Each card the dealer draws is sent to each remaining player in turn (as noted above). Once the dealer either stands or goes bust, the respective player connection is terminated and the dealer moves on to the next player, if any. Note: since the player knows all cards played, it is capable of determining the outcome of the game for itself.
Once the dealer has completed a hand with all players, or all connections have been terminated in any case, it exits.
Players keep track of their own bets and do their own book-keeping with
respect to winnings/losings (if applicable---it is not necessary to
implement this using gambling conventions; as the server does not track such
information, any implementation of betting or money occurs entirely in the
client which is something you've made purely for your own use; we will use
our client to test your server on submission).
Although it doesn't matter how you do book-keeping within the server generally, it is necessary to have standards for the format in which this information is communicated to clients.
"Cards" are represented by a sequence of 2 char's, the first is the ASCII value of the card (2, 3, 4, 5, 6, 7, 8, 9, T, J, Q, K, A - note that we define "10" to be "T" to allow every card to be represented by a single character), the second is the ASCII value of the suit (S, H, D, C). For example, player's cards: seven of hearts, three of clubs together with the dealer's visible card the ace of diamonds would appear as the stream of characters: 7H3CAD.
"Messages" are passed as ASCII text, terminated by a nul byte ('\0').
The dealer should operate correctly in the face of possibly misbehaving
players. In particular, if a player terminates unexpectedly or sends a
message that doesn't conform to protocol rules, it should be disconnected.
You may assume that a player is terminating if it doesn't respond with its
STAND or HIT messages within 10 seconds (except after
the player "stands", of course). This 10 second limit should be applied to
each connected player individually.
An example of an exchange between a player and the server/dealer for a single hand (from the player's perspective) might go something like this:
In clarifying the "restricted rules" of the game, you are aiming for the following:
In the interest of clarity - please note that once a game has started, the dealer interacts with individual clients in whatever order they individually perform actions until all connected players indicate that they "STAND" (or go BUST and are disconnected); once all players have finished their play in this manner, the game is resolved independently for each connected player in a round-robin fashion as the dealer plays out their hand with each player separately (i.e. in our game, it is not possible for players to see other player's cards, although they are all playing from the same deck).
Note that the communication protocol is completely defined for this assignment and you must implement it correctly and not deviate from it, as we will test your servers with our own client that uses the same protocol. In fact, it should be possible to use arbitrary clients with arbitrary servers, if correctly implemented. You should be seeking out other students and arranging to test your clients against their servers (and vice versa) to ensure you have implemented things appropriately. We will not use your client to talk to your server for grading purposes.
You may use low-level I/O operations as appropriate; however, you should feel
free to use fdopen(3) to attach steams to a socket if it suits you.
Ensure you understand what you are doing, regardless of how you chose to
implement it. The protocol must be correct either way.
Note that the operation of the server involves spawning child processes. As you may recall from A2, a child process wants to report its exit status to its parent, and will hang around in a "zombie" state until it the exit status is consumed. Note that a child process sends its parent a SIGCHLD signal when it is ready to exit.
You will need to have your server set up a signal handler to catch SIGCHLD, and execute a wait(2) call in order for the child to exit properly. This isn't complicated, and in fact is provided as an example in the Labs related to A2. Just be aware that this signal can interrupt blocked system calls (such as select(2)), which cannot be restarted so will appear to return failure. You can access the system- provided errno variable and check what the error is: if it is EINTR it was simply an interrupted system call and should simply be restarted. Otherwise it's a real error and should be handled as is appropriate.
This assignment involves you explicitly using the fork() system call to create new processes, and development and testing will involve experiments with possibly many CPU-bound jobs running in the background.
You are responsible for writing a Makefile to compile your code. Typing
"make", "make blackjack" or "make all" should
result in the compilation of all required components with the result being your
blackjack server and subordinate dealer programs (blackjackd and
dealer). You should ensure that all required dependencies are
specified correctly.