Go微服务实战
上QQ阅读APP看书,第一时间看更新

7.2 通信协议

本例 此案例放在chatserver项目下,所有源码都放在项目的protocol包下。选择使用TCP方式进行连接,在传输的时候直接通过字符串类型进行传递。本书后面还会介绍http和rpc的使用,不过本例需求较为简单,也希望处理通信时更为灵活,所以选择TCP通信方式。

由前面的需求分析来看,可以把客户端与服务器的交互分为以下三种类型的命令。

▪发送命令(SEND):客户端发送一个聊天信息。

▪名字命令(NAME):客户端发送自己的名字。

▪信息命令(MESS):服务器向客户端发送广播信息。

所有命令在信息传递时都以不同的命令区分符(上文括号中的英文编码)开始,并且以\n结束。

比如,发送一个Hello给服务器,那么TCP连接传递的具体字符串就是"SEND Hello/n"。服务器接收到字符串以后,再以"MESS Hello/n"广播给其他客户端。

在具体实现上,三种消息类型分别使用结构体进行定义。下面来看一下代码:


chatserver/protocol/command.go
1. package protocol
2.
3. import "errors"
4.
5. var (
6.     UnknownCommand = errors.New("Unknow command")
7. )
8.
9. type SendCmd struct{
10.     Message string
11. }
12.
13. type NameCmd struct{
14.     Name string
15. }
16.
17. type MessCmd struct{
18.     Name string
19.     Message string
20. }

有了这三个命令对应的struct,还需要实现一个reader,用来从tcp socket中读取字符串;再实现一个writer,用于通过tcp socket写字符串。Go语言提供了接口io.Reader和io.Writer用于数据流的读写,我们可以在程序当中实现这两个接口,让程序可以通过这两个接口读写TCP。

io.Reader和io.Writer是两个高度抽象的接口,可以根据这两个接口对具体的业务进行封装。io.Reader只有一个Read方法,io.Writer则只有一个Write方法。

下面先来实现io.Writer接口:


chatserver/protocol/writer.go
1. package protocol
2.
3. import (
4.     "fmt"
5.     "io"
6. )
7.
8. type Writer struct {
9.     writer io.Writer
10. }
11.
12. func NewWriter(writer io.Writer) *Writer  {
13.     return &Writer{
14.         writer:writer,
15.     }
16. }
17.
18. func (w *Writer) writeString(msg string) error {
19.     _,err := w.writer.Write([]byte(msg))
20.
21.     return err
22. }
23.
24. func (w *Writer) Write(command interface{}) error{
25.     var err error
26.
27.     switch v := command.(type) {
28.     case SendCmd:
29.         err = w.writeString(fmt.Sprintf("SEND %v\n",v.Message))
30.     case MessCmd:
31.         err = w.writeString(fmt.Sprintf("MESSAGE %V %v\n",v.Name, v.Message))
32.     case NameCmd:
33.         err = w.writeString(fmt.Sprintf("NAME %v\n",v.Name))
34.     default:
35.         err = UnknownCommand
36.     }
37.     return err
38. }

第8行至第10行,定义了Writer结构体,里面只有一个io.Writer接口类型的变量writer,主要是为了实现io.Writer接口。

第12行至第16行,根据传入的io.Writer类型的变量返回一个Writer地址。注意,此处返回的是地址。

第18行至第22行,可以在writeString方法内看到调用I/O包的标准Write方法,然后Write方法又重新做了符合业务需求的实现。标准的Write方法的参数是一个byte的切片,返回的是写入的字节数和错误,此处不需要写入字节数,仅取返回的错误即可。

第24行至第38行,是我们自己实现的Write方法,方法内使用switch case方法,实现根据不同的命令区分符调用writeString方法传入不同的信息字符串。

接着来看一下Reader的实现,代码如下:


chatserver/protocol/reader.go
1. package protocol
2.
3. import (
4.     "bufio"
5.     "io"
6.     "log"
7. )
8.
9. type Reader struct {
10.     reader *bufio.Reader
11. }
12.
13. func NewReader(reader io.Reader) *Reader  {
14.     return &Reader{
15.         reader: bufio.NewReader(reader),
16.     }
17. }
18.
19. func (r *Reader) Read() (interface{},error){
20.     cmd,err := r.reader.ReadString(' ')
21.
22.     if err != nil {
23.         return nil,err
24.     }
25.
26.     switch cmd {
27.     case "MESS":
28.         user,err := r.reader.ReadString(' ')
29.         if err != nil {
30.             return nil,err
31.         }
32.         message,err := r.reader.ReadString('\n')
33.         return MessCmd{
34.             user[:len(user)-1],
35.             message[:len(message)-1],
36.         },nil
37.     case "SEND":
38.         message,err := r.reader.ReadString('\n')
39.         if err != nil{
40.             return nil,err
41.         }
42.
43.         return SendCmd{message[:len(message)-1]},nil
44.     case "NAME":
45.         name,err := r.reader.ReadString('\n')
46.
47.         if err != nil{
48.             return nil,err
49.         }
50.         return NameCmd{name[:len(name)-1]},nil
51.     default:
52.         log.Printf("Unknow command:%v",cmd)
53.     }
54.     return nil,UnknownCommand
55. }
56. func (r *Reader) ReadAll() ([]interface{},error){
57.     commands := []interface{}{}
58.     for{
59.         command,err := r.Read()
60.
61.         if command != nil{
62.             commands = append(commands,command)
63.         }
64.
65.         if err == io.EOF{
66.             break
67.         }else if err != nil{
68.             return commands,err
69.         }
70.     }
71.     return commands,nil
72. }

第9行至第11行,定义Reader的结构体,不过内部变量reader不再是io.Reader类型的而是bufio.Reader类型的指针。bufio.Reader是带缓存的读取,其实底层也是io.Reader接口的实现,只是在外部又加了一层封装。

第13行至第17行,NewReader函数用于返回刚定义的Reader结构体实体的地址。注意,bufio.NewReader方法返回的也是一个地址,跟前面定义的Reader结构体内的reader变量是统一的。

第19行至第55行,实现的是Read方法,先在第20行使用bufio.Reader.ReadString(‘ ’)方法从网络中读取字符串,注意本处是读取到第一个空格处,如果要一行一行地读取,则使用ReadString(‘\n’)。第一个空格前的字符就是命令区分符,也就是MESS、SEND或NAME。第27行至第53行,根据不同的命令区分符返回不同的结构体,也就是在protocol/command.go文件内定义的三个结构体。注意,在读取信息的时候读取到换行符‘\n’,而在封装结构体的时候要对字符串进行处理,把最后一个字节(‘\n’)去掉。

第56行至第72行,ReadAll方法是基于前面的Read方法一次性把信息都读出来,不管是什么类型的信息都一次性读出来。

protocol包的内容就介绍完了。截至目前,项目结构如下:


--chatserver
----protocol
------command.go
------reader.go
------writer.go

对于后续信息传递时,具体的信息格式我们也再次梳理一下。

客户端向服务器发一个消息“Hello”,这时具体的传输字符串是这样的:


SEND Hello\n

一定要注意SEND字母都是大写,而且后面带有一个空格。同样,如果客户端要给自己设定名字为“Scott”,则信息传输时的格式为:


NAME Scott\n

对通信协议的介绍到这里就结束了,这是为服务器端和客户端的通信准备的,接下来将基于通信协议完成服务器端和客户端的相关介绍。

[1] 此案例放在chatserver项目下,所有源码都放在项目的protocol包下。