【Zinx第二章-初識Zinx框架】Golang輕量級併發服務器框架

【Zinx教程目錄】
Zinx源代碼
https://github.com/aceld/zinx (請拷貝網址,在瀏覽器打開[簡書不讓自動跳轉])
完整教程電子版(在線高清)-下載
Zinx框架視頻教程(框架篇)(完整版下載)連接在下面正文
Zinx框架視頻教程(應用篇)(完整版下載)連接在下面正文
Zinx開發API文檔
Zinx第一章-引言
Zinx第二章-初識Zinx框架
Zinx第三章-基礎路由模塊
Zinx第四章-全局配置
Zinx第五章-消息封裝
Zinx第六章-多路由模式
Zinx第七章-讀寫分離模型
Zinx第八章-消息隊列及多任務
Zinx第九章-連接管理
Zinx第十章-鏈接屬性設置git


【Zinx應用案例-MMO多人在線遊戲】
(1)案例介紹
(2)AOI興趣點算法
(3)數據傳輸協議protocol buffer
(4)Proto3協議定義
(5)構建項目及用戶上線
(6)世界聊天
(7)上線位置信息同步
(8)移動位置與AOI廣播
(9)玩家下線
(10)模擬客戶端AI模塊github


2、初識Zinx框架

​ 這裏先看一下Zinx最簡單的Server雛形。算法

1. Zinx-V0.1-基礎Server

​ 爲了更好的看到Zinx框架,首先Zinx構建Zinx的最基本的兩個模塊zifaceznetapi

ziface主要是存放一些Zinx框架的所有模塊的抽象層接口類,Zinx框架的最基本的是服務類接口iserver,定義在ziface模塊中。瀏覽器

znet模塊是zinx框架中網絡相關功能的實現,全部網絡相關模塊都會定義在znet模塊中。bash

1.1 Zinx-V0.1 代碼實現

A) 建立zinx框架

​ 在$GOPATH/src下建立zinx文件夾服務器

B) 建立ziface、znet模塊

​ 在zinx/下 建立ziface、znet文件夾, 使當前的文件路徑以下:網絡

└── zinx
    ├── ziface
    │   └── 
    └── znet
        ├──
C) 在ziface下建立服務模塊抽象層iserver.go

zinx/ziface/iserver.go架構

package ziface

//定義服務器接口
type IServer interface{
    //啓動服務器方法
    Start()
    //中止服務器方法
    Stop()
    //開啓業務服務方法
    Serve()
}
D) 在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
}


//============== 實現 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
}

好了,以上咱們已經完成了Zinx-V0.1的基本雛形了,雖然只是一個基本的回寫客戶端數據(咱們以後會自定義處理客戶端業務方法),那麼接下來咱們就應該測試咱們當前的zinx-V0.1是否可使用了。框架

1.2 Zinx框架單元測試樣例

​ 理論上咱們應該能夠如今導入zinx框架,而後寫一個服務端程序,再寫一個客戶端程序進行測試,可是咱們能夠經過Go的單元Test功能,進行單元測試

​ 建立zinx/znet/server_test.go

package znet

import (
    "fmt"
    "net"
    "testing"
    "time"
)

/*
    模擬客戶端
 */
 func ClientTest() {

     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("hello ZINX"))
         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 模塊的測試函數
func TestServer(t *testing.T) {

    /*
        服務端測試
    */
    //1 建立一個server 句柄 s
    s := NewServer("[zinx V0.1]")

    /*
        客戶端測試
    */
    go ClientTest()

    //2 開啓服務
    s.Serve()
}

​ 在zinx/znet下執行

$ go test

​ 執行結果,以下:

[START] Server listenner at IP: 0.0.0.0, Port 7777, is starting
Client Test ... start
listen tcp4 err listen tcp4 0.0.0.0:7777: bind: address already in use
 server call back : hello ZINX, cnt = 6
 server call back : hello ZINX, cnt = 6
 server call back : hello ZINX, cnt = 6
 server call back : hello ZINX, cnt = 6

​ 說明咱們的zinx框架已經可使用了。

1.3 使用Zinx-V0.1完成應用程序

​ 固然,若是感受go test 好麻煩,那麼咱們能夠徹底基於zinx寫兩個應用程序,Server.go , Client.go

Server.go

package main

import (
    "zinx/znet"
)

//Server 模塊的測試函數
func main() {

    //1 建立一個server 句柄 s
    s := znet.NewServer("[zinx V0.1]")

    //2 開啓服務
    s.Serve()
}

啓動Server.go

go run Server.go

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("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)
    }
}

啓動Client.go進行測試

go run Client.go

2.Zinx-V0.2-簡單的鏈接封裝與業務綁定

​ V0.1版本咱們已經實現了一個基礎的Server框架,如今咱們須要對客戶端連接和不一樣的客戶端連接所處理的不一樣業務再作一層接口封裝,固然咱們先是把架構搭建起來。

​ 如今在ziface下建立一個屬於連接的接口文件iconnection.go,固然他的實現文件咱們放在znet下的connection.go中。

2.1 Zinx-V0.2代碼實現

A) ziface建立iconnection.go

zinx/ziface/iconnection.go

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

​ 該接口的一些基礎方法,代碼註釋已經介紹的很清楚,這裏先簡單說明一個HandFunc這個函數類型,這個是全部conn連接在處理業務的函數接口,第一參數是socket原生連接,第二個參數是客戶端請求的數據,第三個參數是客戶端請求的數據長度。這樣,若是咱們想要指定一個conn的處理業務,只要定義一個HandFunc類型的函數,而後和該連接綁定就能夠了。

B) znet 建立iconnection.go

zinx/znet/connection.go

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()
}
C) 從新更正一下Server.go中 處理conn的鏈接業務

zinx/znet/server.go

package znet

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

//iServer 接口實現,定義一個Server服務類
type Server struct {
    //服務器的名稱
    Name string
    //tcp4 or other
    IPVersion string
    //服務綁定的IP地址
    IP string
    //服務綁定的端口
    Port int
}

//============== 定義當前客戶端連接的handle api ===========
func CallBackToClient(conn *net.TCPConn, data []byte, cnt int) error {
    //回顯業務
    fmt.Println("[Conn Handle] CallBackToClient ... ")
    if _, err := conn.Write(data[:cnt]); err !=nil {
        fmt.Println("write back buf err ", err)
        return errors.New("CallBackToClient error")
    }
    return nil
}

//============== 實現 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, CallBackToClient)
            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 NewServer (name string) ziface.IServer {
    s:= &Server {
        Name :name,
        IPVersion:"tcp4",
        IP:"0.0.0.0",
        Port:7777,
    }

    return s
}

CallBackToClient是咱們給當前客戶端conn對象綁定的handle方法,固然目前是server端強制綁定的回顯業務,咱們以後會豐富框架,讓這個用戶可讓用戶自定義指定handle。

​ 在 start()方法中,咱們主要作了以下的修改:

//3.3 處理該新鏈接請求的 業務 方法, 此時應該有 handler 和 conn是綁定的
            dealConn := NewConntion(conn, cid, CallBackToClient)
            cid ++

            //3.4 啓動當前連接的處理業務
            go dealConn.Start()

好了,如今咱們已經將connection的鏈接和handle綁定了,下面咱們在測試一下Zinx-V0.2的框架是否可使用吧。

2.2 使用Zinx-V0.2完成應用程序

實際上,目前Zinx框架的對外接口並未改變,因此V0.1的測試依然有效。

Server.go

package main

import (
    "zinx/znet"
)

//Server 模塊的測試函數
func main() {

    //1 建立一個server 句柄 s
    s := znet.NewServer("[zinx V0.1]")

    //2 開啓服務
    s.Serve()
}

啓動Server.go

go run Server.go

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("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)
    }
}

啓動Client.go進行測試

go run Client.go

如今咱們已經簡單初始了Zinx的雛形,可是目前離咱們真正的框架還很遠,接下來咱們來改進zinx框架。


關於做者:

做者:Aceld(劉丹冰)
簡書號:IT無崖子

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

原創聲明:未經做者容許請勿轉載,或者轉載請註明出處!
相關文章
相關標籤/搜索