Zinx第二章初识Zinx框架Golang轻量级并发服务器框架

27次阅读

共计 9633 个字符,预计需要花费 25 分钟才能阅读完成。

【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) 玩家下线
(10) 模拟客户端 AI 模块


二、初识 Zinx 框架

​ 这里先看一下 Zinx 最简单的 Server 雏形。

1. Zinx-V0.1- 基础 Server

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

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

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

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

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

正文完
 0