当前位置: 首页 > article >正文

【Zinx】Day1:初始 Zinx 框架

目录

  • 学习目标
  • 初始 Zinx 框架
  • Zinx v0.2 代码实现
    • 准备工作
      • 创建 Zinx 框架
      • 创建 ziface 与 znet 模块
    • 基础的 Server 实现
      • 在 ziface 下创建服务模块抽象层 iserver.go
      • 在 znet 下实现服务模块 server.go
    • 封装 Connection
      • 在 ziface 创建 iconnection.go
      • 在 znet 创建 connection.go
    • 回顾 Server 中的业务处理部分
    • 单元测试

学习目标

Zinx 框架是刘丹冰老师开源的面向长链接领域的轻量企业级框架,典型的长链接应用场景包括通讯以及游戏服务器等,它是一个基于 Golang 的轻量级并发服务器框架。

自今天(2025.3.2)开始,我将以一周作为周期(在 2025.3.9 之前完成这个项目),完整地对 Zinx 开发教程进行学习,最终完成企业级 TCP 服务框架的开发。

初始 Zinx 框架

阅读 Zinx 的学习文档,刘丹冰老师并没有给出最终版本的完整代码,而是面向针对 TCP 并发服务器开发零基础的小白,从零开始以教程的形式来完成文档的撰写的,因此第一天的任务非常的简单,就是实现一个基础的 Server,并以 Server 为基础对 Client 的连接进行简单地封装,并将业务处理与连接绑定。

基础的 Server 是 Zinx 的 v0.1 版本,它构建了一个服务端的 TCP Server,用来监听来自客户端的连接请求,一旦请求成功建立,基础的 Server 会进行最简单的回显业务,用于验证客户端与服务端的连接及收发能力正常。

在撰写我自己对 Zinx 学习的文档的时候,我不会原封不动地按照刘丹冰老师 Zinx 文档当中的顺序进行总结,而是会在初始文档的基础上加以取舍。

现在让我们直接跳过 Zinx v0.1 版本,直接实现其 v0.2 版本。在这一版当中,Server 的能力基本不变,它要做的就是开启监听服务,不断地监听 Client 的连接请求并与之建立 TCP 连接。连接建立之后,针对 Client 发送过来的 Request,Server 相应返回这个 Connection 所绑定的业务的 Response。

Zinx v0.2 代码实现

准备工作

创建 Zinx 框架

刘丹冰老师在文档当中将 zinx 文件夹建立在 $GOPATH/src 目录下,我们有严格按照这一条来执行,而是将 zinx 当作一个项目建立在我自定义的文件夹下。

创建 ziface 与 znet 模块

目录组织形式如下:

└── zinx
    ├── ziface
    │   └── 
    └── znet
        ├──

基础的 Server 实现

在 ziface 下创建服务模块抽象层 iserver.go

根据模块的命名不难发现,ziface 应该是保存一些接口信息的模块。

在此模块(即文件目录)下,我们创建 iserver.go:

package ziface

// 定义服务器接口
type IServer interface {
	Start() // Start 启动服务器方法
	Stop()  // Stop 停止服务器方法
	Serve() // Serve 开启服务器方法
}

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

在 znet 下创建 server.go,server.go 当中将会定义一个名为 Server 的结构,它将要实现 IServer 接口,也就是说 Server 结构的实例就是服务端的具体实现。

在编写代码之前,我们先思考,一个 TCP 服务器的服务端,需要哪些必备的属性?首先它需要 IP 和 Port,这样 Client 才能够根据 IP 地址和端口找到具体的服务器,它可能还需要一个 Name,Name 指定了服务器的名字。在 Zinx 的实现当中,Server 还加入了一个 IPVersion 属性,它的值可以是 "tcp4" 等。

根据上述分析,我们定义一个 Server 结构:

type Server struct {
	Name      string // Name 为服务器的名称
	IPVersion string // IPVersion: IPv4 or other
	IP        string // IP: 服务器绑定的 IP 地址
	Port      int    // Port: 服务器绑定的端口
}

为了确保 Server 实现了 IServer 接口,根据之前对 GeeRPC 框架的学习,我们可以加入下述一行代码:

// 确保 Server 实现了 ziface.IServer 的所有方法
var _ ziface.IServer = (*Server)(nil)

这样做的话,编译器将会帮助我们判断 Server 是否实现了 IServer 接口。

之后我们正式开始 Server 方法的实现。根据 IServer 的接口文档,Server 需要实现 Start、Stop 和 Serve 三种方法。Start 用于开启服务,Stop 用于关闭服务,而启动服务之后,要做的进一步操作可以在 Serve 中实现。

在 Day1 的学习中,我们重点关注 Start 方法的实现,Stop 和 Serve 方法只需要实现最基本的功能。

Zinx v0.2 中 Start、Stop 和 Serve 的实现如下:

// Start 开启 Server 的网络服务
func (s *Server) Start() {
	fmt.Printf("[START] Server listenner at IP: %s, Port %d, is starting\n", s.IP, s.Port)

	// 开启一个 goroutine 去做服务端的 Listener 业务
	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. 监听服务器地址
		listener, 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 的方法, 比如 snowflake
		// cid 是用于标识 TCP 连接的 id
		var cid uint32
		cid = 0

		// 3. 启动 Server 网络连接服务
		for {
			// 3.1 阻塞等待客户端建立连接请求
			conn, err := listener.AcceptTCP()
			if err != nil {
				fmt.Println("Accept err", err)
				continue
			}

			// 3.2 TODO Server.Start() 设置服务器最大连接控制, 如果超过最大连接, 则关闭此新的连接
			
			// 重要 !!! 这一行下面要做的是根据 client 的 request 进行具体的业务处理, 并将 response 返回给 client;
			// 重要 !!! 因此下面的实现是什么无所谓, 我们只需要知道它处理的是 client 和 server 之间的 业务即可.
			
			// 3.3 TODO Server.Start() 处理该新连接请求的业务方法, 此时应该有 handler, 它和 conn 是绑定的
			// 3.3 CallBackToClient 是一个具体的业务 handler, 它实现了客户端发送给服务端的数据回显业务
			// NewConnection 是一个工厂函数, Server 和 Client 建立连接之后, 新建一个连接对象, 并将具体的业务与连接绑定

			dealConn := NewConnection(conn, cid, CallBackToClient)
			cid++

			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() 是否在启动服务的时候, 还需要做其它事情呢? 比如自定义 Logger 或加入鉴权中间件等

	// 阻塞, 否则 main goroutine 退出, listenner 也将会随之退出
	for {
		time.Sleep(10 * time.Second)
	}
}

Start 方法应该还是比较好理解的。首先在启动之后,现在命令行打印服务器启动的信息,并告知当前 Server 监听的 IP 和 端口号。之后,通过 goroutine 开启服务监听。

监听成功之后,Zinx 暂时使用以零作为初值的 cid 作为 connection 的 id 标识号,它应该是全局唯一的(回忆 bluebell 项目,bluebell 项目使用 snowflake 算法为每一个用户生成一个独一无二的标识 uid,可以借鉴 snowflake 来实现 cid)。

需要注意的是,cid = 0 其实在做的也是 Server 的初始化,cid 声明之后,就要开始阻塞地监听并等待客户端建立连接请求了。在 for 开启的永真循环中,有:

conn, err := listener.AcceptTCP()

如果没有请求到来,那么将一直阻塞在这里;请求到来之后,Server 进行处理,之后建立连接并新开启 goroutine 处理业务(理所当然地需要在子 goroutine 中处理业务,以迎接并处理新的连接请求)。

目前实现数据回显业务的 CallBackToClient 方法如下:

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
}

它的作用是,客户端发送数据给服务端之后,服务端再将数据返回给客户端,称之为回显。

至此,我们已经基本完成了 Zinx v0.2 服务端的基本开发,还需要一个工厂函数来创建 Server:

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

	return s
}

封装 Connection

现在我们的 Server 已经可以在指定 IP:Port 对可能的 Client Request 进行监听,并建立与 Client 的连接并处理业务。但是具体怎样处理业务,连接当中还包含哪些具体的信息?需要我们对连接进行进一步地封装。

在 ziface 创建 iconnection.go

我们首先创建一个连接的接口,并指定用于处理业务的函数格式:

package ziface

import "net"

type IConnection interface {
	Start()            // 启动连接
	Stop()             // 停止连接
	GetConnID() uint32 // 获取远程客户端地址信息
}

// HandFunc 定义了一个统一处理连接业务的接口
type HandFunc func(*net.TCPConn, []byte, int) error

在 znet 创建 connection.go

之后我们在 znet 创建 connection.go,并定义 Connection 结构,它将要实现 IConnection 接口:

type Connecton struct {
	Conn         *net.TCPConn    // 当前连接的 socket TCP 套接字
	ConnID       uint32          // 当前连接的 ID, 也可称为 SessionID, 全局唯一
	isClosed     bool            // 当前连接的开启/关闭状态
	handleAPI    ziface.HandFunc // 该连接的处理方法 api
	ExitBuffChan chan bool       // 告知该连接一经推出/停止的 channel
}

// 确保 Connection 实现 ziface.IConenction 方法
var _ ziface.IConnection = (*Connecton)(nil)

Connection 需要实现 Start、Stop 和 GetConnID 三个方法,我们来逐一实现。

在实现 Start 之前,我们首先实现一个 Connection 的 StartReader 方法,它能够开启 conn 并读取数据:

// StartReader 开启处理 conn 读数据的 goroutine
func (c *Connecton) StartReader() {
	fmt.Println("Reader Goroutine is running")
	defer fmt.Println(c.RemoteAddr().String(), " conn reader exit !")
	defer c.Stop()

	// StartReader 也将会被当作 goroutine 启动
	for {
		// 开启一个 512 bytes 的缓冲区, 用于从连接中读取数据
		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
		}
	}
}

// RemoteAddr 获取远程客户端的地址信息
func (c *Connecton) RemoteAddr() net.Addr {
	return c.Conn.RemoteAddr()
}

之后我们便可以实现 Start 方法:

// Start 实现 IConnection 中的方法, 它启动连接并让当前连接开始工作
func (c *Connecton) Start() {
	// 开启处理该连接读取到客户端数据之后的业务请求
	go c.StartReader()

	// 启动 StartReader 之后, 使用 for、select 和 channel 监听连接状态
	for {
		select {
		case <-c.ExitBuffChan:
			// 得到退出消息则不再阻塞
			return
		}
	}
}

然后我们来实现 Stop 方法,它将会结束当前的连接状态:

// Stop 停止连接, 结束当前连接状态
func (c *Connecton) Stop() {
	// 1. 如果当前连接已经关闭
	if c.isClosed == true {
		return
	}
	c.isClosed = true

	// TODO: Connection Stop() 如果用户注册了该连接的关闭回调业务, 那么应该在此刻显式调用

	// 关闭 socket 连接
	c.Conn.Close()

	// 通知从缓冲队列读数据的业务, 该链接已经关闭
	c.ExitBuffChan <- true

	// 关闭该链接全部管道
	close(c.ExitBuffChan)
}

GetConnID 方法非常简单,返回连接的属性ConnID即可:

// GetConnID 获取当前连接的 ID
func (c *Connecton) GetConnID() uint32 {
	return c.ConnID
}

至此我们便实现了 Connection 对象及其方法,最后我们再加入一个工厂函数:

// NewConnection 创建新的连接
func NewConnection(conn *net.TCPConn, connID uint32, callback_api ziface.HandFunc) *Connecton {
	c := &Connecton{
		Conn:         conn,
		ConnID:       connID,
		isClosed:     false,
		handleAPI:    callback_api,
		ExitBuffChan: make(chan bool, 1),
	}

	return c
}

回顾 Server 中的业务处理部分

// 3. 启动 Server 网络连接服务
for {
	// 3.1 阻塞等待客户端建立连接请求
	conn, err := listener.AcceptTCP()
	if err != nil {
		fmt.Println("Accept err", err)
		continue
	}

	// 3.2 TODO Server.Start() 设置服务器最大连接控制, 如果超过最大连接, 则关闭此新的连接

	// 3.3 TODO Server.Start() 处理该新连接请求的业务方法, 此时应该有 handler, 它和 conn 是绑定的
	// 3.3 处理新连接请求的业务方法, 此时绑定 handler 和 conn

	dealConn := NewConnection(conn, cid, CallBackToClient)
	cid++

	go dealConn.Start()
}

回顾方才我们所实现的 Server 当中的业务处理部分,在 Server 通过 listener 得到 conn 之后,我们对 conn 进行进一步的封装,调用工厂函数 NewConnection 获取 Connection 对象。之后通过 dealConn.Start() 执行业务,它会通过 StartReader 读取来自 Server 的 Response。

需要明确的一点是,我个人认为,Connection 这个对象是 Client 和 Server 之间的桥梁,它是双工的,客户端可以从 Connection 收发数据,服务端也可以从 Connection 收发数据。因此在 Connection 的 StartReader 当中,首先从连接读取数据,读取到的数据是从客户端发送过来的,之后调用业务对应的函数进行回显,业务与连接的绑定是在服务端完成的,因此数据回显服务是服务端提供给客户端的。在此之后,客户端可以继续从 Connection(或是 Go 内置的 Conn 对象)通过Conn.Read 读取数据。

至此我们便完成了 Zinx v2.0,下面实现一个单元测试来验证进行的学习成果。

单元测试

在 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)
	}
}

func TestServer(t *testing.T) {
	s := NewServer("[zinx V0.1]") // 创建一个 server Handler

	go ClientTest() // 启动客户端

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

运行单元测试可以得到数据回显的结果:

=== RUN   TestServer
[START] Server listenner at IP: 0.0.0.0, Port 7777, is starting
Client Test ... start
start Zinx server   [zinx V0.1]  succ, now listenning...
Reader Goroutine is running
[Conn Handle] CallBackToClient...
 server call back: hello ZINX                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      , cnt = 10
[Conn Handle] CallBackToClient...
 server call back: hello ZINX                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      , cnt = 10

需要注意的一点是,Goland Terminal 中显示的是:
在这里插入图片描述
我推测是因为 buffer 的容量是 512,而我们只填充了 10 个字节,如果将 buffer 的容量改为 10 就不会显示占位符了。


http://www.kler.cn/a/569355.html

相关文章:

  • Pandas使用教程 - Pandas 与 Web API 交互
  • 什么是 MGX:MetaGPT
  • C++11特性(笔记一)
  • C++:vector的push_back时间复杂度分析
  • Qt的坐标
  • 手机打电话时如何识别对方按下的DTMF按键的字符-安卓AI电话机器人
  • Java中用Map<String,Object>存储层次结构
  • 力扣1584. 连接所有点的最小费用
  • 使用Docker Compose部署 MySQL8
  • Win32 C++ 电源计划操作
  • Java+Vue+uniapp微信小程序校园自助打印系统(程序+论文+讲解+安装+调试+售后)
  • 阿里管理三板斧课程和管理工具包(视频精讲+工具文档).zip
  • vue3+ts+uniapp+unibest 微信小程序(第二篇)—— 图文详解自定义背景图页面布局、普通页面布局、分页表单页面布局
  • 矩阵的奇异值(SVD)分解和线性变换
  • 11.24 SpringMVC(1)@RequestMapping、@RestController、@RequestParam
  • leetcode:2164. 对奇偶下标分别排序(python3解法)
  • [代码规范]接口设计规范
  • uni.getLocation 微信小程序中获取位置失败原因
  • spring注解开发(Spring整合JUnit+MyBatis)(7)
  • 常见的正则匹配规则