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所示。
输入自己的用户名,比如Scott,然后点击login就可以登录了。注意,因为fyne包的问题,标签的文字如果使用汉字就会出现乱码。因为fyne不是本书的重点,所以并未对此进行深入分析,有兴趣的读者可以参考阅读其他相关资料。
在点击login后,聊天界面发生了变化,如图7-2所示。
聊天窗口黑线的上方是聊天历史记录,send按钮的左侧是文字输入框,输入框会随着输入内容的增多而自动变长。输入完成后点击send按钮,可以看到如图7-3所示的结果。
可以看到,服务器又把发送的信息广播回来了,而且在信息的前面加上了用户名。
到此,客户端的开发也介绍完了,如果读者有更多的想法,想实现更多的功能,欢迎在GitHub上fork代码,提交自己的尝试。