简客-记忆

抽象的才是永恒的

0%

说明

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

实验介绍

接下来我们就要对 Zinx 做一个小小的改变,就是与客户端进修数据交互的 Gouroutine 由一个变成两个,一个专门负责从客户端读取数据,一个专门负责向客户端写数据。这么设计有什么好处,当然是目的就是高内聚,模块的功能单一,对于我们今后扩展功能更加方便。

77

知识点

  • Golang 并发模型
  • 读写分离

准备工作

我们希望 Zinx 在升级到 V0.7 版本的时候,架构是下面这样的:

7701

Server 依然是处理客户端的响应,主要关键的几个方法是 Listen、Accept 等。当建立与客户端的套接字后,那么就会开启两个 Goroutine 分别处理读数据业务和写数据业务,读写数据之间的消息通过一个 Channel 传递。下面我们就开始进行实际的实现。

下面我们就开始实现 Zinx V0.7。

添加读写模块交互数据的管道

我们给Connection新增一个管道成员msgChan,作用是用于读写两个 go 的通信。

zinx/znet/connection.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
type Connection struct {
//当前连接的socket TCP套接字
Conn *net.TCPConn
//当前连接的ID 也可以称作为SessionID,ID全局唯一
ConnID uint32
//当前连接的关闭状态
isClosed bool
//消息管理MsgId和对应处理方法的消息管理模块
MsgHandler ziface.IMsgHandle
//告知该链接已经退出/停止的channel
ExitBuffChan chan bool
//无缓冲管道,用于读、写两个goroutine之间的消息通信
msgChan chan []byte
}
//创建连接的方法
func NewConntion(conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection{
c := &Connection{
Conn: conn,
ConnID: connID,
isClosed: false,
MsgHandler: msgHandler,
ExitBuffChan: make(chan bool, 1),
msgChan:make(chan []byte), //msgChan初始化
}
return c
}

创建 Writer Goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
写消息Goroutine, 用户将数据发送给客户端
*/
func (c *Connection) StartWriter() {
fmt.Println("[Writer Goroutine is running]")
defer fmt.Println(c.RemoteAddr().String(), "[conn Writer exit!]")
for {
select {
case data := <-c.msgChan:
//有数据要写给客户端
if _, err := c.Conn.Write(data); err != nil {
fmt.Println("Send Data error:, ", err, " Conn Writer exit")
return
}
case <- c.ExitBuffChan:
//conn已经关闭
return
}
}
}

关于 for select 和 channel 的用法:

select 语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。select 语句的分支分为两种,一种叫做候选分支,另一种叫做默认分支。候选分支总是以关键字 case 开头,后跟一个 case 表达式和一个冒号,然后我们可以从下一行开始写入当分支被选中时需要执行的语句。

由于 select 语句是专为通道而设计的,所以每个 case 表达式中都只能包含操作通道的表达式,比如接收表达式。使用一个接收值可以接收通道里的值,使用两个接收值可以判断通道是否已经关闭了。

对于 select 语句的执行规则如下:

  • 每个 case 都必须是一个通信。
  • 所有 Channel 表达式都会被求值。
  • 所有被发送的表达式都会被求值。
  • 如果任意某个通信可以进行,它就执行,其他被忽略。
  • 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。 否则:
  • 如果有 default 子句,则执行该语句。
  • 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 Channel 或值进行求值。
    注意这里是和 switch 的操作是不一样的,switch 操作中,只要从上到下有一个满足条件了,就会执行相应的那一个 case,select 中,我们是全部计算一遍,然后再从可满足条件的 case 中公平的执行其中一个。这是为了防止有些通道长期得不到执行。

Reader 将发送客户端的数据改为发送至 Channel

修改 Reader 调用的SendMsg()方法

zinx/znet/connection.go

启动 Reader 和 Writer

zinx/znet/connection.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//启动连接,让当前连接开始工作
func (c *Connection) Start() {
//1 开启用户从客户端读取数据流程的Goroutine
go c.StartReader()
//2 开启用于写回客户端数据流程的Goroutine
go c.StartWriter()
for {
select {
case <- c.ExitBuffChan:
//得到退出消息,不再阻塞
return
}
}
}

Zinx 0.7 测试

这里我们的测试代码不需要做任何修改,大家可以想一想为什么。我们这里的测试步骤也和上一节保持一致。

实验总结

我们今天通过 Channel 实现了 Goroutine 的读写分离,关于 Channel 是 Golang 的一个特色机制,大家可以在课下多找一些资料了解其详情。

学习笔记

一般具体执行为 xxxer,比如 StartWriter StartReader , xxHandler

说明

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

实验介绍

本节实验中,我们将完成 Zinx 框架的多路由模块。如下面的思维导图中所表示的这些功能。
66

知识点
  • 多路由模式
  • 单元测试

准备工作

我们之前在已经给 Zinx 配置了路由模式,但是很惨,之前的 Zinx 好像只能绑定一个路由的处理业务方法。显然这是无法满足基本的服务器需求的,那么现在我们要在之前的基础上,给 Zinx 添加多路由的方式。

既然是多路由的模式,我们这里就需要给 MsgId 和对应的处理逻辑进行捆绑。所以我们需要一个 Map。

1
Apis map[uint32] ziface.IRouter

这里起名字是Apis,其中 key 就是 msgId, value 就是对应的 Router,里面应是使用者重写的 Handle 等方法。

那么这个 Apis 应该放在哪呢。

下面,我们再定义一个消息管理模块来进行维护这个Apis。

创建消息管理模块

创建消息管理模块抽象类

在zinx/ziface下创建imsghandler.go文件, 定义出我们之前图片中的方法。

1
2
3
4
5
6
package ziface
// 消息管理抽象层
type IMsgHandler interface {
DoMsgHandler(request IRequest) // 马上以非阻塞方式处理消息
AddRouter(msgId uint32,router IRouter) // 为消息添加具体的处理逻辑
}

这里面有两个方法,AddRouter()就是添加一个 msgId 和一个路由关系到 Apis 中,那么DoMsgHandler()则是调用 Router 中具体Handle()等方法的接口。

实现消息管理模块

在zinx/znet下创建msghandler.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
package znet
import (
"fmt"
"strconv"
"zinx/ziface"
)

type MsgHandle struct {
Apis map[uint32] ziface.IRouter // 存放每个MsgId 所对应的处理方法的map属性
}

func NewMsgHandle *MsgHandle {
return &MsgHandle {
Apis: make(map[uint32]ziface.IRouter),
}
}

// 马上以非阻塞方式处理消息
func (mh *MsgHandle) DoMsgHandler(request ziface.IRequest){
handler,ok := mh.Apis[request.GetMsgId()]
if !ok {
fmt.Println("api msgId= ",request.GetMsgId()," is not Round!")
return
}
// 执行对应处理方法
handler.PreHandle(request)
handler.Handle(request)
handler.PostHandle(request)
}

// 为消息添加具体的处理逻辑
func (mh *MsgHandle) AddRouter (msgId uint32,router ziface.IRouter){
//1 判断当前msg绑定的API处理方法是否已经存在
if _,ok := mh.Apis[msgId]; ok {
panic("repeated api ,msgId = " + strconv.Itoa(int(msgId)))
}
// 2 添加msg与api的绑定关系
mh.Apis[msgId] = router
fmt.Println(" Add api msgId =" ,msgId)
}

Zinx-V0.6 代码实现

首先iserver的AddRouter()的接口要稍微改一下,增添 MsgId 参数.

iserver.go:

1
2
3
4
5
6
7
8
9
10
11
12
package ziface
//定义服务器接口
type IServer interface{
//启动服务器方法
Start()
//停止服务器方法
Stop()
//开启业务服务方法
Serve()
//路由功能:给当前服务注册一个路由业务方法,供客户端链接处理使用
AddRouter(msgId uint32, router IRouter)
}

其次,Server类中 之前有一个Router成员 ,代表唯一的处理方法,现在应该替换成MsgHandler成员。

zinx/znet/server.go

1
2
3
4
5
6
7
8
9
10
11
12
type Server struct {
//服务器的名称
Name string
//tcp4 or other
IPVersion string
//服务绑定的IP地址
IP string
//服务绑定的端口
Port int
//当前Server的消息管理模块,用来绑定MsgId和对应的处理方法
msgHandler ziface.IMsgHandle
}

初始化 Server 自然也要更正,增加 msgHandler 初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
创建一个服务器句柄
*/
func NewServer () ziface.IServer {
utils.GlobalObject.Reload()
s:= &Server {
Name :utils.GlobalObject.Name,
IPVersion:"tcp4",
IP:utils.GlobalObject.Host,
Port:utils.GlobalObject.TcpPort,
msgHandler: NewMsgHandle(), //msgHandler 初始化
}
return s
}

然后当 Server 在处理 conn 请求业务的时候,创建 conn 的时候也需要把 msgHandler 作为参数传递给 Connection 对象。也就是在我们 server.go 的 Start() 方法中的 3.3 注释下进行如下修改:

1
2
3
//...
dealConn := NewConntion(conn, cid, s.msgHandler)
//...

最后,我们的 AddRouter 方法做了修改,所以要重新实现接口方法:

1
2
3
4
//路由功能:给当前服务注册一个路由业务方法,供客户端链接处理使用
func (s *Server)AddRouter(msgId uint32, router ziface.IRouter) {
s.msgHandler.AddRouter(msgId,router)
}

那么接下来就是 Connection 对象了。固然在 Connection 对象中应该有 MsgHandler 的成员,来查找消息对应的回调路由方法。

zinx/znet/connection.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Connection struct {
//当前连接的socket TCP套接字
Conn *net.TCPConn
//当前连接的ID 也可以称作为SessionID,ID全局唯一
ConnID uint32
//当前连接的关闭状态
isClosed bool
//消息管理MsgId和对应处理方法的消息管理模块
MsgHandler ziface.IMsgHandle
//告知该链接已经退出/停止的channel
ExitBuffChan chan bool
}
//创建连接的方法
func NewConntion(conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection{
c := &Connection{
Conn: conn,
ConnID: connID,
isClosed: false,
MsgHandler: msgHandler,
ExitBuffChan: make(chan bool, 1),
}
return c
}

最后,在 conn 已经拆包之后,需要调用路由业务的时候,我们只需要让 conn 调用 MsgHandler 中的DoMsgHander()方法就好了。

zinx/znet/connection.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
func (c *Connection) StartReader() {
fmt.Println("[Reader Goroutine is running]")
defer fmt.Println(c.RemoteAddr().String(), "[conn Reader exit!]")
defer c.Stop()
for {
// 创建拆包解包的对象
dp := NewDataPack()
//读取客户端的Msg head
headData := make([]byte, dp.GetHeadLen())
if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
fmt.Println("read msg head error ", err)
break
}
//拆包,得到msgid 和 datalen 放在msg中
msg , err := dp.Unpack(headData)
if err != nil {
fmt.Println("unpack error ", err)
break
}
//根据 dataLen 读取 data,放在msg.Data中
var data []byte
if msg.GetDataLen() > 0 {
data = make([]byte, msg.GetDataLen())
if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
fmt.Println("read msg data error ", err)
continue
}
}
msg.SetData(data)
//得到当前客户端请求的Request数据
req := Request{
conn:c,
msg:msg,
}
//从绑定好的消息和对应的处理方法中执行对应的Handle方法
go c.MsgHandler.DoMsgHandler(&req)
}
}

好了,大功告成,我们来测试一下 Zinx 的多路由设置功能吧。

使用 Zinx-V0.6 完成应用程序

这里我们既然完成了多路由模式,那么就可以进行一个服务端,多个客户端的方式进行测试我们的功能模块了。

我们在 Server 端设置 2 个路由,一个是 MsgId 为 0 的消息会执行 PingRouter{}重写的Handle()方法,一个是 MsgId 为 1 的消息会执行 HelloZinxRouter{}重写的Handle()方法。

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
package main
import (
"fmt"
"zinx/ziface"
"zinx/znet"
)
//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}
//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
fmt.Println("Call PingRouter Handle")
//先读取客户端的数据,再回写ping...ping...ping
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
err := request.GetConnection().SendMsg(0, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}
//HelloZinxRouter Handle
type HelloZinxRouter struct {
znet.BaseRouter
}
func (this *HelloZinxRouter) Handle(request ziface.IRequest) {
fmt.Println("Call HelloZinxRouter Handle")
//先读取客户端的数据,再回写ping...ping...ping
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
err := request.GetConnection().SendMsg(1, []byte("Hello Zinx Router V0.6"))
if err != nil {
fmt.Println(err)
}
}
func main() {
//创建一个server句柄
s := znet.NewServer()
//配置路由
s.AddRouter(0, &PingRouter{})
s.AddRouter(1, &HelloZinxRouter{})
//开启服务
s.Serve()
}

我们现在写两个客户端,分别发送 0 消息和 1 消息来进行测试 Zinx 是否能够处理 2 个不同的消息业务。

Client01.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
47
48
49
50
51
52
53
54
55
56
57
package main
import (
"fmt"
"io"
"net"
"time"
"zinx/znet"
)
/*
模拟客户端
*/
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 {
//发封包message消息
dp := znet.NewDataPack()
msg, _ := dp.Pack(znet.NewMsgPackage(1,[]byte("Zinx V0.6 Client1 Test Message")))
_, err := conn.Write(msg)
if err !=nil {
fmt.Println("write error err ", err)
return
}
//先读出流中的head部分
headData := make([]byte, dp.GetHeadLen())
_, err = io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
if err != nil {
fmt.Println("read head error")
break
}
//将headData字节流 拆包到msg中
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}
if msgHead.GetDataLen() > 0 {
//msg 是有data数据的,需要再次读取data数据
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
//根据dataLen从io中读取字节流
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}
fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}
time.Sleep(1*time.Second)
}
}

测试结果:
bbss

实验总结

今天我们完成了 zinx 框架的多路由模式,使得其有了对多个客户端提供服务的功能,下一小节中,我们将继续实现 zinx 的读写分离模块。

学习笔记
1
handler,ok := mh.Apis[request.GetMsgId()] // 直接获取数据,这种不可能是产生err,通过ok 返回是否存在(理解)

说明

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

实验介绍

本节实验中,我们将完成 Zinx 框架的消息封装模块。如下面的思维导图中所表示的这些功能。
55

知识点

tcp 封包拆包
消息封装

Zinx 的消息封装

接下来我们再对 Zinx 做一个简单的升级,现在我们把服务器的全部数据都放在一个 Request 里,当前的 Request 结构如下:

1
2
3
4
type Request struct {
conn ziface.IConnection //已经和客户端建立好的链接
data []byte //客户端请求的数据
}

很明显,现在是用一个[]byte来接受全部数据,又没有长度,又没有消息类型,这不科学。怎么办呢?我们现在就要自定义一种消息类型,把全部的消息都放在这种消息类型里。

创建消息封装类型

在zinx/ziface/下创建imessage.go文件: 将请求的一个消息封装到 message 中,定义抽象层接口,定义好 Getter 方法和 Setter 方法。

zinx/ziface/imessage.go

1
2
3
4
5
6
7
8
9
10
package ziface
// 将请求消息封装到message中,定义抽象层接口
type IMessage interface{
GetDataLen() uint32 //获取消息数据段长度
GetMsgId() uint32 // 获取消息ID
GetData() []byte // 获取消息内容
SetMsgId(uint32) // 设置消息id
SetData([]byte) // 设置消息内容
SetDataLen(uint32) // 设置消息数据长度
}

同时创建实例 message 类,在zinx/znet/下,创建message.go文件。

整理一个基本的 message 包,会包含消息 ID,数据,数据长度三个成员,提供基本的 setter 和 getter 方法,目的是为了以后做封装优化的作用。同时也提供了一个创建一个 message 包的初始化方法NewMegPackage。

这里我们只需要要实现 Message 类,写出构造函数,实现接口中对应的方法,比较的简单,大家可以试试先自己尝试实现。

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
package znet
type Message struct {
Id uint32
DataLen uint32
Data []byte
}
//创建一个Message消息包
func NewMsgPackage(id uint32, data []byte) *Message {
return &Message{
Id : id,
DataLen : uint32(len(data)),
Data: data,
}
}
//获取消息数据段长度
func (msg *Message) GeDataLen() uint32 {
return msg.DataLen
}
// 获取消息ID
func (msg *Message) GetMsgId() uint32 {
return msg.Id
}
// 获取消息内容
func (msg *Message) GetData() []byte {
return msg.Data
}
// 获取消息内容
func (msg *Message) SetDataLen(len uint32) {
msg.DataLen = len
}
// 设置消息ID
func (msg *Message) SetMsgId(msgId uint32) {
msg.Id = msgId
}
// 设置消息内容
func (msg *Message) SetData(data []byte){
msg.Data = data
}

消息的封包与拆包

我们这里就是采用经典的 TLV(Type-Len-Value)封包格式来解决 TCP 粘包问题吧。
5501

图片来源于 zinx 作者。

由于 Zinx 也是 TCP 流的形式传播数据,难免会出现消息 1 和消息 2 一同发送,那么 zinx 就需要有能力区分两个消息的边界,所以 Zinx 此时应该提供一个统一的拆包和封包的方法。在发包之前打包成如上图这种格式的有 head 和 body 的两部分的包,在收到数据的时候分两次进行读取,先读取固定长度的 head 部分,得到后续 Data 的长度,再根据 DataLen 读取之后的 body。这样就能够解决粘包的问题了。

创建拆包封包抽象类

在zinx/ziface下,创建idatapack.go文件

我们需要三个方法:

  • 封包数据。

  • 拆包数据。

  • 得到头部长度。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     package ziface
    /*
    封包数据和拆包数据
    直接面向TCP连接中的数据流,为传输数据添加头部信息,用于处理TCP粘包问题。
    */
    type IDataPack interface {
    GetHeadLen() uint32 // 获取包头长度方法
    Pack(msg IMessage)([]byte,error) // 封包方法
    Unpack([]byte)(IMessage,error) // 拆包方法
    }
实现拆包封包类

在zinx/znet/下,创建datapack.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
47
48
49
50
51
52
53
54
55
56
57
58
59
package znet
import (
"bytes"
"encoding/binary"
"errors"
"zinx/ziface"
"zinx/utils"
)
// 封包拆包类实例,暂时不需要成员
type DataPack struct {}
// 封包拆包实例初始化方法
func NewDataPack() *DataPack {
return &DataPack{}
}
// 获取包头长度方法
func (dp *DataPack) GetHeadLen() uint32 {
//Id uint32(4字节) + DataLen uint32(4字节)
return 8
}
// 封装方法(压缩数据)
func (dp *DataPack) Pack(msg ziface.IMessage) ([]byte, error) {
// 创建一个存放bytes字节的缓冲
dataBuff := bytes.NewBuffer([]byte{})
// 写datalen
if err := binary.Write(dataBuff,binary.LittleEndian,msg.GetDataLen()); err != nil {
return nil, err
}
// 写msgId
if err := binary.Write(dataBuff,binary.LittleEndian,msg.GetMsgId()); err != nil {
return nil, err
}
// 写data数据
if err := binary.Write(dataBuff,binary.LittleEndian,msg.GetData()); err != nil {
return nil, err
}
return dataBuff.Bytes(), nil
}

// 拆包方法(解压数据)
func (dp *DataPack) Unpack(binaryData []byte ) (ziface.IMessage ,error){
// 创建一个从输入二进制数据的ioReader
dataBuff := bytes.NewReader(binaryData)
// 只解压head的信息,得到dataLen和msgID
msg := &Message{}
// 读dataLen
if err := binary.Read(dataBuff, binary.LittleEndian, &msg.DataLen); err != nil {
return nil, err
}
// 读取msgID
if err := binary.Read(dataBuff, binary.LittleEndian,&msg.Id); err != nil {
return nil, err
}
// 判断dataLen的长度是否超出我们允许的最大包长度
if (utils.GlobalObject.MaxPacketSize > 0 && msg.DataLen > utils.GlobalObject.MaxPacketSize){
return nil,errors.New("Too Large msg data recieved")
}
//这里只需要把head的数据拆包出来就可以了,然后再通过head的长度,再从conn读取一次数据
return msg, nil
}

需要注意的是整理的Unpack方法,因为我们从上图可以知道,我们进行拆包的时候是分两次过程的,第二次是依赖第一次的 dataLen 结果,所以Unpack只能解压出包头 head 的内容,得到 msgId 和 dataLen。之后调用者再根据 dataLen 继续从 io 流中读取 body 中的数据。

测试拆包封包功能

为了容易理解,我们先不用集成 zinx 框架来测试,而是使用 Server 和 Client 来测试一下封包拆包的功能。

TestPackServer.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
47
48
49
50
51
52
53
54
package main
import (
"fmt"
"io"
"net"
"zinx/znet"
)

// 只是辅助测试datapack拆包,封包功能
func main() {
// 创建socket TCP Server
listenner,err := net.Listen("tcp","127.0.0.1:7777")
if err != nil {
fmt.Println("server listen err:",err)
return
}
//创建服务器gotoutine,负责从客户端goroutine读取粘包的数据,然后进行解析
for {
conn, err := listenner.Accept()
if err != nil {
fmt.Println(" server accept error", err)
}
// 处理客户端请求
go func(conn net.Conn) {
// 创建封包拆包对象dp
dp := znet.NewDataPack()
for {
// 1、想读取六中的head部分
headData := make([]byte,dp.GetHeadLen())
_,err := io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
if err != nil {
fmt.Println(" read head error",err)
}
// 将headData字节流 拆包到msg中
msgHead,err := dp.Unpack(headData)
if err != nil {
fmt.Println(" server unpack error",err)
}
if msgHead.GetDataLen() > 0 {
//msg 是有data数据的,需要再次读取data数据
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
//根据dataLen从io中读取字节流
_,err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println(" server unpack data err:", err)
return
}
fmt.Println(" ==> Recv Msg : ID=",msg.Id,",len=",msg.DataLen,",data=",string(msg.Data))
}
}
}(conn)
}
}

TestPackClient.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
47
48
package main
import (
"fmt"
"net"
"zinx/znet"
)

func main() {
//客户端goroutine,负责模拟粘包的数据,然后进行发送
//客户端goroutine,负责模拟粘包的数据,然后进行发送
conn, err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client dial err:", err)
return
}
//创建一个封包对象 dp
dp := znet.NewDataPack()
// 封装一个msg1包
msg1 := &znet.Message{
Id: 0,
DataLen:5,
Data: []byte{'h','e','l','l','o'},
}
sendData1, err := dp.Pack(msg1)
if err != nil {
fmt.Println(" client pack msg1 err", err)
return
}
msg2 := &znet.Message{
Id:1,
DataLen:7,
Data: []byte{'w', 'o', 'r', 'l', 'd', '!', '!'},
}

sendData2 , err := dp.Pack(msg2)
if err != nil {
fmt.Println(" client pack msg2 err:", err)
return
}
//将sendData1,和 sendData2 拼接一起,组成粘包
sendData1 = append(sendData1,sendData2...)
// 向服务器端写数据
conn.Write(sendData1)
//客户端阻塞
select {

}
}

这里,我们的消息封装模块就完成了,下面我们将其集成到 zinx 中。

Zinx-V0.5 代码实现
Request 字段修改

首先我们要将我们之前的 Request 中的[]byte类型的 data 字段改成 Message 类型.。并且我们需要把 irequest 的方法新增一个 GetMsgID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package znet
import "zinx/ziface"
type Request struct {
conn ziface.IConnection //已经和客户端建立好的 链接
msg ziface.IMessage //客户端请求的数据
}
//获取请求连接信息
func(r *Request) GetConnection() ziface.IConnection {
return r.conn
}
//获取请求消息的数据
func(r *Request) GetData() []byte {
return r.msg.GetData()
}
//获取请求的消息的ID
func (r *Request) GetMsgID() uint32 {
return r.msg.GetMsgId()
}
1
2
3
4
5
6
7
8
9
10
package ziface
/*
IRequest 接口:
实际上是把客户端请求的链接信息 和 请求的数据 包装到了 Request里
*/
type IRequest interface{
GetConnection() IConnection //获取请求连接信息
GetData() []byte //获取请求消息的数据
GetMsgID() uint32 //hu获取消息的id
}
集成拆包过程

接下来我们需要在 Connection 的StartReader()方法中

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
func (c *Connection) StartReader() {
fmt.Println("Reader Goroutine is running")
defer fmt.Println(c.RemoteAddr().String(), " conn reader exit!")
defer c.Stop()
for {
// 创建拆包解包的对象
dp := NewDataPack()
//读取客户端的Msg head
headData := make([]byte, dp.GetHeadLen())
if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
fmt.Println("read msg head error ", err)
c.ExitBuffChan <- true
continue
}
//拆包,得到msgid 和 datalen 放在msg中
msg , err := dp.Unpack(headData)
if err != nil {
fmt.Println("unpack error ", err)
c.ExitBuffChan <- true
continue
}
//根据 dataLen 读取 data,放在msg.Data中
var data []byte
if msg.GetDataLen() > 0 {
data = make([]byte, msg.GetDataLen())
if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
fmt.Println("read msg data error ", err)
c.ExitBuffChan <- true
continue
}
}
msg.SetData(data)
//得到当前客户端请求的Request数据
req := Request{
conn:c,
msg:msg, //将之前的buf 改成 msg
}
//从路由Routers 中找到注册绑定Conn的对应Handle
go func (request ziface.IRequest) {
//执行注册的路由方法
c.Router.PreHandle(request)
c.Router.Handle(request)
c.Router.PostHandle(request)
}(&req)
}
}
提供封包方法

现在我们已经将拆包的功能集成到 Zinx 中了,但是使用 Zinx 的时候,如果我们希望给用户返回一个 TLV 格式的数据,总不能每次都经过这么繁琐的过程,所以我们应该给 Zinx 提供一个封包的接口,供 Zinx 发包使用。 我们在 iconnection.go 中新增 SendMsg()方法。

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
//直接将Message数据发送数据给远程的TCP客户端
SendMsg(msgId uint32, data []byte) error
}

然后,我们到 connection.go 中实现这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//直接将Message数据发送数据给远程的TCP客户端
func (c *Connection) SendMsg(msgId uint32, data []byte) error {
if c.isClosed == true {
return errors.New("Connection closed when send msg")
}
//将data封包,并且发送
dp := NewDataPack()
msg, err := dp.Pack(NewMsgPackage(msgId, data))
if err != nil {
fmt.Println("Pack error msg id = ", msgId)
return errors.New("Pack error msg ")
}
//写回客户端
if _, err := c.Conn.Write(msg); err != nil {
fmt.Println("Write msg id ", msgId, " error ")
c.ExitBuffChan <- true
return errors.New("conn Write error")
}
return nil
}

注意,做出修改后,我们需要在 connection.go 中将 io 和 errors 包引入进来。

现在我们所需要的方法就全部完成了,下面我们来编写功能测试模块。

使用 Zinx-V0.5 完成应用程序

我们这里测试依然继续使用 Server.go 和 Client.go 的方法。

当前 Server 端是先把客户端发送来 Msg 解析,然后返回一个 MsgId 为 1 的消息,消息内容是”ping…ping…ping”

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
47
48
49
50
51
52
53
package main
import (
"fmt"
"zinx/znet"
"zinx/ziface"
)

//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter //一定要先基础BaseRouter
}

// test PreHandle
// func (this *PingRouter) PreHandle(request ziface.IRequest){
// fmt.Println(" Call Router PreHandle")
// _,err := request.GetConnection().GetTCPConnection().Write([]byte("before ping ....\n"))
// if err != nil{
// fmt.Println("call back ping err")
// }
// }


// test Handle
func (this *PingRouter) Handle(request ziface.IRequest){
fmt.Println(" Call Router Handle")
fmt.Println("recv from client : msgId=", request.GetMsgId(), ", data=", string(request.GetData()))
//回写数据
err := request.GetConnection().SendMsg(1, []byte("ping...ping...ping"))
if err != nil{
fmt.Println("call back ping err")
}
}

// test PostHandle
// func (this *PingRouter) PostHandle(request ziface.IRequest){
// fmt.Println(" Call Router PostHandle")
// _,err := request.GetConnection().GetTCPConnection().Write([]byte("after ping ....\n"))
// if err != nil{
// fmt.Println("call back ping err")
// }
// }




//Server 模块的测试函数
func main() {
//1 创建一个server 句柄 s
s := znet.NewServer()
s.AddRouter(&PingRouter{})
//2 开启服务
s.Serve()
}

这里 Client 客户端,模拟一个 MsgId 为 0 的”Zinx V0.5 Client Test Message”消息,然后把服务端返回的数据打印出来。

Client.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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main
import (
"fmt"
"net"
"time"
"io"
"zinx/znet"
)
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 {
//发封包message消息
dp := znet.NewDataPack()
msg, _ := dp.Pack(znet.NewMsgPackage(0,[]byte("Zinx V0.5 Client Test Message")))
_, err := conn.Write(msg)
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)

//先读出流中的head部分
headData := make([]byte, dp.GetHeadLen())
_, err = io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
if err != nil {
fmt.Println("read head error")
break
}
//将headData字节流 拆包到msg中
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}
if msgHead.GetDataLen() > 0 {
//msg 是有data数据的,需要再次读取data数据
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
//根据dataLen从io中读取字节流
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}
fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}


time.Sleep(1*time.Second)
}
}

测试结果:
wwwd

本节实验中,我们将完成 Zinx 框架的全局配置模块。如下面的思维导图中所表示的这些功能。

44

知识点

  • json 格式问题
  • 全局配置文件的好处

Zinx 的全局配置

随着架构逐步的变大,参数就会越来越多,为了省去我们后续大频率修改参数的麻烦,接下来 Zinx 需要做一个加载配置的模块,和一个全局获取 Zinx 参数的对象。 这样也方便了我们以后的维护操作,试想一下,如果你的项目里每个类的配置都写在自己的类方法里,后续出现需要更改配置的时候再去修改配置就会很麻烦。所以,通常我们都会用一个全局的配置功能来方便后续维护。

Zinx-V0.4 增添全局配置代码实现

我们先做一个简单的加载配置模块,要加载的配置文件的文本格式,就选择比较通用的json格式,配置信息暂时如下:

我们先在 zinx 文件夹下新建 conf 文件夹,然后再 conf 文件夹下新建 zinx.json 文件。

配置信息如下:

1
2
3
4
5
6
{
"Name": "demo server",
"Host": "127.0.0.1",
"TcpPort": 7777,
"MaxConn": 3
}

这里需要大家注意一下 json 的书写规范如下:

1、数组或对象之中的字符串必须使用双引号,不能使用单引号。
2、对象的成员名称必须使用双引号。
3、数组或对象最后一个成员的后面,不能加逗号 。
4、数组或对象的每个成员的值,可以是简单值,也可以是复合值。简单值分为四种:字符串、数值(必须以十进制表示)、布尔值和 null(NaN, Infinity, -Infinity 和 undefined 都会被转为 null)。复合值分为两种:符合 JSON 格式的对象和符合 JSON 格式的数组。

现在我们需要建立一个全局配置信息的对象。

创建全局参数文件

创建zinx/utils文件夹,在下面创建globalobj.go文件,暂时编写如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package utils
import (
"encoding/json"
"io/ioutil"
"zinx/ziface"
)
/*
存储一切有关Zinx框架的全局参数,供其他模块使用
一些参数也可以通过 用户根据 zinx.json来配置
*/
type GlobalObj struct {
TcpServer ziface.IServer //当前Zinx的全局Server对象
Host string //当前服务器主机IP
TcpPort int //当前服务器主机监听端口号
Name string //当前服务器名称
Version string //当前Zinx版本号
MaxPacketSize uint32 //都需数据包的最大值
MaxConn int //当前服务器主机允许的最大链接个数
}
/*
定义一个全局的对象
*/
var GlobalObject *GlobalObj

我们在全局定义了一个GlobalObject对象,目的就是让其他模块都能访问到里面的参数。

提供 init 初始化方法

然后我们提供一个init()方法,目的是初始化GlobalObject对象,和加载服务端应用配置文件conf/zinx.json

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
package utils
import (
"encoding/json"
"io/ioutil"
"zinx/ziface"
)
/*
存储一切有关Zinx框架的全局参数,供其他模块使用
一些参数也可以通过 用户根据 zinx.json来配置
*/
type GlobalObj struct {
TcpServer ziface.IServer //当前Zinx的全局Server对象
Host string //当前服务器主机IP
TcpPort int //当前服务器主机监听端口号
Name string //当前服务器名称
Version string //当前Zinx版本号
MaxPacketSize uint32 //都需数据包的最大值
MaxConn int //当前服务器主机允许的最大链接个数
}
/*
定义一个全局的对象
*/
var GlobalObject *GlobalObj
//读取用户的配置文件
func (g *GlobalObj) Reload() {
data, err := ioutil.ReadFile("conf/zinx.json")
if err != nil {
panic(err)
}
//将json数据解析到struct中
//fmt.Printf("json :%s\n", data)
err = json.Unmarshal(data, &GlobalObject)
if err != nil {
panic(err)
}
}
/*
提供init方法,默认加载
*/
func init() {
//初始化GlobalObject变量,设置一些默认值
GlobalObject = &GlobalObj{
Name: "ZinxServerApp",
Version: "V0.4",
TcpPort: 7777,
Host: "0.0.0.0",
MaxConn: 12000,
MaxPacketSize:4096,
}
//从配置文件中加载一些用户配置的参数
GlobalObject.Reload()
}

这里的 init 方法其实是 Golang 的一个特性,在执行 Go 语言程序时,Golang 会先看各个包里有没有 init 方法,如果有就先执行初始化。初始化方法全都运行完之后才会执行主函数。

硬参数替换与 Server 初始化参数配置

我们这里来修改 znet/server.go 文件的 NewServer 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这里别忘了在头部 import "zinx/utils"

/*
创建一个服务器句柄
*/
func NewServer () ziface.IServer {
//先初始化全局配置文件
utils.GlobalObject.Reload()
s:= &Server {
Name :utils.GlobalObject.Name,//从全局参数获取
IPVersion:"tcp4",
IP:utils.GlobalObject.Host,//从全局参数获取
Port:utils.GlobalObject.TcpPort,//从全局参数获取
Router: nil,
}
return s
}

我们为方便验证我们的参数已经成功被加载,在Server.Start()方法中加入几行调试信息

1
2
3
4
5
6
7
func (s *Server) Start() {
// 如果我们的方法正确,运行时就会打印出Version: V0.4, MaxConn: 3, MaxPacketSize: 4096
fmt.Printf("[START] Server name: %s,listenner at IP: %s, Port %d is starting\n", s.Name, s.IP, s.Port)
fmt.Printf("[Zinx] Version: %s, MaxConn: %d, MaxPacketSize: %d\n",

// 其他代码
}

实验总结

我们今天完成了对我们框架进行全局配置的实现。同时使用到了 json 反序列化的方式使配置文件变成了配置对象,来方便我们对其进行操作。

point有以下几种状态

graph LR

    剑阵阵位 --> 引导状态-飞剑降落到此处
    剑阵阵位 --> 过渡反射状态-中间过渡一下
    剑阵阵位 --> 停留状态-为下次攻击准备
    剑阵阵位 --> 无状态


    停留状态-为下次攻击准备--> 可偏转剑阵
    停留状态-为下次攻击准备--> 可提前攻击
    停留状态-为下次攻击准备--> 双击每个点能量足够可原路攻击回去



    剑阵出招 --> 3个点
    剑阵出招 --> 4个点

背景

平常开发vue项目,有双向绑定,数据和展示是分离的,不需要关系什么时候去更新view
但cocos中,view是需要自己去更新。搜索很久终于发现了一篇博客

参考

CocosCreator开发中为什么get/set如此重要

使用

以下是自己在ts中是实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对msgEnemy变量进行的动态绑定,先定义一个私有变量
private _msgEnemy:msg.Enemy = null // 此处msg.Enemy是一个protobuf结构

@property
get msgEnemy():msg.Enemy{
//console.log('get msgEnemy')
return this._msgEnemy;
}
set msgEnemy(value:msg.Enemy){//set:必须无返回值类型
//console.log('set msgEnemy')
this._msgEnemy=value;
this.node.getChildByName('progressBar').getChildByName('maxHP').getComponent(cc.Label).string = this._msgEnemy.MaxHP.toString()
this.node.getChildByName('progressBar').getChildByName('HP').getComponent(cc.Label).string = this._msgEnemy.HP.toString()
this.node.getChildByName('progressBar').getChildByName('bar').width = Math.ceil(this._msgEnemy.HP/this._msgEnemy.MaxHP)*100
}

当进行血量减少时

1
2
3
subHP(val){
this.msgEnemy = Object.assign(this.msgEnemy,{HP:this.msgEnemy.HP - val}); // 只有整个替换才会触发set
}

结语

使用 get/set 的好处
灵活控制读写权限
为抽象做准备
数据可靠性
可以实现 MVVM

实验介绍

本节实验中,我们将完成 Zinx 框架的基础路由的模块。如下面的思维导图中所表示的这些功能。
33

知识点

路由功能模块

准备

现在我们就给用户提供一个自定义的 conn 处理业务的接口吧,很显然,我们不能把业务处理业务的方法绑死在type HandFunc func(*net.TCPConn, []byte, int) error这种格式中,我们需要定一些interface{}来让用户填写任意格式的连接处理业务方法。

那么,很显然 func 是满足不了我们需求的,我们需要再做几个抽象的接口类。

IRequest 消息请求抽象类

我们现在需要把客户端请求的连接信息和请求的数据,放在一个叫 Request 的请求类里,这样的好处是我们可以从 Request 里得到全部客户端的请求信息,也为我们之后拓展框架有一定的作用,一旦客户端有额外的含义的数据信息,都可以放在这个 Request 里。可以理解为每次客户端的全部请求数据,Zinx 都会把它们一起放到一个 Request 结构体里。

创建抽象 IRequest 层

在 ziface 下创建新文件 irequest.go。

zinx/ziface/irequest.go

1
2
3
4
5
6
7
package ziface
// IRequest 接口:
//实际上是把客户端请求的链接信息 和 请求的数据 包装到了 Request里
type IRequest interface{
GetConnection() IConnection //获取请求连接信息
GetData() []byte //获取请求消息的数据
}

不难看出,当前的抽象层只提供了两个 Getter 方法,所以有个成员应该是必须的,一个是客户端连接,一个是客户端传递进来的数据,当然随着 Zinx 框架的功能丰富,这里面还应该继续添加新的成员。

实现 Request 类

在 znet 下创建 IRequest 抽象接口的一个实例类文件 request.go

zinx/znet/request.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package znet
import (
"zinx/ziface"
)

type Request struct {
conn ziface.IConnection //已经和客户端建立好的 链接
data []byte //客户端请求的数据
}

// 获取请求连接信息
func (r *Request) GetConnection() ziface.IConnection {
return r.conn
}

// 获取请求消息的数据
func (r *Request) GetData() []byte {
return r.data
}

IRouter 路由配置抽象类

现在我们来给 Zinx 实现一个非常简单基础的路由功能,目的当然就是为了快速的让 Zinx 步入到路由的阶段。后续我们会不断的完善路由功能。

创建抽象的 IRouter 层

我们知道 router 实际上的作用就是,服务端应用可以给 Zinx 框架配置当前链接的处理业务方法,之前的 Zinx-V0.2 我们的 Zinx 框架处理链接请求的方法是固定的,现在是可以自定义,并且有 3 种接口可以重写。

Handle:是处理当前链接的主业务函数

PreHandle:如果需要在主业务函数之前有前置业务,可以重写这个方法

PostHandle:如果需要在主业务函数之后又后置业务,可以重写这个方法

当然每个方法都有一个唯一的形参 IRequest 对象,也就是客户端请求过来的连接和请求数据,作为我们业务方法的输入数据。

1
2
3
4
5
6
7
8
9
10
package ziface
/*
路由接口, 这里面路由是 使用框架者给该链接自定的 处理业务方法
路由里的IRequest 则包含用该链接的链接信息和该链接的请求数据信息
*/
type IRouter interface {
PreHandle(request IRequest) //在处理conn业务之前的钩子方法
Handle(request IRequest) //处理conn业务的方法
PostHandle(request IRequest) //处理conn业务之后的钩子方法
}
实现 Router 类

在 znet 下创建 router.go 文件

1
2
3
4
5
6
7
8
9
10
11
12
package znet
import "zinx/ziface"

//实现router时,先嵌入这个基类,然后根据需要对这个基类的方法进行重写
type BaseRouter struct {}

//这里之所以BaseRouter的方法都为空,
// 是因为有的Router不希望有PreHandle或PostHandle
// 所以Router全部继承BaseRouter的好处是,不需要实现PreHandle和PostHandle也可以实例化
func (br *BaseRouter) PreHandle(req ziface.IRequest){}
func (br *BaseRouter) Handle(req ziface.IRequest){}
func (br *BaseRouter) PostHandle(req ziface.IRequest){}

Zinx-V0.3-集成简单路由功能

IServer 增添路由添加功能

我们需要给 IServer 类,增加一个抽象方法 AddRouter,目的也是让 Zinx 框架使用者,可以自定一个 Router 处理业务方法。

zinx/ziface/iserver.go

1
2
3
4
5
6
7
8
9
10
11
12
package ziface
// 定义服务器接口
type IServer interface {
// 启动服务器方法
Start()
// 停止服务器方法
Stop()
// 开启业务服务的方法
Serve()
// 路由功能:给当前服务注册一个路由业务方法,供客户端链接处理使用
AddRouter(router IRouter)
}
Server 类增添 Router 成员

有了抽象的方法,自然 Server 就要实现,并且还要添加一个 Router 成员.

zinx/znet/server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//iServer 接口实现,定义一个Server服务类
type Server struct {
//服务器的名称
Name string
//tcp4 or other
IPVersion string
//服务绑定的IP地址
IP string
//服务绑定的端口
Port int
//当前Server由用户绑定的回调router,也就是Server注册的链接对应的处理业务
Router ziface.IRouter
}
copy

然后NewServer()方法, 初始化 Server 对象的方法也要加一个初始化成员

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
创建一个服务器句柄
*/
func NewServer (name string) ziface.IServer {
s:= &Server {
Name :name,
IPVersion:"tcp4",
IP:"0.0.0.0",
Port:7777,
Router: nil,
}
return s
}
Connection 类绑定一个 Router 成员

zinx/znet/connection.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

type Connection struct {
// 当前链接的socket TCP套接字
Conn *net.TCPConn
// 当前连接的ID 也可以称为作为SessionID,ID全局唯一
ConnID uint32
// 当前连接的关闭状态
isClosed bool
// // 该连接的处理方法api
// handleAPI ziface.HandFunc
//该连接的处理方法router
Router ziface.IRouter
// 告知该连接已经退出/停止的channel
ExitBuffChan chan bool
}
在 Connection 调用注册的 Router 处理业务

zinx/znet/connection.go

这里我们在 conn 读取完客户端数据之后,将数据和 conn 封装到一个 Request 中,作为 Router 的输入数据。

然后我们开启一个 goroutine 去调用给 Zinx 框架注册好的路由业务。

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
// 处理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
}
// 得到当前客户端请求的Request数据
req := Request{
conn: c,
data: buf,
}

// 从路由Routers 中找到注册绑定Conn的对应Handle
go func (request ziface.IRequest){
// 执行注册的路由方法
c.Router.PreHandle(request)
c.Router.Handle(request)
c.Router.PostHandle(request)
}(&req)

// // 调用当前链接业务(这里执行的是当前conn绑定的handle方法)
// if err := c.handleAPI(c.Conn,buf,cnt); err != nil {
// fmt.Println("connID",c.ConnID,"handle is error")
// c.ExitBuffChan <- true
// return
// }
}
}

zinx/znet/server.go

1
2
3
4
5
//路由功能:给当前服务注册一个路由业务方法,供客户端链接处理使用
func (s *Server) AddRouter(router ziface.IRouter){
s.Router = router
fmt.Println("Add Router succ!")
}
1
2
//3.3 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
dealConn := NewConnection(conn,cid,s.Router)

zinx/znet/conneciont.go

1
2
3
4
5
6
7
8
9
10
11
12
// func NewConnection(conn *net.TCPConn,connID uint32,callback_api ziface.HandFunc) *Connection{
func NewConnection(conn *net.TCPConn,connID uint32,router ziface.IRouter) *Connection{
c := &Connection{
Conn: conn,
ConnID: connID,
isClosed: false,
// handleAPI: callback_api,
Router: router,
ExitBuffChan: make(chan bool,1),
}
return c
}
1
2
// cnt,err := c.Conn.Read(buf)
_,err := c.Conn.Read(buf)
测试基于 Zinx 完成的服务端应用

Server.go

我们这里自定义了一个类似 Ping 操作的路由,就是当客户端发送数据,我们的处理业务就是返回给客户端”ping…ping..ping..”, 为了测试,当前路由也同时实现了 PreHandle 和 PostHandle 两个方法。实际上 Zinx 会利用模板的设计模式,依次在框架中调用PreHandle、Handle、PostHandle三个方法。

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
package main
import (
"zinx/znet"
"fmt"
"zinx/ziface"
)

//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter //一定要先基础BaseRouter
}

// test PreHandle
func (this *PingRouter) PreHandle(request ziface.IRequest){
fmt.Println(" Call Router PreHandle")
if _,err := request.GetConnection().GetTCPConnection().Write([]byte("before ping ....\n")){
fmt.Println("call back ping err")
}
}


// test Handle
func (this *PingRouter) Handle(request ziface.IRequest){
fmt.Println(" Call Router Handle")
if _,err := request.GetConnection().GetTCPConnection().Write([]byte(" ping ....\n")){
fmt.Println("call back ping err")
}
}

// test PostHandle
func (this *PingRouter) PostHandle(request ziface.IRequest){
fmt.Println(" Call Router PostHandle")
if _,err := request.GetConnection().GetTCPConnection().Write([]byte("after ping ....\n")){
fmt.Println("call back ping err")
}
}




//Server 模块的测试函数
func main() {
//1 创建一个server 句柄 s
s := znet.NewServer("[zinx V0.1]")
s.AddRouter(&PingRouter{})
//2 开启服务
s.Serve()
}
客户端应用测试程序

和之前的 Client.go 一样 没有改变 。

这里我们进行测试的时候,和上一节一样,大家再测试的时候千万别忘了,启动的时候先启动 Server 再启动 Client。关闭的时候也是先关闭 Server 再关闭 Client。

执行结果如下:
vb

说明

本文档按照实验楼–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时,开始从下往上执行输出)

说明

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

将完成 Zinx 框架的 server 模块。也就是实现如下图所示的功能模块:

11

知识点

  • 服务器基本知识
  • 单元测试

初始化

为了更好的看到 Zinx 框架,首先我们需要构建 Zinx 的最基本的两个模块 zifaceznet

ziface 主要是存放一些 Zinx 框架的全部模块的抽象层接口类,Zinx 框架的最基本的是服务类接口 iserver,定义在 ziface 模块中。

znet 模块是 zinx 框架中网络相关功能的实现,所有网络相关模块都会定义在znet模块中。

我们通过在命令行中运行如下命令,进行初始化操作。

1
wget https://labfile.oss.aliyuncs.com/courses/1639/init.sh && /bin/bash init.sh

现在在我们的 src 目录下的文件路径如下:

1
2
3
4
5
6
7
8
9
10
.
├── init.sh
└── src
└── zinx
├── ziface
│   └── iserver.go
└── znet
└── server.go

4 directories, 3 files

在 ziface 下创建服务模块抽象层 iserver.go
作为接口,我们对外只提供方法,所以我们可以抽象为:

启动服务器方法。
停止服务器方法。
开启业务服务方法。

1
2
3
4
5
6
7
8
9
10
package ziface
//定义服务器接口
type IServer interface{
//启动服务器方法
Start()
//停止服务器方法
Stop()
//开启业务服务方法
Serve()
}

在 znet 下实现服务模块 server.go

我们这里使用 Server 结构体来实现上面接口中所定义的方法。首先,作为一个服务器,必须要有的三个属性就是:

  • 服务器名。
  • 服务器 IP。
  • 服务器监听的端口。
    但是现在的 IP 地址不只有 IPv4 ,IPv6 也在推行当中了。所以我们还需要一个 IPversion 的属性来表示 IP 地址的版本。

所以,这个结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//iServer 接口实现,定义一个Server服务类
type Server struct {
//服务器的名称
Name string
//tcp4 or other
IPVersion string
//服务绑定的IP地址
IP string
//服务绑定的端口
Port int
}

/*
创建一个服务器句柄
*/
func NewServer (name string) ziface.IServer {
s:= &Server {
Name :name,
IPVersion:"tcp4",
IP:"0.0.0.0",
Port:7777,
}
return s
}

启动服务器的实现

启动一个服务器分为三步:

获取 TCP 的地址。
监听服务器地址。
启动 server 网络连接业务。
实现过程如下:

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
//开启网络服务
func (s *Server) Start() {
fmt.Printf("[START] Server listenner at IP: %s, Port %d, is starting\n", s.IP, s.Port)
//开启一个go去做服务端Linster业务
go func() {
//1 获取一个TCP的Addr
addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
if err != nil {
fmt.Println("resolve tcp addr err: ", err)
return
}
//2 监听服务器地址
listenner, err:= net.ListenTCP(s.IPVersion, addr)
if err != nil {
fmt.Println("listen", s.IPVersion, "err", err)
return
}
//已经监听成功
fmt.Println("start Zinx server ", s.Name, " succ, now listenning...")
//3 启动server网络连接业务
for {
//3.1 阻塞等待客户端建立连接请求
conn, err := listenner.AcceptTCP()
if err != nil {
fmt.Println("Accept err ", err)
continue
}
//3.2 TODO Server.Start() 设置服务器最大连接控制,如果超过最大连接,那么则关闭此新的连接
//3.3 TODO Server.Start() 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
//我们这里暂时做一个最大512字节的回显服务
go func () {
//不断的循环从客户端获取数据
for {
buf := make([]byte, 512)
cnt, err := conn.Read(buf)
if err != nil {
fmt.Println("recv buf err ", err)
continue
}
//回显
if _, err := conn.Write(buf[:cnt]); err !=nil {
fmt.Println("write back buf err ", err)
continue
}
}
}()
}
}()
}

剩余两个方法的实现

这里,我们的 Stop 方法和 Serve 方法,只做出一个打印的功能。因为这些功能需要与其他功能相结合使用。

1
2
3
4
5
6
7
8
9
10
11
12
func (s *Server) Stop() {
fmt.Println("[STOP] Zinx server , name " , s.Name)
//TODO: Server.Stop() 将其他需要清理的连接信息或者其他信息 也要一并停止或者清理
}
func (s *Server) Serve() {
s.Start()
//TODO: Server.Serve() 是否在启动服务的时候 还要处理其他的事情呢 可以在这里添加
//阻塞,否则主Go退出, listenner的go将会退出
for {
time.Sleep(10*time.Second)
}
}

完整代码

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
91
92
package znet
import (
"fmt"
"net"
"time"
"zinx/ziface"
)
//iServer 接口实现,定义一个Server服务类
type Server struct {
//服务器的名称
Name string
//tcp4 or other
IPVersion string
//服务绑定的IP地址
IP string
//服务绑定的端口
Port int
}
//============== 实现 ziface.IServer 里的全部接口方法 ========
//开启网络服务
func (s *Server) Start() {
fmt.Printf("[START] Server listenner at IP: %s, Port %d, is starting\n", s.IP, s.Port)
//开启一个go去做服务端Linster业务
go func() {
//1 获取一个TCP的Addr
addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
if err != nil {
fmt.Println("resolve tcp addr err: ", err)
return
}
//2 监听服务器地址
listenner, err:= net.ListenTCP(s.IPVersion, addr)
if err != nil {
fmt.Println("listen", s.IPVersion, "err", err)
return
}
//已经监听成功
fmt.Println("start Zinx server ", s.Name, " succ, now listenning...")
//3 启动server网络连接业务
for {
//3.1 阻塞等待客户端建立连接请求
conn, err := listenner.AcceptTCP()
if err != nil {
fmt.Println("Accept err ", err)
continue
}
//3.2 TODO Server.Start() 设置服务器最大连接控制,如果超过最大连接,那么则关闭此新的连接
//3.3 TODO Server.Start() 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
//我们这里暂时做一个最大512字节的回显服务
go func () {
//不断的循环从客户端获取数据
for {
buf := make([]byte, 512)
cnt, err := conn.Read(buf)
if err != nil {
fmt.Println("recv buf err ", err)
continue
}
//回显
if _, err := conn.Write(buf[:cnt]); err !=nil {
fmt.Println("write back buf err ", err)
continue
}
}
}()
}
}()
}
func (s *Server) Stop() {
fmt.Println("[STOP] Zinx server , name " , s.Name)
//TODO Server.Stop() 将其他需要清理的连接信息或者其他信息 也要一并停止或者清理
}
func (s *Server) Serve() {
s.Start()
//TODO Server.Serve() 是否在启动服务的时候 还要处理其他的事情呢 可以在这里添加
//阻塞,否则主Go退出, listenner的go将会退出
for {
time.Sleep(10*time.Second)
}
}
/*
创建一个服务器句柄
*/
func NewServer (name string) ziface.IServer {
s:= &Server {
Name :name,
IPVersion:"tcp4",
IP:"0.0.0.0",
Port:7777,
}
return s
}

入坑笔记

因为引用了本地的库

1
2
3
4
5
6
import (
"fmt"
"net"
"time"
"zinx/ziface" // 这一行
)

所以
先看下gopath

1
go env

使用前将本地的库临时加进去

1
export GOPATH=$GOPATH:/Users/jinkangli/project/go/my/zinx

其他知识点

Go语言中Goroutine与线程的区别
https://www.cnblogs.com/xi-jie/p/11447905.html

1、什么是Goroutine?

Goroutine是建立在线程之上的轻量级的抽象。它允许我们以非常低的代价在同一个地址空间中并行地执行多个函数或者方法。相比于线程,它的创建和销毁的代价要小很多,并且它的调度是独立于线程的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"time"
)

func learning() {
fmt.Println("My first goroutine")
}

func main() {
go learning()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}

这段代码的输出是:

My first goroutine

main function

如果将Sleep去掉,将会输出的是:

main function

这是因为,和线程一样,golang的主函数(其实也是跑在一个goroutine中)并不会等待其他goroutine结束。如果主goroutine结束了,所有其他goroutine都将结束。

参考

https://blog.csdn.net/aigao3209/article/details/101336418

一、存储过程

什么是存储过程

大多数SQL语句都是针对一个或多个表的单条语句。并非所有的操作都这么简单。

经常会有一个完整的操作需要很多条才能完成。

存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,存储在数据库中经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。

存储过程是数据库中的一个重要对象,任何一个设计良好的数据库应用程序都应该用到存储过程。

为什么要使用存储过程

  • (1).存储过程增强了SQL语言的功能和灵活性。存储过程可以用流控制语句编写,有很强的灵活性,可以完成复杂的判断和较复杂的运算。

  • (2).存储过程允许标准组件是编程。存储过程被创建后,可以在程序中被多次调用,而不必重新编写该存储过程的SQL语句。而且数据库专业人员可以随时对存储过程进行修改,对应用程序源代码毫无影响。

  • (3).存储过程能实现较快的执行速度。如果某一操作包含大量的Transaction-SQL代码或分别被多次执行,那么存储过程要比批处理的执行速度快很多。因为存储过程是预编译的。在首次运行一个存储过程时查询,优化器对其进行分析优化,并且给出最终被存储在系统表中的执行计划。而批处理的Transaction-SQL语句在每次运行时都要进行编译和优化,速度相对要慢一些。

  • (4).存储过程能过减少网络流量。针对同一个数据库对象的操作(如查询、修改),如果这一操作所涉及的Transaction-SQL语句被组织程存储过程,那么当在客户计算机上调用该存储过程时,网络中传送的只是该调用语句,从而大大增加了网络流量并降低了网络负载。

  • (5).存储过程可被作为一种安全机制来充分利用。系统管理员通过执行某一存储过程的权限进行限制,能够实现对相应的数据的访问权限的限制,避免了非授权用户对数据的访问,保证了数据的安全。

为什么不使用存储过程:

  • 1) 可移植性差

  • 2) 对于简单的SQL语句,存储过程没什么优势

  • 3) 如果存储过程中不一定会减少网络传输

  • 4) 如果只有一个用户使用数据库,那么存储过程对安全也没什么影响

  • 5) 团队开发时需要先统一标准,否则后期维护成本大

  • 6) 在大并发量访问的情况下,不宜写过多涉及运算的存储过程

  • 7) 业务逻辑复杂时,特别是涉及到对很大的表进行操作的时候,不如在前端先简化业务逻辑

定义存储过程
语法:

1
2
3
4
5
6
7
create procedure 过程名(参数1,参数2....)

begin

sql语句;

end

创建存储过程之前我们必须修改mysql语句默认结束符 ; 要不能我们不能创建成功

使用delimiter可以修改执行符号

DELIMITER是分割符的意思,因为MySQL默认以”;”为分隔符,如果我们没有声明分割符,那么编译器会把存储过程当成SQL语句进行处理,则存储过程的编译过程会报错,所以要事先用DELIMITER关键字申明当前段分隔符,这样MySQL才会将”;”当做存储过程中的代码,不会执行这些代码,用完了之后要把分隔符还原。

语法:

1
2
3
4
5
6
7
8
9
10
11
delimiter 新执行符号

mysql> delimiter % 这样结束符就为%

mysql> create procedure selCg()

-> begin

-> select * from category;

-> end %

调用存储过程
语法:

1
2
3
4
call 过程名(参数1,参数2);

mysql> call selCg() %

存储过程参数类型
In参数
特点:读取外部变量值,且有效范围仅限存储过程内部

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> delimiter //

mysql> create procedure pin(in p_in int)

-> begin

-> select p_in;

-> set p_in=2;

-> select p_in;

-> end;

-> //

mysql> delimiter ; 使用完马上恢复默认的

mysql> set @p_in=1;

等同于

对比下,

例:定义存储过程 getOneBook,当输入某书籍 id 后,可以调出对应书籍记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> create procedure getOneBook(in b int)

-> begin

-> select * from books where bId=b;

-> end //

Query OK, 0 rows affected (0.01 sec)

mysql> call getOneBook(3);//

+-----+-----------------------------+---------+-----------------------------+-------+------------+--------+-----------+

| bId | bName | bTypeId | publishing | price | pubDate | author | ISBN |

+-----+-----------------------------+---------+-----------------------------+-------+------------+--------+-----------+

| 3 | 网络程序与设计-asp | 2 | 北方交通大学出版社| 43 | 2005-02-01 | 王玥| 75053815x |

+-----+-----------------------------+---------+-----------------------------+-------+------------+--------+-----------+

1 row in set (0.00 sec)

Out参数
特点:不读取外部变量值,在存储过程执行完毕后保留新值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> delimiter //

mysql> create procedure pout(out p_out int)

-> begin

-> select p_out;

-> set p_out=2;

-> select p_out;

-> end;

-> //

mysql> delimiter ;

mysql> set @p_out=1;

mysql> call pout(@p_out);

等同于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
call pout()=2; set @p_out=2;

create procedure pout(out p_outs int) 杯子A的定义

-> begin

-> set p_outs=8; p_outs 理解为杯子A,作用于存储过程内部

-> select p_outs;

-> end; 存储过程执行完毕,杯子A中放的是8

mysql> set @p_out=1; 系统中又定义一个杯子B

select @p_out1 杯子B中放的是1

mysql> call pout(@p_out); 该语句执行时,存储过程内部用过杯子A去保存执行的结果,然后执行完毕,用户拿杯子B,去接杯子A的值,实际上就是等于将杯子A的值赋值给杯子B,所以,执行完后,杯子B里面放的是8

call pout()=8(这个8放在杯子A中); set @p_out(杯子B)=8 (这个8是从杯子A中来的);

select @p_out8


不论你怎么赋值都是2 注意此处的call pout(@p_out); 中的这个@p_out这个参数,是我们在系统中定义的那个@p_out

我们的存储过程中的那个p_out参数是存储过程中自己用的,作用范围仅限于存储过程的的begin到end之间,所以这两个p_out是不同的两个参数

这里只是刚好名字一样而已.

In传入参数,是外部将值传给存储过程来使用的,而out传出参数是为了讲存储过程的执行结果回传给调用他的程序来使用的.

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
mysql> create procedure demo(out pa varchar(200))

-> begin

-> select bName into pa from books where bId=3;

-> end //

调用,执行:

mysql> call demo(@a); //

查看变量@a 中的值:

mysql> select @a;//

+-----------------------------+

| @a |

+-----------------------------+

| 网络程序与设计-asp |

+-----------------------------+

Inout参数
特点:读取外部变量,在存储过程执行完后保留新值<类似银行存款>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> delimiter //

mysql> create procedure pinout(inout p_inout int)

-> begin

-> select p_inout;

-> set p_inout=2;

-> select p_inout;

-> end;

-> //

mysql> delimiter ;

mysql> set @p_inout=1;

mysql> call pinout(@p_inout);

不加参数的情况
如果在创建存储过程时没有指定参数类型,则需要在调用的时候指定参数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql> create table t2(id int(11)); 创建表

mysql> create procedure t2(n1 int)

-> begin

-> set @x=0;

-> repeat set @x=@x+1;

-> insert into t2 values(@x);

-> until @x>n1

-> end repeat;

-> end;

-> //

mysql> delimiter ;

mysql> call t2(5); 循环5

存储过程变量的使用
MySQL中使用declare进行变量定义

变量定义:DECLARE variable_name [,variable_name…] datatype [DEFAULT value];

datatype为MySQL的数据类型,如:int, float, date, varchar(length)

变量赋值: SET 变量名 = 表达式值 [,variable_name = expression …]

变量赋值可以在不同的存储过程中继承

1
2
3
4
5
6
7
8
9
10
11
mysql> create procedure decl()

-> begin

-> declare name varchar(200);

-> set name=(select bName from books where bId=12);

-> select name;

-> end//

存储过程语句的注释
做过开发的都知道,写注释是个利人利己的事情。便于理解维护

MySQL注释有两种风格

“–“:单行注释

“/…../”:一般用于多行注释

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> create procedure decl() --procedure name is decl

->/*procedure body

->/* start begin */

-> begin

-> declare name varchar(200);

-> set name=(select bName from books where bId=12);

-> select name;

-> end//

存储过程流程控制语句
变量作用域:
内部的变量在其作用域范围内享有更高的优先权,当执行到end。变量时,内部变量消失,此时已经在其作用域外,变量不再可见了,应为在存储过程外再也不能找到这个申明的变量,但是你可以通过out参数或者将其值指派给会话变量来保存其值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql > DELIMITER // 

mysql > CREATE PROCEDURE proc3()

-> begin

-> declare x1 varchar(5) default 'outer';

-> begin

-> declare x1 varchar(5) default 'inner';

-> select x1;

-> end;

-> select x1;

-> end;

-> //

mysql > DELIMITER ;

条件语句
1:if-then -else语句

2:case语句:

循环语句:
1:while ···· end while:

while 1 do ……. if **** then break; end while;

2:repeat···· end repeat:

执行操作后检查结果,而while则是执行前进行检查。

3:loop ·····end loop:

loop循环不需要初始条件,这点和while 循环相似,同时和repeat循环一样不需要结束条件, leave语句的意义是离开循环。

4:LABLES 标号:

标号可以用在begin repeat while 或者loop 语句前,语句标号只能在合法的语句前面使用。可以跳出循环,使运行指令达到复合语句的最后一步。

5:ITERATE迭代

通过引用复合语句的标号,来从新开始复合语句

查看存储过程
查看存储过程内容:

mysql> show create procedure demo \G

查看存储过程状态:

mysql> show procedure status \G 查看所有存储过程

修改存储过程:
使用alter语句修改

ALTER {PROCEDURE | FUNCTION} sp_name [characteristic …]
characteristic:
{ CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT ‘string’

sp_name参数表示存储过程或函数的名称

characteristic参数指定存储函数的特性

CONTAINS SQL表示子程序包含SQL语句,但不包含读或写数据的语句;

NO SQL表示子程序中不包含SQL语句

READS SQL DATA表示子程序中包含读数据的语句

MODIFIES SQL DATA表示子程序中包含写数据的语句

SQL SECURITY { DEFINER | INVOKER }指明谁有权限来执行

DEFINER表示只有定义者自己才能够执行

INVOKER表示调用者可以执行

COMMENT ‘string’是注释信息。

/**/

删除存储过程
语法:

方法一:DROP PROCEDURE 过程名

1
mysql> drop procedure p_inout;

方法二:DROP PROCEDURE IF EXISTS存储过程名

这个语句被用来移除一个存储程序。不能在一个存储过程中删除另一个存储过程,只能调用另一个存储过程