实现链接封装业务与业务绑定

说明

本文档按照实验楼–Go 并发服务器框架 Zinx 入门的文档同步学习记录(大部分内容相同)
https://www.lanqiao.cn/courses/1639
主要有以下原因:
1、模仿大神写教程的风格
2、验证每一个步骤,而不是简简单单的复制教程中的代码。简单重现

本节实验中,我们将完成 Zinx 框架的链接封装与业务绑定的模块。如下面的思维导图中所表示的这些功能。

22

知识点

  • 链接封装
  • 单元测试

ziface创建iconnection.go

zinx/ziface/iconnection.go

因为接口中只定义方法,我们在实验介绍中的接口方法中,已经声明了这些方法。所以我们来直接看一下它的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package ziface
import "net"
//定义连接接口
type IConnection interface {
//启动连接,让当前连接开始工作
Start()
//停止连接,结束当前连接状态M
Stop()
//从当前连接获取原始的socket TCPConn
GetTCPConnection() *net.TCPConn
//获取当前连接ID
GetConnID() uint32
//获取远程客户端地址信息
RemoteAddr() net.Addr
}
//定义一个统一处理链接业务的接口
type HandFunc func(*net.TCPConn, []byte, int) error

znet创建connection.go

zinx/znet/connection.go

我们这里再 Connection 中去实现接口中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package znet
import (
"fmt"
"net"
"zinx/ziface"
)
type Connection struct {
//当前连接的socket TCP套接字
Conn *net.TCPConn
//当前连接的ID 也可以称作为SessionID,ID全局唯一
ConnID uint32
//当前连接的关闭状态
isClosed bool
//该连接的处理方法api
handleAPI ziface.HandFunc
//告知该链接已经退出/停止的channel
ExitBuffChan chan bool
}
//创建连接的方法
func NewConntion(conn *net.TCPConn, connID uint32, callback_api ziface.HandFunc) *Connection{
c := &Connection{
Conn: conn,
ConnID: connID,
isClosed: false,
handleAPI: callback_api,
ExitBuffChan: make(chan bool, 1),
}
return c
}
/* 处理conn读数据的Goroutine */
func (c *Connection) StartReader() {
fmt.Println("Reader Goroutine is running")
defer fmt.Println(c.RemoteAddr().String(), " conn reader exit!")
defer c.Stop()
for {
//读取我们最大的数据到buf中
buf := make([]byte, 512)
cnt, err := c.Conn.Read(buf)
if err != nil {
fmt.Println("recv buf err ", err)
c.ExitBuffChan <- true
continue
}
//调用当前链接业务(这里执行的是当前conn的绑定的handle方法)
if err := c.handleAPI(c.Conn, buf, cnt); err !=nil {
fmt.Println("connID ", c.ConnID, " handle is error")
c.ExitBuffChan <- true
return
}
}
}
//启动连接,让当前连接开始工作
func (c *Connection) Start() {
//开启处理该链接读取到客户端数据之后的请求业务
go c.StartReader()
for {
select {
case <- c.ExitBuffChan:
//得到退出消息,不再阻塞
return
}
}
}
//停止连接,结束当前连接状态M
func (c *Connection) Stop() {
//1. 如果当前链接已经关闭
if c.isClosed == true {
return
}
c.isClosed = true
//TODO Connection Stop() 如果用户注册了该链接的关闭回调业务,那么在此刻应该显示调用
// 关闭socket链接
c.Conn.Close()
//通知从缓冲队列读数据的业务,该链接已经关闭
c.ExitBuffChan <- true
//关闭该链接全部管道
close(c.ExitBuffChan)
}
//从当前连接获取原始的socket TCPConn
func (c *Connection) GetTCPConnection() *net.TCPConn {
return c.Conn
}
//获取当前连接ID
func (c *Connection) GetConnID() uint32{
return c.ConnID
}
//获取远程客户端地址信息
func (c *Connection) RemoteAddr() net.Addr {
return c.Conn.RemoteAddr()
}

重新更正一下Server.go中处理conn的连接业务

我们的修改在 3.3 和 3.4 处。

1
2
3
4
5
//3.3 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
dealConn := NewConntion(conn, cid, CallBackToClient)
cid ++
//3.4 启动当前链接的处理业务
go dealConn.Start()

CallBackToClient 是我们给当前客户端 conn 对象绑定的 handle 方法,当然目前是 server 端强制绑定的回显业务,我们之后会丰富框架,让这个用户可以让用户自定义指定 handle。

测试

进行测试之前,我们这一次选择不使用 go test 的方式进行,这里我们使用两个 go 文件,一个叫做 Server.go 一个叫做 Client.go ,这样分别启动两个文件来模拟客户端请求服务器的过程。当然你也可以选择继续使用 go test 的方式进行测试。这里只是做演示,表示测试也是有多种方法的。

我们在 zinx 文件夹下新建 Server.go 和 Client.go 的文件。 实际上,目前 Zinx 框架的对外接口并未改变,所以 V0.1 的测试依然有效。 所以,我们只是将 Server 和 Client 的功能进行拆分了。

Server.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main
import (
"zinx/znet"
)
//Server 模块的测试函数
func main() {
//1 创建一个server 句柄 s
s := znet.NewServer("[zinx V0.1]")
//2 开启服务
s.Serve()
}
Client.go:

```go

package main
import (
"fmt"
"net"
"time"
)
func main() {
fmt.Println("Client Test ... start")
//3秒之后发起测试请求,给服务端开启服务的机会
time.Sleep(3 * time.Second)
conn,err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client start err, exit!")
return
}
for {
_, err := conn.Write([]byte("hahaha"))
if err !=nil {
fmt.Println("write error err ", err)
return
}
buf :=make([]byte, 512)
cnt, err := conn.Read(buf)
if err != nil {
fmt.Println("read buf error ")
return
}
fmt.Printf(" server call back : %s, cnt = %d\n", buf, cnt)
time.Sleep(1*time.Second)
}
}

然后我们先启动 Server go run Server.go 再启动 Client go run Client.go。

我们可以看到,经过本次完善后,现在服务端就已经开始有回显数据的功能了。

rr

知识点

关于Go defer的详细使用
https://www.cnblogs.com/phpper/p/11984161.html

自我理解:

这些调用直到 return 前才被执。因此,可以用来做资源清理。(到return时,开始从下往上执行输出)