【Zinx第三章-基礎路由模組】Golang輕量級併發伺服器框架

Aceld發表於2019-04-29

【Zinx 教程目錄】 Zinx 原始碼

https://github.com/aceld/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) 玩家下線

三、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

>原創宣告:未經作者允許請勿轉載,或者轉載請註明出處!

更多原創文章乾貨分享,請關注公眾號
  • 【Zinx第三章-基礎路由模組】Golang輕量級併發伺服器框架
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章