A TCP API server

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:

  • Use raw TCP to communicate between client and server
  • Develop a simple text-based protocol, over TCP, for communication
  • Clients can query the server for global currency information with text commands
  • Use a goroutine per connection to handle connection concurrency
  • Maintain connection until the client disconnects

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.

Connecting to the TCP server with telnet

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.

Connecting to the TCP server with Go

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, &param) 
         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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.202.27