Application-Level Protocols

应用层协议

A client and a server exchange messages consisting of message types and message data. This requires design of a suitable message exchange protocol. This chapter looks at some of the issues involved in this, and gives a complete example of a simple client-server application.

客户端和服务器的交互包括消息类型和消息数据,这就需要有适当的交互协议。本章着重讨论客户端和服务器交互相关的问题,并给出一个完整又简单的客户端服务器交互的例子。

Introduction

介绍

A client and server need to exchange information via messages. TCP and UDP provide the transport mechanisms to do this. The two processes also need to have a protocol in place so that message exchange can take place meaningfully. A protocol defines what type of conversation can take place between two components of a distributed application, by specifying messages, data types, encoding formats and so on.

客户端和服务器需要通过消息来进行交互。TCP和UDP是信息交互的两种传输机制。在这两种传输机制之上就需要有协议来约定传输内容的含义。协议清楚说明分布式应用的两个模块之间交互消息的消息体、消息的数据类型、编码格式等。

Protocol Design

协议设计

There are many possibilities and issues to be decided on when designing a protocol. Some of the issues include:

当设计协议的时候,有许多许多的情况和问题需要考虑,比如:

  • Is it to be broadcast or point to point?

    Broadcast must be UDP, local multicast or the more experimental MBONE. Point to point could be either TCP or UDP.

  • Is it to be stateful vs stateless?

    Is it reasonable for one side to maintain state about the other side? It is often simpler to do so, but what happens if something crashes?

  • Is the transport protocol reliable or unreliable?

    Reliable is often slower, but then you don't have to worry so much about lost messages.

  • Are replies needed?

    If a reply is needed, how do you handle a lost reply? Timeouts may be used.

  • What data format do you want?

    Two common possibilities are MIME or byte encoding.

  • Is your communication bursty or steady stream?

    Ethernet and the Internet are best at bursty traffic. Steady stream is needed for video streams and particularly for voice. If required, how do you manage Quality of Service (QoS)?

  • Are there multiple streams with synchronisation required?

    Does the data need to be synchronised with anything? e.g. video and voice.

  • Are you building a standalone application or a library to be used by others?

    The standards of documentation required might vary.

  • 是广播还是单播?

    广播必须使用UDP,本地组播或者是更成熟的组播骨干网(MBONE)。单播就可以使用TCP或者UDP。

  • 消息应该是有状态还是无状态的?

    任意一边是否有必要维持另外一边的状态消息?虽然这个似乎很好实现,但是如果发生异常的时候又应该怎么做呢?

  • 协议是可靠服务还是不可靠服务?

    一般来说,提供可靠服务就会更慢些,但好处是不需要考虑丢失消息的情况。

  • 是否需要响应?

    如果是需要响应的话,如何处理没有响应回复的情况?或许可以设置超时时间。

  • 你想要使用什么数据格式?

    一般使用两种格式:MIME和字节流

  • 消息流是使用突发性的还是稳定性的?

    Ethernet和Internet最好使用突发性消息流。稳定性的消息流通常是用在视频和声频流传输上。如果要求的话,你如何保证你的服务质量(QoS)?

  • 有多流同步的需求吗?

    是否有多种数据流需要进行同步?比如视频流和声频流。

  • 建立的是单独的应用还是需要给别人使用的库?

    可能需要花很大的精力编写标准文档。

Version control

版本控制

A protocol used in a client/server system will evolve over time, changing as the system expands. This raises compatability problems: a version 2 client will make requests that a version 1 server doesn't understand, whereas a version 2 server will send replies that a version 1 client won't understand.

随着时间变化和系统的升级,客户端/服务器之间的协议也会升级。这可能会引起兼容性的问题:版本2的客户端发出的请求可能版本1的服务器无法解析,反之也一样,版本2的服务器回复的消息版本1的客户端无法解析。

Each side should ideally be able to understand messages for its own version and all earlier ones. It should be able to write replies to old style queries in old style response format.

理想情况下,不论是哪一端,都应该既能满足自己当前版本的消息规范,也能满足早期版本的消息规范。任意一端对于旧版本的请求应该返回旧版本的响应。

应用层协议 - 图1

The ability to talk earlier version formats may be lost if the protocol changes too much. In this case, you need to be able to ensure that no copies of the earlier version still exist - and that is generally imposible.

但是如果协议变化太大的话,可能就很难保持与早期版本的兼容了。在这种情况下,你就需要保证已经不存在早期的版本了 — 当然这个几乎是不可能的。

Part of the protocol setup should involve version information.

所以,协议应该包含有版本消息。

The Web

Web协议

The Web is a good example of a system that is messed up by different versions. The protocol has been through three versions, and most servers/browsers now use the latest version. The version is given in each request

Web协议就是一个由于有不同协议版本同时存在而出现混乱的例子。Web协议已经有三个版本了,通常服务器和浏览器都是使用最新的版本,版本消息包含在请求中:

request version
GET / pre 1.0
GET / HTTP/1.0 HTTP 1.0
GET / HTTP/1.1 HTTP 1.1

But the content of the messages has been through a large number of versions:

但是消息体的内容已经被大量版本制定修改过:

  • HTML versions 1-4 (all different), with version 5 on the horizon;
  • non-standard tags recognised by different browsers;
  • non-HTML documents often require content handlers that may or may not be present - does your browser have a handler for Flash?
  • inconsistent treatment of document content (e.g. some stylesheet content will crash some browsers)
  • Different support for JavaScript (and different versions of JavaScript)
  • Different runtime engines for Java
  • Many pages do not conform to any HTML versions (e.g. with syntax errors)
  • HTML版本1-4(每个版本都不一样),还有即将到来的HTML5;
  • 不同浏览器各自支持非标准化的标签;
  • HTML文档之外的内容也通常需要不同的内容处理器 — 比如你的浏览器支持Flash播放器吗?
  • 文档内容的不一致的处理方法(例如,在一些浏览器上,有的css会冲突)
  • 浏览器对JavaScript的不同支持程度(当然JavaScript也同时存在不同的版本)
  • 不同的Java运行引擎
  • 有的页面并没有遵守任何HTML版本规范(比如HTML格式错误的页面)

Message Format

消息格式

In the last chapter we discussed some possibilities for representing data to be sent across the wire. Now we look one level up, to the messages which may contain such data.

上一章我们讨论了数据传输的几种可能的表现形式。现在我们进一步研究包含数据的消息。

  • The client and server will exchange messages with different meanings. e.g.
    • Login request,
    • get record request,
    • login reply,
    • record data reply.
  • The client will prepare a request which must be understood by the server.
  • The server will prepare a reply which must be understood by the client.
  • 客户端和服务器会交换不同含义的消息,比如:
    • 登陆请求
    • 获取某些记录的请求
    • 登陆请求的回复
    • 获取某些记录请求的回复
  • 客户端必须发送能被服务器解析的请求。
  • 服务器必须回复能被客户端解析的响应。

Commonly, the first part of the message will be a message type.

通常来说,消息的头部必须包含消息类型。

  • Client to server
  1. LOGIN name passwd
  2. GET cpe4001 grade
  • Server to client
  1. LOGIN succeeded
  2. GRADE cpe4001 D
  • 客户端发送给服务器
  1. LOGIN name passwd
  2. GET cpe4001 grade
  • 服务器返回给客户端
  1. LOGIN succeeded
  2. GRADE cpe4001 D

The message types can be strings or integers. e.g. HTTP uses integers such as 404 to mean "not found" (although these integers are written as strings). The messages from client to server and vice versa are disjoint: "LOGIN" from client to server is different to "LOGIN" from server to client.

消息类型应该设置为字符型或者整型。比如,HTTP使用整数404来表示“未找到资源”(尽管这个整型是被当做字符串使用)。客户端到服务器的消息和服务器到客户端的消息是不一样的:比如从客户端到服务器的“LOGIN”消息就不同于服务器到客户端的“LOGIN”消息。

Data Format

数据格式

There are two main format choices for messages: byte encoded or character encoded.

对于消息来说,有两种主要的数据格式可供选择:字节编码和字符编码。

Byte format

字节编码

In the byte format

对于字节编码

  • the first part of the message is typically a byte to distinguish between message types.
  • The message handler would examine this first byte to distinguish message type and then perform a switch to select the appropriate handler for that type.
  • Further bytes in the message would contain message content according to a pre-defined format (as discussed in the previous chapter).
  • 消息的头部通常使用一个字节来标示消息的类型。
  • 消息处理者应该根据消息头部的类型字节来选择合适的处理程序来处理这个类型的消息。
  • 消息后面的字节应该是根据事先定义的格式(上一章节讨论的)来包含消息具体内容。

The advantages are compactness and hence speed. The disadvantages are caused by the opaqueness of the data: it may be harder to spot errors, harder to debug, require special purpose decoding functions. There are many examples of byte-encoded formats, including major protocols such as DNS and NFS , upto recent ones such as Skype. Of course, if your protocol is not publicly specified, then a byte format can also make it harder for others to reverse-engineer it!

字节编码的优势就是紧凑小巧,传输速度快。劣势就是数据的不透明性:字节编码很难定位错误,也很难调试。往往是要求写一些额外的解码函数。有许多字节编码格式的例子,大部分协议都是使用字节编码,例如DNS和NFS协议,还有最近出现的Skype协议。当然,如果你的协议没有公开说明结构,使用字节编码可以让其他人使用反向工程手段很难破解!

Pseudocode for a byte-format server is

字节编码的服务器的伪代码如下

  1. handleClient(conn) {
  2. while (true) {
  3. byte b = conn.readByte()
  4. switch (b) {
  5. case MSG_1: ...
  6. case MSG_2: ...
  7. ...
  8. }
  9. }
  10. }

Go has basic support for managing byte streams. The interface Conn has methods

Go提供了基本的管理字节流的方法。 接口Conn 包含有方法

  1. (c Conn) Read(b []byte) (n int, err os.Error)
  2. (c Conn) Write(b []byte) (n int, err os.Error)

and these methods are implemented by TCPConn and UDPConn.

这两个方法的具体实现类有 TCPConn and UDPConn

Character Format

字符编码

In this mode, everything is sent as characters if possible. For example, an integer 234 would be sent as, say, the three characters '2', '3' and '4' instead of the one byte 234. Data that is inherently binary may be base64 encoded to change it into a 7-bit format and then sent as ASCII characters, as discussed in the previous chapter.

在这个编码模式下,所有消息都尽可能以字符的形式发送。例如,整型数字234会被处理成三个字符‘2’,‘3’,‘4’,而不会被处理成234的字节码。二进制数据将会使用base64编码变成为7-bit的格式,然后当做ASCII码传递,就和我们上一章讨论的一样。

In character format,

对于字符编码,

  • A message is a sequence of one or more lines
  • The start of the first line of the message is typically a word that represents the message type.
  • String handling functions may be used to decode the message type and data.
  • The rest of the first line and successive lines contain the data.
  • Line-oriented functions and line-oriented conventions are used to manage this.
  • 一条消息会是一行或者很多行内容
  • 消息的第一行通常使用一个单词来说明消息的类型。
  • 使用字符处理函数来解码消息类型和消息内容。
  • 第一行之后的信息和其他行包含消息数据。
  • 使用行处理函数和行处理规范来处理消息。

Pseudocode is

伪代码如下

  1. handleClient() {
  2. line = conn.readLine()
  3. if (line.startsWith(...) {
  4. ...
  5. } else if (line.startsWith(...) {
  6. ...
  7. }
  8. }

Character formats are easier to setup and easier to debug. For example, you can use telnet to connect to a server on any port, and send client requests to that server. It isn't so easy the other way, but you can use tools like tcpdump to snoop on TCP traffic and see immediately what clients are sending to servers.

字符编码很容易进行组装,也很容易调试。例如,你可以telnet连接到一台服务器的端口上,然后发送客户的请求到服务器。其他的编码方式无法轻易地监听请求。但是对于字符编码,你可以使用tcpdump 这样的工具监听TCP的交互,并且立刻就能看到客户端发送给服务器端的消息。

There is not the same level of support in Go for managing character streams. There are significant issues with character sets and character encodings, and we will explore these issues in a later chapter.

在Go中没有像字节流那样专门处理字符流的工具。如何处理字符集和字符编码是非常重要的,我们将会在下一章专门讨论这些问题。

If we just pretend everything is ASCII, like it was once upon a time, then character formats are quite straightforward to deal with. The principal complication at this level is the varying status of "newline" across different operating systems. Unix uses the single character '\n'. Windows and others (more correctly) use the pair "\r\n". On the internet, the pair "\r\n" is most common - Unix systems just need to take care that they don't assume '\n'.

如果和以前一样,处理的所有字符都是ASCII码,那么我们能直接又简单地处理这些字符。但是实际上,字符处理复杂的原因是不同的操作系统上有各种不统一的“换行符”。Unix使用简单的'\n' 来表示换行,Windows和其他的系统(这种方法更正确)使用“\r\n”来表示。在实际的网络传输中,使用一对“\r\n”是更通用的方案 — 因为Unix系统只需要注意不要设定换行符只有“\n”就可以满足这个方案。

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.

这个例子展示的是一个文件夹浏览协议 — 基本上就是一个简单的FTP协议,只是连FTP的文件传输都没有实现。我们考虑这个例子包含的功能有:展示文件夹名称,列出文件夹内包含的文件,改变当前文件夹路径 — 当然所有这些文件都是在服务器的。这是一个完整的包含客户端和服务器的例子。这个简单的程序既需要两个方向的消息交互,也需要消息的具体协议设计。

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

在开始例子之前,我们先看一个简单的程序,这个程序不是客户端和服务器交互的程序,它实现的功能包括:展示文件夹中的文件,打印出文件夹在服务器上的路径。在这里我们忽略正在拷贝中的文件,因为考虑这些细节会增加代码长度,却对我们要介绍的重要概念没有什么帮助。简单假设:所有的文件名都是7位的ASCII码。先考虑这个独立的程序,它的伪代码应该是:

  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
  17.  

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

一个非分布式的应用是将UI和文件存储代码连接起来

应用层协议 - 图2

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.

在包含有客户端和服务器的情况下,客户端就代表用户终端,用来和服务器交互。这个程序最独立的部分就是表现层,比如如何获取用户的命令等。这个程序的消息有的是从客户端到服务器,有的只是在服务器。

应用层协议 - 图3

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
  17.  

where the italicised 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

GUI程序可以很方便展示文件夹内容,选择文件,做一些诸如改变文件夹路径的操作。客户端被图形化对象中的各种定义好的事件所驱动从而实现功能。伪代码如下:

  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

不同的UI实现的功能都是一样的 — 改变表现层并不需要改变网络传输的代码

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
  • 所有的消息都是7位的US-ASCII码
  • 所有的消息都是大小写敏感
  • 每条消息都是由一系列的行组成
  • 第一行的第一个单词是用来说明消息类型,其他单词都是具体的消息数据
  • 相邻的单词应该只有一个空格符分隔
  • 每一行以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
  • 消息类型类型应该是大小不写敏感的。 对于表示消息类型的字符串,我们就需要在解码前将它小写化
  • 单词与单词间的多余空白字符应该被丢弃掉。当然这会增加一些代码的复杂度,去处理压缩空白符
  • 像“\”这样的续行符应该被使用,它能将一个大的长句子分隔成几行。从这里开始,程序渐渐变得更复杂了
  • 像“\n”这样的字符也应该能被解析为换行符,就和“\r\n”一样。这个就让辨识解析程序的结束符更为复杂了

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. }

State

状态

Applications often make use of state information to simplify what is going on. For example

应用程序经常保存状态消息来简化下面要做的事情,比如

  • Keeping file pointers to current file location
  • Keeping current mouse position
  • Keeping current customer value.
  • 保存当前文件路径的文件指针状态
  • 保存当前的鼠标位置状态
  • 保存当前的客户值状态

In a distributed system, such state information may be kept in the client, in the server, or in both.

在分布式的系统中,这样的状态消息可能是保存在客户端,服务器,也可能两边都保存。

The important point is to whether one process is keeping state information about itself or about the other process. One process may keep as much state information about itself as it wants, without causing any problems. If it needs to keep information about the state of the other process, then problems arise: the process' actual knowledge of the state of the other may become incorrect. This can be caused by loss of messages (in UDP), by failure to update, or by s/w errors.

最重要的一点是,进程是否需要保存 自身进程 或者其他进程 的状态消息。一个进程保存再多自己的状态信息,也不会引发其他问题。如果需要保存其他进程的状态消息,这个问题就复杂了:当前保存的其他进程的状态消息和实际的状态消息可能是不一致的。这可能会引起消息丢失(在UDP中)、更新失败、或者s/w错误等。

An example is reading a file. In single process applications the file handling code runs as part of the application. It maintains a table of open files and the location in each of them. Each time a read or write is done this file location is updated. In the DCE file system, the file server keeps track of a client's open files, and where the client's file pointer is. If a message could get lost (but DCE uses TCP) these could get out of synch. If the client crashes, the server must eventually timeout on the client's file tables and remove them.

一个例子就是读取文件。在单个进程中,文件处理代码是应用程序的一部分。它维持一个表,表中包含所有打开的文件和文件指针位置。每次文件读写的时候,文件指针位置就会更新。在数据通信(DCE)文件系统中,文件系统必须追踪客户端打开了哪些文件,客户端的文件指针在哪。如果一个消息丢失了(但是DCE是使用TCP的),这些状态消息就不能保持同步了。如果出现客户端崩溃了,服务器就必须对这个表触发超时并删除。

应用层协议 - 图4

In NFS, the server does not maintain this state. The client does. Each file access from the client that reaches the server must open the file at the appropriate point, as given by the client, to perform the action.

在NFS文件系统中,服务器并没有保存这个状态消息,而是有客户端保存的。客户端每次在服务器进行的读取文件操作必须能在准确的文件位置打开文件,而这个文件位置是由客户端提供的,从而才能进行后续的操作。

应用层协议 - 图5

If the server maintains information about the client, then it must be able to recover if the client crashes. If information is not saved, then on each transaction the client must transfer sufficient information for the server to function.

如果由服务器保持客户端的状态消息,服务器必须在客户端崩溃的时候进行修复。如果服务器没有储存状态消息,那么客户端的每次事务交互都需要提供足够的消息来让服务器进行操作。

If the connection is unreliable, then additional handling must be in place to ensure that the two do not get out of synch. The classic example is of bank account transactions where the messages get lost. A transaction server may need to be part of the client-server system.

如果连接是不可靠的,那么必须要有额外的处理程序来确保双方没有失去同步。一个消息丢失的典型例子就是银行账号交易系统。交易系统是客户端与服务器交互的一部分。

Application State Transition Diagram

应用状态转换图

A state transition diagram keeps track of the current state of an application and the changes that move it to new states.

一个状态转换图清晰说明了当前应用的状态和进入到新的状态需要的转换。

Example: file transfer with login:

例如:带登陆功能的文件传输:

应用层协议 - 图6

This can also be expressed as a table

这个也可以使用一个表来表示

Current state Transition Next state
login login failed login
login succeeded file transfer
file transfer dir file transfer
get file transfer
logout login
quit -

Client state transition diagrams

客户端状态转换图

The client state diagram must follow the application diagram. It has more detail though: it writes and then reads

客户端状态转换图就和应用转换图一样。不同的就是要注意更多细节:它包含有操作

Current state Write Read Next state
login LOGIN name password FAILED login
SUCCEEDED file transfer
file transfer CD dir SUCCEEDED file transfer
FAILED file transfer
GET filename #lines + contents file transfer
ERROR file transfer
DIR #files + filenames file transfer
ERROR file transfer
quit none quit
logout none login

Server state transition diagrams

服务器状态转换图

The server state diagram must also follow the application diagram. It also has more detail: it reads and then writes

服务器状态转换图也和应用转换图一样。不同的就是也要注意更多细节:它包含有 操作

Current state Read Write Next state
login LOGIN name password FAILED login
SUCCEEDED file transfer
file transfer CD dir SUCCEEDED file transfer
FAILED file transfer
GET filename #lines + contents file transfer
ERROR file transfer
DIR #files + filenames file transfer
ERROR file transfer
quit none quit
logout none login

Server pseudocode

服务器伪代码

  1. state = login
  2. while true
  3. read line
  4. switch (state)
  5. case login:
  6. get NAME from line
  7. get PASSWORD from line
  8. if NAME and PASSWORD verified
  9. write SUCCEEDED
  10. state = file_transfer
  11. else
  12. write FAILED
  13. state = login
  14. case file_transfer:
  15. if line.startsWith CD
  16. get DIR from line
  17. if chdir DIR okay
  18. write SUCCEEDED
  19. state = file_transfer
  20. else
  21. write FAILED
  22. state = file_transfer
  23. ...

We don't give the actual code for this server or client since it is pretty straightforward.

由于这个伪代码已经足够清晰了,所以我们并不用给出具体的代码了。

Summary

总结

Building any application requires design decisions before you start writing code. For distributed applications you have a wider range of decisions to make compared to standalone systems. This chapter has considered some of those aspects and demonstrated what the resultant code might look like.

任何应用程序在开始编写前都需要详尽的设计。开发一个分布式的系统比开发一个独立系统需要更宽广的视野和思维来做决定和思考。这一章已经考虑到了一些这样的问题,并且展示了最终代码的大致样子。 Copyright Jan Newmarch, jan@newmarch.name

If you like this book, please contribute using Flattr

or donate using PayPal

应用层协议 - 图7