7.2 通信协议
本例选择使用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包下。