We’ll finish this chapter with a chat server that lets several
users broadcast textual messages to each other.
There are four kinds of goroutine in this program.
There is one instance apiece of the main
and broadcaster
goroutines, and for each client connection there is one
handleConn
and one clientWriter
goroutine.
The broadcaster is a good illustration of how select
is used,
since it has to respond to three different kinds of messages.
The job of the main goroutine, shown below, is to listen for and
accept incoming network connections from clients.
For each one, it creates a new handleConn
goroutine, just as
in the concurrent echo server we saw at the start of this chapter.
func main() { listener, err := net.Listen("tcp", "localhost:8000") if err != nil { log.Fatal(err) } go broadcaster() for { conn, err := listener.Accept() if err != nil { log.Print(err) continue } go handleConn(conn) } }
Next is the broadcaster.
Its local variable clients
records the current set of
connected clients.
The only information recorded about each client is the identity of its
outgoing message channel, about which more later.
type client chan<- string // an outgoing message channel var ( entering = make(chan client) leaving = make(chan client) messages = make(chan string) // all incoming client messages ) func broadcaster() { clients := make(map[client]bool) // all connected clients for { select { case msg := <-messages: // Broadcast incoming message to all // clients' outgoing message channels. for cli := range clients { cli <- msg } case cli := <-entering: clients[cli] = true case cli := <-leaving: delete(clients, cli) close(cli) } } }
The broadcaster listens on the global entering
and
leaving
channels for announcements of arriving and departing
clients.
When it receives one of these events, it updates the
clients
set, and if the event was a departure,
it closes the client’s outgoing message channel.
The broadcaster also listens for events on the global messages
channel, to which each client sends all its incoming messages.
When the broadcaster receives one of these events,
it broadcasts the message to every connected client.
Now let’s look at the per-client goroutines.
The handleConn
function creates a new outgoing message channel
for its client and announces the arrival of this client to the
broadcaster over the entering
channel.
Then it reads every line of text from the client, sending each line
to the broadcaster over the global incoming message channel,
prefixing each message with the identity of its sender.
Once there is nothing more to read from the client, handleConn
announces the departure of the client over the leaving
channel
and closes the connection.
func handleConn(conn net.Conn) { ch := make(chan string) // outgoing client messages go clientWriter(conn, ch) who := conn.RemoteAddr().String() ch <- "You are " + who messages <- who + " has arrived" entering <- ch input := bufio.NewScanner(conn) for input.Scan() { messages <- who + ": " + input.Text() } // NOTE: ignoring potential errors from input.Err() leaving <- ch messages <- who + " has left" conn.Close() } func clientWriter(conn net.Conn, ch <-chan string) { for msg := range ch { fmt.Fprintln(conn, msg) // NOTE: ignoring network errors } }
In addition, handleConn
creates a clientWriter
goroutine
for each client that receives messages broadcast to the client’s
outgoing message channel and writes them to the client’s network
connection.
The client writer’s loop terminates when the broadcaster closes
the channel after receiving a leaving
notification.
The display below shows the server in action with two clients in
separate windows on the same computer, using netcat
to chat:
$ go build gopl.io/ch8/chat $ go build gopl.io/ch8/netcat3 $ ./chat & $ ./netcat3 You are 127.0.0.1:64208 $ ./netcat3 127.0.0.1:64211 has arrived You are 127.0.0.1:64211 Hi! 127.0.0.1:64208: Hi! 127.0.0.1:64208: Hi! Hi yourself. 127.0.0.1:64211: Hi yourself. 127.0.0.1:64211: Hi yourself. ^C 127.0.0.1:64208 has left $ ./netcat3 You are 127.0.0.1:64216 127.0.0.1:64216 has arrived Welcome. 127.0.0.1:64211: Welcome. 127.0.0.1:64211: Welcome. ^C 127.0.0.1:64211 has left
While hosting a chat session for n clients, this program
runs 2n+2 concurrently communicating goroutines,
yet it needs no explicit locking operations (§9.2).
The clients
map is confined to a single goroutine,
the broadcaster, so it cannot be accessed concurrently.
The only variables that are shared by multiple goroutines are channels
and instances of net.Conn
, both of which are concurrency
safe.
We’ll talk more about confinement, concurrency safety, and the
implications of sharing variables across goroutines in the next
chapter.
Exercise 8.12:
Make the broadcaster announce the current set of clients to each new
arrival.
This requires that the clients
set and the entering
and
leaving
channels record the client name too.
Exercise 8.13:
Make the chat server disconnect idle clients, such as those that have
sent no messages in the last five minutes.
Hint: calling conn.Close()
in another goroutine unblocks active
Read
calls such as the one done by input.Scan()
.
Exercise 8.14: Change the chat server’s network protocol so that each client provides its name on entering. Use that name instead of the network address when prefixing each message with its sender’s identity.
Exercise 8.15: Failure of any client program to read data in a timely manner ultimately causes all clients to get stuck. Modify the broadcaster to skip a message rather than wait if a client writer is not ready to accept it. Alternatively, add buffering to each client’s outgoing message channel so that most messages are not dropped; the broadcaster should use a non-blocking send to this channel.
18.217.220.114