Networking is a natural domain in which to use concurrency since
servers typically handle many connections from their clients at once,
each client being essentially independent of the others.
In this section, we’ll introduce the net
package, which
provides the components for building networked client and server
programs that communicate over TCP, UDP, or Unix domain sockets.
The net/http
package we’ve been using since Chapter 1 is built on top of functions from the
net
package.
Our first example is a sequential clock server that writes the current time to the client once per second:
// Clock1 is a TCP server that periodically writes the time. package main import ( "io" "log" "net" "time" ) func main() { listener, err := net.Listen("tcp", "localhost:8000") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { log.Print(err) // e.g., connection aborted continue } handleConn(conn) // handle one connection at a time } } func handleConn(c net.Conn) { defer c.Close() for { _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) if err != nil { return // e.g., client disconnected } time.Sleep(1 * time.Second) } }
The Listen
function creates a net.Listener
, an object
that listens for incoming connections on a network port,
in this case TCP port localhost:8000
.
The listener’s Accept
method blocks until an incoming connection
request is made, then returns a net.Conn
object representing the connection.
The handleConn
function handles one complete client connection.
In a loop, it writes the current time, time.Now()
, to the client.
Since net.Conn
satisfies the io.Writer
interface, we can
write directly to it.
The loop ends when the write fails, most likely because the client has
disconnected, at which point handleConn
closes its side of the
connection using a deferred call to Close
and goes back to
waiting for another connection request.
The time.Time.Format
method provides a way to format
date and time information by example.
Its argument is a template indicating how to format a reference time,
specifically Mon Jan 2 03:04:05PM 2006 UTC-0700
.
The reference time has eight components (day of the week, month, day of
the month, and so on). Any collection of them can appear in the
Format
string in any order and in a number of formats; the
selected components of the
date and time will be displayed in the selected formats.
Here we are just using the hour, minute, and second of the time.
The time
package defines templates for many standard
time formats, such as time.RFC1123
.
The same mechanism is used in reverse when parsing a time using
time.Parse
.
To connect to the server, we’ll need a client program such as nc
(“netcat”),
a standard utility program for manipulating network connections:
$ go build gopl.io/ch8/clock1 $ ./clock1 & $ nc localhost 8000 13:58:54 13:58:55 13:58:56 13:58:57 ^C
The client displays the time sent by the server each second
until we interrupt the client with Control-C,
which on Unix systems is echoed as ^C
by the shell.
If nc
or netcat
is not installed on your system,
you can use telnet
or this simple Go version of netcat
that
uses net.Dial
to connect to a TCP server:
// Netcat1 is a read-only TCP client. package main import ( "io" "log" "net" "os" ) func main() { conn, err := net.Dial("tcp", "localhost:8000") if err != nil { log.Fatal(err) } defer conn.Close() mustCopy(os.Stdout, conn) } func mustCopy(dst io.Writer, src io.Reader) { if _, err := io.Copy(dst, src); err != nil { log.Fatal(err) } }
This program reads data from the connection and writes it to the
standard output until an end-of-file condition or an error occurs.
The mustCopy
function is a utility used in several examples in
this section.
Let’s run two clients at the same time on different terminals,
one shown to the left and one to the right:
$ go build gopl.io/ch8/netcat1 $ ./netcat1 13:58:54 $ ./netcat1 13:58:55 13:58:56 ^C 13:58:57 13:58:58 13:58:59 ^C $ killall clock1
The killall
command is a Unix utility that
kills all processes with the given name.
The second client must wait until the first client is finished because
the server is sequential; it deals with only one client at a time.
Just one small change is needed to make the server concurrent: adding
the go
keyword to the call to handleConn
causes each
call to run in its own goroutine.
for { conn, err := listener.Accept() if err != nil { log.Print(err) // e.g., connection aborted continue } go handleConn(conn) // handle connections concurrently }
Now, multiple clients can receive the time at once:
$ go build gopl.io/ch8/clock2 $ ./clock2 & $ go build gopl.io/ch8/netcat1 $ ./netcat1 14:02:54 $ ./netcat1 14:02:55 14:02:55 14:02:56 14:02:56 14:02:57 ^C 14:02:58 14:02:59 $ ./netcat1 14:03:00 14:03:00 14:03:01 14:03:01 ^C 14:03:02 ^C $ killall clock2
Exercise 8.1:
Modify clock2
to accept a port number, and
write a program, clockwall
, that acts as a client of several
clock servers at once, reading the times from each one and displaying
the results in a table, akin to the wall of clocks seen in some
business offices.
If you have access to geographically distributed computers, run
instances remotely; otherwise run local instances on different ports
with fake time zones.
$ TZ=US/Eastern ./clock2 -port 8010 & $ TZ=Asia/Tokyo ./clock2 -port 8020 & $ TZ=Europe/London ./clock2 -port 8030 & $ clockwall NewYork=localhost:8010 London=localhost:8020 Tokyo=localhost:8030
Exercise 8.2:
Implement a concurrent File Transfer Protocol (FTP) server.
The server should interpret commands from each client such as
cd
to change directory, ls
to list a directory,
get
to send the contents of a file, and close
to close
the connection.
You can use the standard ftp
command as the client, or write
your own.