At this point, the chapter has covered the minimum networking components necessary to create client and service programs. The remainder of the chapter will discuss different versions of a server that implement a monetary currency information service. The service returns ISO 4217 monetary currency information with each request. The intent is to show the implications of creating networked services, along with their clients, using different application-level protocols.
Earlier we introduced a very simple server to demonstrate the necessary steps required to set up a networked service. This section dives deeper into network programming by creating a TCP server that scales to handle many concurrent connections. The server code presented in this section has the following design goals:
The following lists an abbreviated version of the server code. The program uses the curr
package (found at https://github.com/vladimirvivien/learning-go/ch11/curr0), not discussed here, to load monetary currency data from a local CSV file into slice currencies
.
Upon successful connection to a client, the server parses the incoming client commands specified with a simple text protocol with the format GET <currency-filter-value> where <currency-filter-value> specifies a string value used to search for currency information:
import ( "net" ... curr "https://github.com/vladimirvivien/learning-go/ch11/curr0" ) var currencies = curr.Load("./data.csv") func main() { ln, _ := net.Listen("tcp", ":4040") defer ln.Close() // connection loop for { conn, err := ln.Accept() if err != nil { fmt.Println(err) conn.Close() continue } go handleConnection(conn) } } // handle client connection func handleConnection(conn net.Conn) { defer conn.Close() // loop to stay connected with client for { cmdLine := make([]byte, (1024 * 4)) n, err := conn.Read(cmdLine) if n == 0 || err != nil { return } cmd, param := parseCommand(string(cmdLine[0:n])) if cmd == "" { continue } // execute command switch strings.ToUpper(cmd) { case "GET": result := curr.Find(currencies, param) // stream result to client for _, cur := range result { _, err := fmt.Fprintf( conn, "%s %s %s %s ", cur.Name, cur.Code, cur.Number, cur.Country, ) if err != nil { return } // reset deadline while writing, // closes conn if client is gone conn.SetWriteDeadline( time.Now().Add(time.Second * 5)) } // reset read deadline for next read conn.SetReadDeadline( time.Now().Add(time.Second * 300)) default: conn.Write([]byte("Invalid command ")) } } } func parseCommand(cmdLine string) (cmd, param string) { parts := strings.Split(cmdLine, " ") if len(parts) != 2 { return "", "" } cmd = strings.TrimSpace(parts[0]) param = strings.TrimSpace(parts[1]) return }
golang.fyi/ch11/tcpserv0.go
Unlike the simple server introduced in the last section, this server is able to service multiple client connections at the same time. Upon accepting a new connection, with ln.Accept()
, it delegates the handling of new client connections to a goroutine with go handleConnection(conn)
. The connection loop then continues immediately and waits for the next client connection.
The handleConnection
function manages the server communication with the connected client. It first reads and parses a slice of bytes, from the client, into a command string using cmd, param := parseCommand(string(cmdLine[0:n]))
. Next, the code tests the command with a switch
statement. If the cmd
is equal to "GET"
, the code searches slice currencies
for values that matches param
with a call to curr.Find(currencies, param)
. Finally, it streams the search result to the client's connection using fmt.Fprintf(conn, "%s %s %s %s
", cur.Name, cur.Code, cur.Number, cur.Country)
.
The simple text protocol supported by the server does not include any sort of session control or control messages. Therefore, the code uses the conn.SetWriteDeadline
method to ensure the connection to the client does not linger unnecessarily for long periods of time. The method is called during the loop that streams out a response to the client. It is set for a deadline of 5 seconds to ensure the client is always ready to receive the next chunk of bytes within that time, otherwise it times the connection out.
Because the currency server presented earlier uses a simple text-based protocol, it can be tested using a telnet client, assuming the server code has been compiled and running (and listening on port 4040
). The following shows the output of a telnet session querying the server for currency information:
$> telnet localhost 4040 Trying ::1... Connected to localhost. Escape character is '^]'. GET Gourde Gourde HTG 332 HAITI GET USD US Dollar USD 840 AMERICAN SAMOA US Dollar USD 840 BONAIRE, SINT EUSTATIUS AND SABA US Dollar USD 840 GUAM US Dollar USD 840 HAITI US Dollar USD 840 MARSHALL ISLANDS (THE) US Dollar USD 840 UNITED STATES OF AMERICA (THE) ... get india Indian Rupee INR 356 BHUTAN US Dollar USD 840 BRITISH INDIAN OCEAN TERRITORY (THE) Indian Rupee INR 356 INDIA
As you can see, you can query the server by using the get
command followed by a filter parameter as explained earlier. The telnet client sends the raw text to the server which parses it and sends back raw text as the response. You can open multiple telnet sessions against the server and all request are served concurrently in their respective goroutine.
A simple TCP client can also be written in Go to connect to the TCP server. The client captures the command from the console's standard input and sends it to the server as is shown in the following code snippet:
var host, port = "127.0.0.1", "4040" var addr = net.JoinHostPort(host, port) const prompt = "curr" const buffLen = 1024 func main() { conn, err := net.Dial("tcp", addr) if err != nil { fmt.Println(err) return } defer conn.Close() var cmd, param string // repl - interactive shell for client for { fmt.Print(prompt, "> ") _, err = fmt.Scanf("%s %s", &cmd, ¶m) if err != nil { fmt.Println("Usage: GET <search string or *>") continue } // send command line cmdLine := fmt.Sprintf("%s %s", cmd, param) if n, err := conn.Write([]byte(cmdLine)); n == 0 || err != nil { fmt.Println(err) return } // stream and display response conn.SetReadDeadline( time.Now().Add(time.Second * 5)) for { buff := make([]byte, buffLen) n, err := conn.Read(buff) if err != nil { break } fmt.Print(string(buff[0:n])) conn.SetReadDeadline( time.Now().Add(time.Millisecond * 700)) } } }
golang.fyi/ch11/tcpclient0.go
The source code for the Go client follows the same pattern as we have seen in the earlier client example. The first portion of the code dials out to the server using net.Dial()
. Once a connection is obtained, the code sets up an event loop to capture text commands from the standard input, parses it, and sends it as a request to the server.
There is a nested loop that is set up to handle incoming responses from the server (see code comment). It continuously streams incoming bytes into variables buff
with conn.Read(buff)
. This continues until the Read
method encounters an error. The following lists the sample output produced by the client when it is executed:
$> Connected to Global Currency Service curr> get pound Egyptian Pound EGP 818 EGYPT Gibraltar Pound GIP 292 GIBRALTAR Sudanese Pound SDG 938 SUDAN (THE) ... Syrian Pound SYP 760 SYRIAN ARAB REPUBLIC Pound Sterling GBP 826 UNITED KINGDOM OF GREAT BRITAIN (THE) curr>
An even better way of streaming the incoming bytes from the server is to use buffered IO as done in the following snippet of code. In the updated code, the conbuf
variable, of the bufio.Buffer
type, is used to read and split incoming streams from the server using the conbuf.ReadString
method:
conbuf := bufio.NewReaderSize(conn, 1024) for { str, err := conbuf.ReadString(' ') if err != nil { break } fmt.Print(str) conn.SetReadDeadline( time.Now().Add(time.Millisecond * 700)) }
golang.fyi/ch11/tcpclient1.go
As you can see, writing networked services directly on top of raw TCP has some costs. While raw TCP gives the programmer complete control of the application-level protocol, it also requires the programmer to carefully handle all data processing which can be error-prone. Unless it is absolutely necessary to implement your own custom protocol, a better approach is to leverage an existing and proven protocols to implement your server programs. The remainder of this chapter continues to explore this topic using services that are based on HTTP as an application-level protocol.
3.145.202.27