Simple Example

This example deals with a directory browsing protocol - basically a stripped down version of FTP, but without even the file transfer part. We only consider listing a directory name, listing the contents of a directory and changing the current directory - all on the server side, of course. This is a complete worked example of creating all components of a client-server application. It is a simple program which includes messages in both directions, as well as design of messaging protocol.

Look at a simple non-client-server program that allows you to list files in a directory and change and print the directory on the server. We omit copying files, as that adds to the length of the program without really introducing important concepts. For simplicity, all filenames will be assumed to be in 7-bit ASCII. If we just looked at a standalone application first, then the pseudo-code would be

  1. read line from user
  2. while not eof do
  3. if line == dir
  4. list directory
  5. else
  6. if line == cd <dir>
  7. change directory
  8. else
  9. if line == pwd
  10. print directory
  11. else
  12. if line == quit
  13. quit
  14. else
  15. complain
  16. read line from user

A non-distributed application would just link the UI and file access code

single

In a client-server situation, the client would be at the user end, talking to a server somewhere else. Aspects of this program belong solely at the presentation end, such as getting the commands from the user. Some are messages from the client to the server, some are solely at the server end.

dist

For a simple directory browser, assume that all directories and files are at the server end, and we are only transferring file information from the server to the client. The client side (including presentation aspects) will become

  1. read line from user
  2. while not eof do
  3. if line == dir
  4. *list directory*
  5. else
  6. if line == cd <dir>
  7. *change directory*
  8. else
  9. if line == pwd
  10. *print directory*
  11. else
  12. if line == quit
  13. quit
  14. else
  15. complain
  16. read line from user

where the stared lines involve communication with the server.

Alternative presentation aspects

A GUI program would allow directory contents to be displayed as lists, for files to be selected and actions such as change directory to be be performed on them. The client would be controlled by actions associated with various events that take place in graphical objects. The pseudo-code might look like

  1. change dir button:
  2. if there is a selected file
  3. *change directory*
  4. if successful
  5. update directory label
  6. *list directory*
  7. update directory list

The functions called from the different UI’s should be the same - changing the presentation should not change the networking code

Protocol - informal

client request server response
dir send list of files
cd <dir> change dir
send error if failed
send ok if succeed
pwd send current directory
quit quit

Text protocol

This is a simple protocol. The most complicated data structure that we need to send is an array of strings for a directory listing. In this case we don’t need the heavy duty serialisation techniques of the last chapter. In this case we can use a simple text format.

But even if we make the protocol simple, we still have to specify it in detail. We choose the following message format:

  • All messages are in 7-bit US-ASCII
  • The messages are case-sensitive
  • Each message consists of a sequence of lines
  • The first word on the first line of each message describes the message type. All other words are message data
  • All words are separated by exactly one space character
  • Each line is terminated by CR-LF

Some of the choices made above are weaker in real-life protocols. For example

  • Message types could be case-insensitive. This just requires mapping message type strings down to lower-case before decoding
  • An arbitrary amount of white space could be left between words. This just adds a little more complication, compressing white space
  • Continuation characters such as "\" can be used to break long lines over several lines. This starts to make processing more complex
  • Just a "\n" could be used as line terminator, as well as "\r\n". This makes recognising end of line a bit harder

All of these variations exist in real protocols. Cumulatively, they make the string processing just more complex than in our case.

client request server response
send "DIR" send list of files, one per line terminated by a blank line
send "CD <dir>" change dir
send “ERROR” if failed
send “OK”
send "PWD" send current working directory

Server code

  1. /* FTP Server
  2. */
  3. package main
  4. import (
  5. "fmt"
  6. "net"
  7. "os"
  8. )
  9. const (
  10. DIR = "DIR"
  11. CD = "CD"
  12. PWD = "PWD"
  13. )
  14. func main() {
  15. service := "0.0.0.0:1202"
  16. tcpAddr, err := net.ResolveTCPAddr("tcp", service)
  17. checkError(err)
  18. listener, err := net.ListenTCP("tcp", tcpAddr)
  19. checkError(err)
  20. for {
  21. conn, err := listener.Accept()
  22. if err != nil {
  23. continue
  24. }
  25. go handleClient(conn)
  26. }
  27. }
  28. func handleClient(conn net.Conn) {
  29. defer conn.Close()
  30. var buf [512]byte
  31. for {
  32. n, err := conn.Read(buf[0:])
  33. if err != nil {
  34. conn.Close()
  35. return
  36. }
  37. s := string(buf[0:n])
  38. // decode request
  39. if s[0:2] == CD {
  40. chdir(conn, s[3:])
  41. } else if s[0:3] == DIR {
  42. dirList(conn)
  43. } else if s[0:3] == PWD {
  44. pwd(conn)
  45. }
  46. }
  47. }
  48. func chdir(conn net.Conn, s string) {
  49. if os.Chdir(s) == nil {
  50. conn.Write([]byte("OK"))
  51. } else {
  52. conn.Write([]byte("ERROR"))
  53. }
  54. }
  55. func pwd(conn net.Conn) {
  56. s, err := os.Getwd()
  57. if err != nil {
  58. conn.Write([]byte(""))
  59. return
  60. }
  61. conn.Write([]byte(s))
  62. }
  63. func dirList(conn net.Conn) {
  64. defer conn.Write([]byte("\r\n"))
  65. dir, err := os.Open(".")
  66. if err != nil {
  67. return
  68. }
  69. names, err := dir.Readdirnames(-1)
  70. if err != nil {
  71. return
  72. }
  73. for _, nm := range names {
  74. conn.Write([]byte(nm + "\r\n"))
  75. }
  76. }
  77. func checkError(err error) {
  78. if err != nil {
  79. fmt.Println("Fatal error ", err.Error())
  80. os.Exit(1)
  81. }
  82. }

Client code

  1. /* FTPClient
  2. */
  3. package main
  4. import (
  5. "fmt"
  6. "net"
  7. "os"
  8. "bufio"
  9. "strings"
  10. "bytes"
  11. )
  12. // strings used by the user interface
  13. const (
  14. uiDir = "dir"
  15. uiCd = "cd"
  16. uiPwd = "pwd"
  17. uiQuit = "quit"
  18. )
  19. // strings used across the network
  20. const (
  21. DIR = "DIR"
  22. CD = "CD"
  23. PWD = "PWD"
  24. )
  25. func main() {
  26. if len(os.Args) != 2 {
  27. fmt.Println("Usage: ", os.Args[0], "host")
  28. os.Exit(1)
  29. }
  30. host := os.Args[1]
  31. conn, err := net.Dial("tcp", host+":1202")
  32. checkError(err)
  33. reader := bufio.NewReader(os.Stdin)
  34. for {
  35. line, err := reader.ReadString('\n')
  36. // lose trailing whitespace
  37. line = strings.TrimRight(line, " \t\r\n")
  38. if err != nil {
  39. break
  40. }
  41. // split into command + arg
  42. strs := strings.SplitN(line, " ", 2)
  43. // decode user request
  44. switch strs[0] {
  45. case uiDir:
  46. dirRequest(conn)
  47. case uiCd:
  48. if len(strs) != 2 {
  49. fmt.Println("cd <dir>")
  50. continue
  51. }
  52. fmt.Println("CD \"", strs[1], "\"")
  53. cdRequest(conn, strs[1])
  54. case uiPwd:
  55. pwdRequest(conn)
  56. case uiQuit:
  57. conn.Close()
  58. os.Exit(0)
  59. default:
  60. fmt.Println("Unknown command")
  61. }
  62. }
  63. }
  64. func dirRequest(conn net.Conn) {
  65. conn.Write([]byte(DIR + " "))
  66. var buf [512]byte
  67. result := bytes.NewBuffer(nil)
  68. for {
  69. // read till we hit a blank line
  70. n, _ := conn.Read(buf[0:])
  71. result.Write(buf[0:n])
  72. length := result.Len()
  73. contents := result.Bytes()
  74. if string(contents[length-4:]) == "\r\n\r\n" {
  75. fmt.Println(string(contents[0 : length-4]))
  76. return
  77. }
  78. }
  79. }
  80. func cdRequest(conn net.Conn, dir string) {
  81. conn.Write([]byte(CD + " " + dir))
  82. var response [512]byte
  83. n, _ := conn.Read(response[0:])
  84. s := string(response[0:n])
  85. if s != "OK" {
  86. fmt.Println("Failed to change dir")
  87. }
  88. }
  89. func pwdRequest(conn net.Conn) {
  90. conn.Write([]byte(PWD))
  91. var response [512]byte
  92. n, _ := conn.Read(response[0:])
  93. s := string(response[0:n])
  94. fmt.Println("Current dir \"" + s + "\"")
  95. }
  96. func checkError(err error) {
  97. if err != nil {
  98. fmt.Println("Fatal error ", err.Error())
  99. os.Exit(1)
  100. }
  101. }