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

7.4 客户端

客户端的主要功能如下:

▪连接服务器。

▪使用用户名登录。

▪发送消息。

▪接收其他人发送的消息。

所有客户端的服务都放到client包里了。注意此处提到的客户端服务指的是偏底层的代码实现,不包括界面部分。客户端其实还包括一个GUI(图形化用户界面),不过我们并未将这部分放到client包里,而是放到专门的gui包中,后面再详细介绍。此处我们重点关注client包的实现。

按照惯例,还是先定义接口,再实现接口。下面先来看一下接口代码:


chatserver/client/client.go
1. package client
2.
3. import (
4.     "github.com/ScottAI/chatserver/protocol"
5. )
6.
7.
8. type Client interface {
9.     Dial(address string) error
10.     Start()
11.     Close()
12.     Send(command interface{}) error
13.     SetName(name string) error
14.     SendMess(message string) error
15.     InComing() chan protocol.MessCmd
16. }

在接口内定义了6个方法,这6个方法如下。

▪Dial:用于客户端向服务器端发起连接请求,参数是服务器端的地址。

▪Start:客户端启动,启动后所有客户端的服务都可以使用。

▪Close:关闭客户端。

▪Send:发送信息,注意参数是任意内容。

▪SetName:设置用户名。

▪SendMess:发送信息,这时候参数是字符串。

接下来在另一个文件内实现这些方法,并且完成所有客户端需要的服务功能:


chatserver/client/tcp_client.go
1. package client
2.
3. import (
4.     "io"
5.     "log"
6.     "net"
7.     "time"
8.
9.     "github.com/ScottAI/chatserver/protocol"
10. )
11.
12. type TcpClient struct {
13.     conn net.Conn
14.     cmdReader *protocol.Reader
15.     cmdWriter *protocol.Writer
16.     name string
17.     incoming chan protocol.MessCmd
18. }
19.
20. func NewClient() *TcpClient  {
21.     return &TcpClient{
22.         incoming:make(chan protocol.MessCmd),
23.     }
24. }
25.
26. func (c *TcpClient) Dial(address string) error {
27.     log.Println(address)
28.     conn,err := net.Dial("tcp",address)
29.
30.     if err == nil {
31.         c.conn = conn
32.     }else {
33.         log.Println("dial error!")
34.         return err
35.     }
36.
37.     c.cmdReader = protocol.NewReader(conn)
38.     c.cmdWriter = protocol.NewWriter(conn)
39.     return err
40. }
41.
42. func (c *TcpClient) Start()  {
43.     log.Println("starting client")
44.     time.Sleep(4*time.Second)
45.     for {
46.         cmd,err := c.cmdReader.Read()
47.
48.         if err == io.EOF{
49.             break
50.         }else if err != nil{
51.             log.Printf("Read error %v",err)
52.         }
53.
54.         if cmd != nil {
55.             switch v := cmd.(type) {
56.             case protocol.MessCmd:
57.                 c.incoming <- v
58.             default:
59.                 log.Printf("Unknown command:%v",v)
60.             }
61.         }
62.     }
63. }
64.
65. func (c *TcpClient) Close()  {
66.     c.conn.Close()
67. }
68.
69. func (c *TcpClient) InComing() chan protocol.MessCmd  {
70.     return c.incoming
71. }
72.
73. func (c *TcpClient) Send(command interface{}) error  {
74.     return c.cmdWriter.Write(command)
75. }
76.
77. func (c *TcpClient) SetName(name string) error  {
78.     return c.Send(protocol.NameCmd{name})
79. }
80.
81. func (c *TcpClient) SendMess(message string) error  {
82.     return c.Send(protocol.SendCmd{
83.         Message:message,
84.     })
85. }

在分析代码以前,先说一个编码习惯。在做包导入的时候,一般是按照包名称的字母排序的,如第4行至第7行。而对于标准包和来自互联网的唯一包资源,比如GitHub的包,是使用一个空行分开的,代码中第8行就是一个空行。

第12行至第18行,定义TcpClient结构体,用于存储客户端服务用到的数据信息。其中conn存放一个网络连接,这是必不可少的。客户端启动后首先就是要创建这个连接,并且保存到conn变量内,后续所有的操作都是基于这个连接的。cmdReader和cmdWriter则用于存放protocol包的Reader和Writer的指针,因为信息的读取和发送都是基于protocol的这两个对象实现的。name用于存放客户端用户自己输入的用户名。incoming则是protocol.MessCmd类型的通道,命令信息都是通过该通道来处理的。

第20行至第24行,创建一个TcpClient的对象实例,并返回引用。在本方法执行后其内部只有incoming变量已经创建,其他变量还需要通过其他方法进行赋值。

第26行至第40行,Dial方法是客户端在启动的时候先要执行的方法,其主要是创建一个tcp连接,并且赋值给conn变量。然后基于tcp连接创建protocol的Reader和Writer,并分别赋值给cmdReader和cmdWriter。

第42行至第63行,用Start方法启动客户端服务。首先要注意第44行,在执行Start方法的时候先休眠了4秒,这是因为在启动client的时候是使用goroutine运行该Start方法的。主goroutine用于启动UI,而UI启动是比较慢的,所以这里要休眠一定时间。具体可以在客户端启动的代码(chatserver/gui/cmd/main.go)中查看。Start内的for循环是一个死循环,要结束该循环只能使内部代码满足条件后执行break命令。第48行不停地判断EOF状态,如果满足条件则结束循环,客户端停止。如果读取没有错误也不是EOF,则对读取的内容进行类型判断。注意第55行的用法,这是取对象的类型的方法,如果读取的对象是protocol.MessCmd类型,则写入通道,否则报错。如果信息写入了通道,UI部分会启动goroutine,从incoming通道内不停地读取信息,这样就完成了从服务器读取信息的功能。

本代码段的其他几个方法比较简单,此处不再一一介绍。

在客户端的服务写好以后,还是要介绍一些GUI的实现。虽然Go语言最擅长的领域是服务端编程,但其在桌面端编程方面也在高速发展。要完成桌面编程,需要借助一些第三方包,如跨平台使用效果比较好的fyne包。开源包fyne的GitHub地址为https://github.com/fyne-io/fyne。fyne包的具体使用方法请读者自己结合其官方提供的文档进行学习。考虑到篇幅问题,本书不对fyne做过多介绍。

读者在开发本部分代码的时候,需要先用go get获取fyne:


$ go get fyne.io/fyne

GUI的功能不再放到client包内,而是单独新建了一个gui包专门存放相关代码。图形化功能基本放在一个文件(gui)内,源码如下:


chatserver/gui/gui.go
1. package gui
2.
3. import (
4.     "fmt"
5.     "fyne.io/fyne"
6.     "fyne.io/fyne/app"
7.     "fyne.io/fyne/layout"
8.     "fyne.io/fyne/widget"
9.
10.     "github.com/ScottAI/chatserver/client"
11. )
12.
13. func StartUi(c client.Client) {
14.     app := app.New()
15.
16.     loginWindow := app.NewWindow("登录")
17.     input := widget.NewEntry()
18.     input.ReadOnly = false
19.     input.Resize(fyne.NewSize(24,5))
20.     label := widget.NewLabel("Please input your name:")
21.     button := widget.NewButton("login", func() {
22.         if len(input.Text) >0 {
23.             c.SetName(input.Text)
24.             label.Hidden=true
25.
26.             input.SetText("")
27.             input.Hidden=true
28.             changeWindow(loginWindow,c)
29.         }
30.     })
31.     loginWindow.SetContent(widget.NewVBox(
32.         label,
33.         input,
34.         button,
35.     ))
36.     loginWindow.Resize(fyne.NewSize(24,24))
37.     loginWindow.ShowAndRun()
38.
39. }
40.
41. func changeWindow(window fyne.Window,c client.Client)  {
42.
43.     history := widget.NewMultiLineEntry()
44.     history.ReadOnly=true
45.     history.Resize(fyne.NewSize(480,300))
46.     input := widget.NewEntry()
47.     input.ReadOnly=false
48.     input.Resize(fyne.NewSize(460,20))
49.     send := widget.NewButton("send", func() {
50.         if len(input.Text)>0 {
51.             fmt.Println("Send start")
52.             c.SendMess(input.Text)
53.             input.SetText("")
54.         }
55.     })
56.     send.Resize(fyne.NewSize(20,20))
57.     group := widget.NewHBox(input,send)
58.     group.Resize(fyne.NewSize(480,20))
59.     content := fyne.NewContainerWithLayout(layout.NewVBoxLayout(),history,group)
60.     content.Resize(fyne.NewSize(480,320))
61.     window.SetContent(content)
62.     window.Resize(fyne.NewSize(480,320))
63.
64.     go func() {
65.         for msg := range c.InComing(){
66.             AddMessage(history,msg.Name,msg.Message)
67.         }
68.     }()
69. }
70. func  AddMessage(history *widget.Entry,user string,msg string)  {
71.     history.SetText(history.Text+"\n"+user+":"+msg)
72.
73. }

第13行至第39行,StartUI方法用于启动桌面的界面,注意,参数是client.Client,这是前面定义的接口,因为界面上的一些操作需要通过这个参数调用相关的方法进行处理,比如第23行,当button事件触发时,会通过client.Client的SetName方法设定用户名。第28行的chaneWindow方法也是在button的事件内触发的,该方法的作用是改变界面,由原来的登录界面改为聊天界面。

第41行至第69行,实现的是改变桌面界面。在此过程中,要特别注意的是第64行至第68行,这几行会运行goroutine,用于不停地刷新聊天界面的信息。也就是说每当服务器广播信息时,都会刷新到聊天界面。

因为案例比较简单,而且偏重服务端的开发,所以这里的GUI做得比较简单,通过这一个源码文件就完成了界面功能。

开发到这里,是不是迫不及待地想看一下运行效果呢?

此时,客户端开发还并不完善,还差一个客户端的启动程序,可通过一个main包和一个main方法实现。在gui文件内新建cmd路径,然后创建main包和main方法:


chatserver/gui/cmd/main.go
1. package main
2.
3. import (
4.     "flag"
5.
6.     "github.com/ScottAI/chatserver/client"
7.     "github.com/ScottAI/chatserver/gui"
8.     "github.com/gpmgo/gopm/modules/log"
9. )
10.
11. func main() {
12.     address := flag.String("server","127.0.0.1:3333","address of server")
13.     flag.Parse()
14.     client := client.NewClient()
15.     err := client.Dial(*address)
16.
17.     if err != nil {
18.         log.Fatal("Error when connect to server",err)
19.     }
20.
21.     defer client.Close()
22.
23.     go client.Start()
24.     gui.StartUi(client)
25. }

客户端的启动方法非常简单,首先通过flag包配置参数。因为是在同一台机器上测试服务器端和客户端,所以配置的地址是127.0.0.1:3333。连接服务器后,第23行启动了客户端服务,第24行启动了桌面的界面。

现在来看一下客户端完成的代码结构:


--chatserver
----client
------client.go
------tcp_client.go
----gui
------gui.go
------cmd
--------main.go

现在我们来看一下运行效果。服务器端运行没有界面,但是客户端运行后首先是登录界面,如图7-1所示。

图7-1 客户端登录界面

输入自己的用户名,比如Scott,然后点击login就可以登录了。注意,因为fyne包的问题,标签的文字如果使用汉字就会出现乱码。因为fyne不是本书的重点,所以并未对此进行深入分析,有兴趣的读者可以参考阅读其他相关资料。

在点击login后,聊天界面发生了变化,如图7-2所示。

图7-2 点击login后的聊天界面

聊天窗口黑线的上方是聊天历史记录,send按钮的左侧是文字输入框,输入框会随着输入内容的增多而自动变长。输入完成后点击send按钮,可以看到如图7-3所示的结果。

图7-3 聊天界面示意图

可以看到,服务器又把发送的信息广播回来了,而且在信息的前面加上了用户名。

到此,客户端的开发也介绍完了,如果读者有更多的想法,想实现更多的功能,欢迎在GitHub上fork代码,提交自己的尝试。