【Zinx第三章-基础路由模块】Golang轻量级并发服务器框架

Zinx源代码

github
https://github.com/aceld/zinx
gitee码云
https://gitee.com/Aceld/zinx


在线开发教程

【B站】
zinx视频教程-Golang轻量级TCP服务器框架-适合自学者

【YouTube】
zinx开发YouTube中国版

微信端文档

技术资源分享.jpg

【Zinx教程目录】
完整教程电子版(在线高清)-下载
Zinx框架视频教程(框架篇)(完整版下载)链接在下面正文
Zinx框架视频教程(应用篇)(完整版下载)链接在下面正文
Zinx开发API文档
Zinx第一章-引言
Zinx第二章-初识Zinx框架
Zinx第三章-基础路由模块
Zinx第四章-全局配置
Zinx第五章-消息封装
Zinx第六章-多路由模式
Zinx第七章-读写分离模型
Zinx第八章-消息队列及多任务
Zinx第九章-链接管理
Zinx第十章-连接属性设置


【Zinx应用案例-MMO多人在线游戏】
(1)案例介绍
(2)AOI兴趣点算法
(3)数据传输协议protocol buffer
(4)Proto3协议定义
(5)构建项目及用户上线
(6)世界聊天
(7)上线位置信息同步
(8)移动位置与AOI广播
(9)玩家下线
(10)模拟客户端AI模块


三、Zinx框架基础路由模块

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

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

​3.1 IRequest 消息请求抽象类

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

A) 创建抽象IRequest层

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

zinx/ziface/irequest.go

package ziface

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

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

B) 实现Request类

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

zinx/znet/request.go

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
}

​ 好了现在我们Request类创建好了,稍后我们会用到它。

3.2 IRouter 路由配置抽象类

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

A) 创建抽象的IRouter层

​ 在ziface下创建irouter.go文件

zinx/ziface/irouter.go

package ziface

/*
    路由接口, 这里面路由是 使用框架者给该链接自定的 处理业务方法
    路由里的IRequest 则包含用该链接的链接信息和该链接的请求数据信息
*/
type IRouter interface{
    PreHandle(request IRequest)  //在处理conn业务之前的钩子方法
    Handle(request IRequest)     //处理conn业务的方法
    PostHandle(request IRequest) //处理conn业务之后的钩子方法
}

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

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

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

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

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

B) 实现Router类

​ 在znet下创建router.go文件

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目录结构应该如下:

.
├── README.md
├── ziface
│   ├── iconnnection.go
│   ├── irequest.go
│   ├── irouter.go
│   └── iserver.go
└── znet
    ├── connection.go
    ├── request.go
    ├── router.go
    ├── server.go
    └── server_test.go

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

A) IServer增添路由添加功能

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

zinx/ziface/irouter.go

package ziface

//定义服务器接口
type IServer interface{
    //启动服务器方法
    Start()
    //停止服务器方法
    Stop()
    //开启业务服务方法
    Serve()
    //路由功能:给当前服务注册一个路由业务方法,供客户端链接处理使用
    AddRouter(router IRouter)
}
B) Server类增添Router成员

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

zinx/znet/server.go

//iServer 接口实现,定义一个Server服务类
type Server struct {
    //服务器的名称
    Name string
    //tcp4 or other
    IPVersion string
    //服务绑定的IP地址
    IP string
    //服务绑定的端口
    Port int
    //当前Server由用户绑定的回调router,也就是Server注册的链接对应的处理业务
    Router ziface.IRouter
}

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

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

    return s
}
C) Connection类绑定一个Router成员

zinx/znet/connection.go

type Connection struct {
    //当前连接的socket TCP套接字
    Conn *net.TCPConn
    //当前连接的ID 也可以称作为SessionID,ID全局唯一
    ConnID uint32
    //当前连接的关闭状态
    isClosed bool

    //该连接的处理方法router
    Router  ziface.IRouter

    //告知该链接已经退出/停止的channel
    ExitBuffChan chan bool
}
D) 在Connection调用注册的Router处理业务

zinx/znet/connection.go

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)
        _, 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读取完客户端数据之后,将数据和conn封装到一个Request中,作为Router的输入数据。

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

3.4 Zinx-V0.3代码实现

zinx/znet/server.go

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
    //当前Server由用户绑定的回调router,也就是Server注册的链接对应的处理业务
    Router ziface.IRouter
}

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

    return s
}
//============== 实现 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...")

        //TODO server.go 应该有一个自动生成ID的方法
        var cid uint32
        cid = 0

        //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 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
            dealConn := NewConntion(conn, cid, s.Router)
            cid ++

            //3.4 启动当前链接的处理业务
            go dealConn.Start()
        }
    }()
}

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 (s *Server)AddRouter(router ziface.IRouter) {
    s.Router = router

    fmt.Println("Add Router succ! " )
}

zinx/znet/conneciont.go

package znet

import (
    "fmt"
    "net"
    "zinx/ziface"
)

type Connection struct {
    //当前连接的socket TCP套接字
    Conn *net.TCPConn
    //当前连接的ID 也可以称作为SessionID,ID全局唯一
    ConnID uint32
    //当前连接的关闭状态
    isClosed bool

    //该连接的处理方法router
    Router  ziface.IRouter

    //告知该链接已经退出/停止的channel
    ExitBuffChan chan bool
}


//创建连接的方法
func NewConntion(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection{
    c := &Connection{
        Conn:     conn,
        ConnID:   connID,
        isClosed: false,
        Router: router,
        ExitBuffChan: make(chan bool, 1),
    }

    return c
}

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)
        _, 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)
    }
}

//启动连接,让当前连接开始工作
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()
}

3.5 使用Zinx-V0.3完成应用程序

​ 接下来我们在基于Zinx写服务器,就可以配置一个简单的路由功能了。

A) 测试基于Zinx完成的服务端应用

Server.go

package main

import (
    "fmt"
    "zinx/ziface"
    "zinx/znet"
)

//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 ping ping error")
    }
}
//Test Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
    fmt.Println("Call PingRouter Handle")
    _, err := request.GetConnection().GetTCPConnection().Write([]byte("ping...ping...ping\n"))
    if err !=nil {
        fmt.Println("call back ping ping ping error")
    }
}

//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 ping ping error")
    }
}

func main(){
    //创建一个server句柄
    s := znet.NewServer("[zinx V0.3]")

    s.AddRouter(&PingRouter{})

    //2 开启服务
    s.Serve()
}

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

B) 启动Server.go
go run Server.go
C) 客户端应用测试程序

和之前的Client.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("Zinx V0.3"))
        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)
    }
}
D) 启动Client.go
go run Client.go

运行结果如下:

服务端:

$ go run Server.go 
Add Router succ! 
[START] Server listenner at IP: 0.0.0.0, Port 7777, is starting
start Zinx server   [zinx V0.3]  succ, now listenning...
Reader Goroutine is  running
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
Call Router PreHandle
Call PingRouter Handle
Call Router PostHandle
...

客户端:

$ go run Client.go 
Client Test ... start
 server call back : before ping ....
, cnt = 17
 server call back : ping...ping...ping
After ping .....
, cnt = 36
 server call back : before ping ....
ping...ping...ping
After ping .....
, cnt = 53
 server call back : before ping ....
ping...ping...ping
After ping .....
, cnt = 53
 server call back : before ping ....
ping...ping...ping
After ping .....
, cnt = 53
...

现在Zinx框架已经有路由功能了,虽然说目前只能配置一个,不过不要着急,很快我们会增加配置多路由的能力。


关于作者:

作者:Aceld(刘丹冰)
简书号:IT无崖子

mail: danbing.at@gmail.com
github: https://github.com/aceld
原创书籍gitbook: http://legacy.gitbook.com/@aceld

原创声明:未经作者允许请勿转载,或者转载请注明出处!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容