• 并发服务器框架——zinx

并发服务器框架——zinx

2025-04-26 09:51:28 1 阅读

zinx框架

Zinx 是一个用 Go 语言编写的高性能、轻量级的 TCP 服务器框架,它被设计为简单、快速且易于使用。Zinx 提供了一系列的功能,包括但不限于连接管理、数据编解码、业务处理、负载均衡等,适用于构建各种 TCP 网络服务,如游戏服务器、即时通讯服务器等。

下面实现zinx的多个功能包括:路由、全局配置、消息封装、读写分离、消息队列、链接管理等。


utils包下GlobalObj.go

package utils

import (
	"datarace/zinx/ziface"
	"encoding/json"
	"io/ioutil"
)

/*
存储一切有关Zinx框架的全局参数,供其他模块使用
一些参数也可以通过 用户根据 zinx.json来配置
*/
type GlobalObj struct {
	TcpServer        ziface.IServer //当前Zinx的全局Server对象
	Host             string         //当前服务器主机IP
	TcpPort          int            //当前服务器主机监听端口号
	Name             string         //当前服务器名称
	Version          string         //当前Zinx版本号
	MaxPacketSize    uint32         //都需数据包的最大值
	MaxConn          int            //当前服务器主机允许的最大链接个数
	WorkerPoolSize   uint32         //业务工作Worker池的数量
	MaxWorkerTaskLen uint32         //业务工作Worker对应负责的任务队列最大任务存储数量
	ConfFilePath     string
	MaxMsgChanLen    int
}

/*
定义一个全局的对象
*/
var GlobalObject *GlobalObj

// 读取用户的配置文件
func (g *GlobalObj) Reload() {
	data, err := ioutil.ReadFile("zinx.json")
	if err != nil {
		panic(err)
	}
	//将json数据解析到struct中
	//fmt.Printf("json :%s
", data)
	err = json.Unmarshal(data, &GlobalObject)
	if err != nil {
		panic(err)
	}
}
func init() {
	//初始化GlobalObject变量,设置一些默认值
	GlobalObject = &GlobalObj{
		Name:             "ZinxServerApp",
		Version:          "V0.4",
		TcpPort:          7777,
		Host:             "0.0.0.0",
		MaxConn:          12000,
		MaxPacketSize:    4096,
		ConfFilePath:     "conf/zinx.json",
		WorkerPoolSize:   10,
		MaxWorkerTaskLen: 1024,
	}
	//从配置文件中加载一些用户配置的参数
	GlobalObject.Reload()
}

ziface包

package ziface

type IConnManager interface {
	Add(conn IConnection)                   //添加链接
	Remove(conn IConnection)                //删除连接
	Get(connID uint32) (IConnection, error) //利用ConnID获取链接
	Len() int                               //获取当前连接
	ClearConn()                             //删除并停止所有链接
}

package ziface

import "net"

type IConnection interface {
	Start()
	Stop()
	GetConnID() uint32
	GetTCPConnection() *net.TCPConn
	RemoteAddr() net.Addr
	SendMsg(msgId uint32, data []byte) error
	//直接将Message数据发送给远程的TCP客户端(有缓冲)
	SendBuffMsg(msgId uint32, data []byte) error //添加带缓冲发送消息接口
	//设置链接属性
	SetProperty(key string, value interface{})
	//获取链接属性
	GetProperty(key string) (interface{}, error)
	//移除链接属性
	RemoveProperty(key string)
}
type HandFunc func(*net.TCPConn, []byte, int) error

package ziface

type IDataPack interface {
	GetHeadLen() int32
	Pack(msg IMessage) ([]byte, error)
	Unpack([]byte) (IMessage, error)
}
package ziface

type IMessage interface {
	GetDataLen() uint32
	GetMsgId() uint32
	GetData() []byte
	SetMsgId(uint32)
	SetData([]byte)
	SetDataLen(uint32)
}

package ziface

type IMsgHandle interface {
	DoMsgHandler(request IRequest)          //马上以非阻塞方式处理消息
	AddRouter(msgId uint32, router IRouter) //为消息添加具体的处理逻辑
	StartWorkerPool()                       //启动worker工作池
	SendMsgToTaskQueue(request IRequest)    //将消息交给TaskQueue,由worker进行处理
}

package ziface

type IRequest interface {
	GetConnection() IConnection
	GetData() []byte
	GetMsgID() uint32
}

package ziface

type IRouter interface {
	PreHandle(req IRequest)
	Handle(req IRequest)
	PostHandle(req IRequest)
}

package ziface

type IServer interface {
	Start()
	Stop()
	Serve()
	AddRouter(msgId uint32, router IRouter)
	//得到链接管理
	GetConnMgr() IConnManager
	//设置该Server的连接创建时Hook函数
	SetOnConnStart(func(IConnection))
	//设置该Server的连接断开时的Hook函数
	SetOnConnStop(func(IConnection))
	//调用连接OnConnStart Hook函数
	CallOnConnStart(conn IConnection)
	//调用连接OnConnStop Hook函数
	CallOnConnStop(conn IConnection)
}

znet包
connection

package znet

import (
	"datarace/zinx/utils"
	"datarace/zinx/ziface"
	"errors"
	"fmt"
	"io"
	"net"
	"sync"
)

type Connection struct {
	//当前Conn属于哪个Server
	TcpServer ziface.IServer //当前conn属于哪个server,在conn初始化的时候添加即可
	//当前连接的socket TCP套接字
	Conn *net.TCPConn
	//当前连接的ID 也可以称作为SessionID,ID全局唯一
	ConnID uint32
	//当前连接的关闭状态
	isClosed bool
	//消息管理MsgId和对应处理方法的消息管理模块
	MsgHandler ziface.IMsgHandle
	//告知该链接已经退出/停止的channel
	ExitBuffChan chan bool
	//无缓冲管道,用于读、写两个goroutine之间的消息通信
	msgChan chan []byte
	//有关冲管道,用于读、写两个goroutine之间的消息通信
	msgBuffChan chan []byte
	//链接属性
	property map[string]interface{}
	//保护链接属性修改的锁
	propertyLock sync.RWMutex
}

// 创建连接的方法
func NewConntion(server ziface.IServer, conn *net.TCPConn, connID uint32, msgHandler ziface.IMsgHandle) *Connection {
	//初始化Conn属性
	c := &Connection{
		TcpServer:    server, //将隶属的server传递进来
		Conn:         conn,
		ConnID:       connID,
		isClosed:     false,
		MsgHandler:   msgHandler,
		ExitBuffChan: make(chan bool, 1),
		msgChan:      make(chan []byte),
		msgBuffChan:  make(chan []byte, utils.GlobalObject.MaxMsgChanLen), //不要忘记初始化
		property:     make(map[string]interface{}),                        //对链接属性map初始化
	}
	//将新创建的Conn添加到链接管理中
	c.TcpServer.GetConnMgr().Add(c) //将当前新创建的连接添加到ConnManager中
	return c
}

// 设置链接属性
func (c *Connection) SetProperty(key string, value interface{}) {
	c.propertyLock.Lock()
	defer c.propertyLock.Unlock()
	c.property[key] = value
}

// 获取链接属性
func (c *Connection) GetProperty(key string) (interface{}, error) {
	c.propertyLock.RLock()
	defer c.propertyLock.RUnlock()
	if value, ok := c.property[key]; ok {
		return value, nil
	} else {
		return nil, errors.New("no property found")
	}
}

// 移除链接属性
func (c *Connection) RemoveProperty(key string) {
	c.propertyLock.Lock()
	defer c.propertyLock.Unlock()
	delete(c.property, key)
}
func (c *Connection) startReader() {
	fmt.Println("[Reader Goroutine is running]")
	defer fmt.Println(c.RemoteAddr().String(), "[conn Reader exit!]")
	defer c.Stop()
	for {
		// 创建拆包解包的对象
		dp := NewDataPack()
		//读取客户端的Msg head
		headData := make([]byte, dp.GetHeadLen())
		if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
			fmt.Println("read msg head error ", err)
			break
		}
		//拆包,得到msgid 和 datalen 放在msg中
		msg, err := dp.Unpack(headData)
		if err != nil {
			fmt.Println("unpack error ", err)
			break
		}
		//根据 dataLen 读取 data,放在msg.Data中
		var data []byte
		if msg.GetDataLen() > 0 {
			data = make([]byte, msg.GetDataLen())
			if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
				fmt.Println("read msg data error ", err)
				continue
			}
		}
		msg.SetData(data)
		//得到当前客户端请求的Request数据
		req := Request{
			conn: c,
			msg:  msg,
		}
		//从绑定好的消息和对应的处理方法中执行对应的Handle方法
		if utils.GlobalObject.WorkerPoolSize > 0 {
			//已经启动工作池机制,将消息交给Worker处理
			c.MsgHandler.SendMsgToTaskQueue(&req)
		} else {
			//从绑定好的消息和对应的处理方法中执行对应的Handle方法
			go c.MsgHandler.DoMsgHandler(&req)
		}
	}
}

// 启动连接,让当前连接开始工作
func (c *Connection) Start() {
	//1 开启用户从客户端读取数据流程的Goroutine
	go c.startReader()
	//2 开启用于写回客户端数据流程的Goroutine
	go c.StartWriter()
	//按照用户传递进来的创建连接时需要处理的业务,执行钩子方法
	c.TcpServer.CallOnConnStart(c)
}

//func (c *Connection) Start() {
//	go c.startReader()
//	for {
//		select {
//		case <-c.ExitBuffChan:
//			return
//		}
//	}
//}

// 停止连接,结束当前连接状态M
func (c *Connection) Stop() {
	fmt.Println("Conn Stop()...ConnID = ", c.ConnID)
	//如果当前链接已经关闭
	if c.isClosed == true {
		return
	}
	c.isClosed = true
	//==================
	//如果用户注册了该链接的关闭回调业务,那么在此刻应该显示调用
	c.TcpServer.CallOnConnStop(c)
	//==================
	// 关闭socket链接
	c.Conn.Close()
	//关闭Writer
	c.ExitBuffChan <- true
	//将链接从连接管理器中删除
	c.TcpServer.GetConnMgr().Remove(c)
	//关闭该链接全部管道
	close(c.ExitBuffChan)
	close(c.msgBuffChan)
}

/*
写消息Goroutine, 用户将数据发送给客户端
*/
func (c *Connection) StartWriter() {
	fmt.Println("[Writer Goroutine is running]")
	defer fmt.Println(c.RemoteAddr().String(), "[conn Writer exit!]")
	for {
		select {
		case data := <-c.msgChan:
			//有数据要写给客户端
			if _, err := c.Conn.Write(data); err != nil {
				fmt.Println("Send Data error:, ", err, " Conn Writer exit")
				return
			}
			//针对有缓冲channel需要些的数据处理
		case data, ok := <-c.msgBuffChan:
			if ok {
				//有数据要写给客户端
				if _, err := c.Conn.Write(data); err != nil {
					fmt.Println("Send Buff Data error:, ", err, " Conn Writer exit")
					return
				}
			} else {
				break
				fmt.Println("msgBuffChan is Closed")
			}
		case <-c.ExitBuffChan:
			return
		}
	}
}

// 直接将Message数据发送数据给远程的TCP客户端
func (c *Connection) SendMsg(msgId uint32, data []byte) error {
	if c.isClosed == true {
		return errors.New("Connection closed when send msg")
	}
	//将data封包,并且发送
	dp := NewDataPack()
	msg, err := dp.Pack(NewMsgPackage(msgId, data))
	if err != nil {
		fmt.Println("Pack error msg id = ", msgId)
		return errors.New("Pack error msg ")
	}
	//写回客户端
	c.msgChan <- msg //将之前直接回写给conn.Write的方法 改为 发送给Channel 供Writer读取
	return nil
}

// 从当前连接获取原始的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()
}

// // 直接将Message数据发送数据给远程的TCP客户端
//
//	func (c *Connection) SendMsg(msgId uint32, data []byte) error {
//		if c.isClosed == true {
//			return errors.New("Connection closed when send msg")
//		}
//		//将data封包,并且发送
//		dp := NewDataPack()
//		msg, err := dp.Pack(NewMsgPackage(msgId, data))
//		if err != nil {
//			fmt.Println("Pack error msg id = ", msgId)
//			return errors.New("Pack error msg ")
//		}
//		//写回客户端
//		if _, err := c.Conn.Write(msg); err != nil {
//			fmt.Println("Write msg id ", msgId, " error ")
//			c.ExitBuffChan <- true
//			return errors.New("conn Write error")
//		}
//		return nil
//	}
func (c *Connection) SendBuffMsg(msgId uint32, data []byte) error {
	if c.isClosed == true {
		return errors.New("Connection closed when send buff msg")
	}
	//将data封包,并且发送
	dp := NewDataPack()
	msg, err := dp.Pack(NewMsgPackage(msgId, data))
	if err != nil {
		fmt.Println("Pack error msg id = ", msgId)
		return errors.New("Pack error msg ")
	}
	//写回客户端
	c.msgBuffChan <- msg
	return nil
}

ConnManager

package znet

import (
	"datarace/zinx/ziface"
	"errors"
	"fmt"
	"sync"
)

type ConnManager struct {
	connections map[uint32]ziface.IConnection
	connLock    sync.RWMutex
}

/*
创建一个链接管理
*/
func NewConnManager() *ConnManager {
	return &ConnManager{
		connections: make(map[uint32]ziface.IConnection),
	}
}

// 添加链接
func (connMgr *ConnManager) Add(conn ziface.IConnection) {
	//保护共享资源Map 加写锁
	connMgr.connLock.Lock()
	defer connMgr.connLock.Unlock()
	//将conn连接添加到ConnMananger中
	connMgr.connections[conn.GetConnID()] = conn
	fmt.Println("connection add to ConnManager successfully: conn num = ", connMgr.Len())
}

// 删除连接
func (connMgr *ConnManager) Remove(conn ziface.IConnection) {
	//保护共享资源Map 加写锁
	connMgr.connLock.Lock()
	defer connMgr.connLock.Unlock()
	//删除连接信息
	delete(connMgr.connections, conn.GetConnID())
	fmt.Println("connection Remove ConnID=", conn.GetConnID(), " successfully: conn num = ", connMgr.Len())
}

// 利用ConnID获取链接
func (connMgr *ConnManager) Get(connID uint32) (ziface.IConnection, error) {
	//保护共享资源Map 加读锁
	connMgr.connLock.RLock()
	defer connMgr.connLock.RUnlock()
	if conn, ok := connMgr.connections[connID]; ok {
		return conn, nil
	} else {
		return nil, errors.New("connection not found")
	}
}

// 获取当前连接
func (connMgr *ConnManager) Len() int {
	return len(connMgr.connections)
}

// 清除并停止所有连接
func (connMgr *ConnManager) ClearConn() {
	//保护共享资源Map 加写锁
	connMgr.connLock.Lock()
	defer connMgr.connLock.Unlock()
	//停止并删除全部的连接信息
	for connID, conn := range connMgr.connections {
		//停止
		conn.Stop()
		//删除
		delete(connMgr.connections, connID)
	}
	fmt.Println("Clear All Connections successfully: conn num = ", connMgr.Len())
}

datapack

package znet

import (
	"bytes"
	"datarace/zinx/utils"
	"datarace/zinx/ziface"
	"encoding/binary"
	"errors"
)

type DataPack struct{}

func NewDataPack() *DataPack {
	return &DataPack{}
}
func (dp *DataPack) GetHeadLen() uint32 {
	//Id uint32(4字节) +  DataLen uint32(4字节)
	return 8
}

// 封包方法(压缩数据)
func (dp *DataPack) Pack(msg ziface.IMessage) ([]byte, error) {
	//创建一个存放bytes字节的缓冲
	dataBuff := bytes.NewBuffer([]byte{})
	//写dataLen
	if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetDataLen()); err != nil {
		return nil, err
	}
	//写msgID
	if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetMsgId()); err != nil {
		return nil, err
	}
	//写data数据
	if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetData()); err != nil {
		return nil, err
	}
	return dataBuff.Bytes(), nil
}
func (dp *DataPack) Unpack(binaryData []byte) (ziface.IMessage, error) {
	//创建一个从输入二进制数据的ioReader
	dataBuff := bytes.NewReader(binaryData)
	//只解压head的信息,得到dataLen和msgID
	msg := &Message{}
	//读dataLen
	if err := binary.Read(dataBuff, binary.LittleEndian, &msg.DataLen); err != nil {
		return nil, err
	}
	//读msgID
	if err := binary.Read(dataBuff, binary.LittleEndian, &msg.Id); err != nil {
		return nil, err
	}
	//判断dataLen的长度是否超出我们允许的最大包长度
	if utils.GlobalObject.MaxPacketSize > 0 && msg.DataLen > utils.GlobalObject.MaxPacketSize {
		return nil, errors.New("Too large msg data recieved")
	}
	//这里只需要把head的数据拆包出来就可以了,然后再通过head的长度,再从conn读取一次数据
	return msg, nil
}

message

package znet

type Message struct {
	Id      uint32
	DataLen uint32
	Data    []byte
}

func NewMsgPackage(id uint32, data []byte) *Message {
	return &Message{
		Id:      id,
		DataLen: uint32(len(data)),
		Data:    data,
	}
}

// 获取消息数据段长度
func (msg *Message) GetDataLen() uint32 {
	return msg.DataLen
}

// 获取消息ID
func (msg *Message) GetMsgId() uint32 {
	return msg.Id
}

// 获取消息内容
func (msg *Message) GetData() []byte {
	return msg.Data
}

// 设置消息数据段长度
func (msg *Message) SetDataLen(len uint32) {
	msg.DataLen = len
}

// 设计消息ID
func (msg *Message) SetMsgId(msgId uint32) {
	msg.Id = msgId
}

// 设计消息内容
func (msg *Message) SetData(data []byte) {
	msg.Data = data
}

msgHandler

package znet

import (
	"datarace/zinx/utils"
	"datarace/zinx/ziface"
	"fmt"
	"strconv"
)

type MsgHandle struct {
	Apis           map[uint32]ziface.IRouter
	WorkerPoolSize uint32
	TaskQueue      []chan ziface.IRequest
}

func NewMsgHandle() *MsgHandle {
	return &MsgHandle{
		Apis:           make(map[uint32]ziface.IRouter),
		WorkerPoolSize: utils.GlobalObject.WorkerPoolSize,
		TaskQueue:      make([]chan ziface.IRequest, utils.GlobalObject.WorkerPoolSize),
	}
}

// 马上以非阻塞方式处理消息
func (mh *MsgHandle) DoMsgHandler(request ziface.IRequest) {
	handler, ok := mh.Apis[request.GetMsgID()]
	if !ok {
		fmt.Println("api msgId = ", request.GetMsgID(), " is not FOUND!")
		return
	}
	//执行对应处理方法
	handler.PreHandle(request)
	handler.Handle(request)
	handler.PostHandle(request)
}

// 为消息添加具体的处理逻辑
func (mh *MsgHandle) AddRouter(msgId uint32, router ziface.IRouter) {
	//1 判断当前msg绑定的API处理方法是否已经存在
	if _, ok := mh.Apis[msgId]; ok {
		panic("repeated api , msgId = " + strconv.Itoa(int(msgId)))
	}
	//2 添加msg与api的绑定关系
	mh.Apis[msgId] = router
	fmt.Println("Add api msgId = ", msgId)
}

// 启动一个Worker工作流程
func (mh *MsgHandle) StartOneWorker(workerID int, taskQueue chan ziface.IRequest) {
	fmt.Println("Worker ID = ", workerID, " is started.")
	//不断的等待队列中的消息
	for {
		select {
		//有消息则取出队列的Request,并执行绑定的业务方法
		case request := <-taskQueue:
			mh.DoMsgHandler(request)
		}
	}
}

// 启动worker工作池
func (mh *MsgHandle) StartWorkerPool() {
	//遍历需要启动worker的数量,依此启动
	for i := 0; i < int(mh.WorkerPoolSize); i++ {
		//一个worker被启动
		//给当前worker对应的任务队列开辟空间
		mh.TaskQueue[i] = make(chan ziface.IRequest, utils.GlobalObject.MaxWorkerTaskLen)
		//启动当前Worker,阻塞的等待对应的任务队列是否有消息传递进来
		go mh.StartOneWorker(i, mh.TaskQueue[i])
	}
}

// 将消息交给TaskQueue,由worker进行处理
func (mh *MsgHandle) SendMsgToTaskQueue(request ziface.IRequest) {
	//根据ConnID来分配当前的连接应该由哪个worker负责处理
	//轮询的平均分配法则
	//得到需要处理此条连接的workerID
	workerID := request.GetConnection().GetConnID() % mh.WorkerPoolSize
	fmt.Println("Add ConnID=", request.GetConnection().GetConnID(), " request msgID=", request.GetMsgID(), "to workerID=", workerID)
	//将请求消息发送给任务队列
	mh.TaskQueue[workerID] <- request
}

request

package znet

import (
	"datarace/zinx/ziface"
)

type Request struct {
	conn ziface.IConnection
	msg  ziface.IMessage
}

func (r *Request) GetConnection() ziface.IConnection {
	return r.conn
}

// 获取请求消息的数据
func (r *Request) GetData() []byte {
	return r.msg.GetData()
}

// 获取请求的消息的ID
func (r *Request) GetMsgID() uint32 {
	return r.msg.GetMsgId()
}

router

package znet

import "datarace/zinx/ziface"

type BaseRouter struct{}

func (br *BaseRouter) PreHandle(request ziface.IRequest)  {}
func (br *BaseRouter) Handle(request ziface.IRequest)     {}
func (br *BaseRouter) PostHandle(request ziface.IRequest) {}

server

package znet

import (
	"datarace/zinx/utils"
	"datarace/zinx/ziface"
	"fmt"
	"net"
	"time"
)

// iServer 接口实现,定义一个Server服务类
type Server struct {
	//服务器的名称
	Name string
	//tcp4 or other
	IPVersion string
	//服务绑定的IP地址
	IP string
	//服务绑定的端口
	Port int
	//当前Server的消息管理模块,用来绑定MsgId和对应的处理方法
	msgHandler ziface.IMsgHandle
	//当前Server的链接管理器
	ConnMgr ziface.IConnManager
	//新增两个hook函数原型
	//该Server的连接创建时Hook函数
	OnConnStart func(conn ziface.IConnection)
	//该Server的连接断开时的Hook函数
	OnConnStop func(conn ziface.IConnection)
}

// 得到链接管理
func (s *Server) GetConnMgr() ziface.IConnManager {
	return s.ConnMgr
}

// ============== 实现 ziface.IServer 里的全部接口方法 ========
// 开启网络服务
func (s *Server) Start() {
	fmt.Printf("[START] Server listenner at IP: %s, Port %d, is starting
", s.IP, s.Port)
	fmt.Printf("[START] Server name: %s,listenner at IP: %s, Port %d is starting
", s.Name, s.IP, s.Port)
	fmt.Printf("[Zinx] Version: %s, MaxConn: %d,  MaxPacketSize: %d
",
		utils.GlobalObject.Version,
		utils.GlobalObject.MaxConn,
		utils.GlobalObject.MaxPacketSize)
	//开启一个go去做服务端Linster业务
	go func() {
		//1 获取一个TCP的Addr
		s.msgHandler.StartWorkerPool()
		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...")
		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 设置服务器最大连接控制,如果超过最大连接,那么则关闭此新的连接
			if s.ConnMgr.Len() >= utils.GlobalObject.MaxConn {
				conn.Close()
				continue
			}
			//=============
			//3.3 处理该新连接请求的 业务 方法, 此时应该有 handler 和 conn是绑定的
			dealConn := NewConntion(s, conn, cid, s.msgHandler)
			cid++
			//3.4 启动当前链接的处理业务
			go dealConn.Start()
			//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)
	//将其他需要清理的连接信息或者其他信息 也要一并停止或者清理
	s.ConnMgr.ClearConn()
}
func (s *Server) Serve() {
	s.Start()
	//TODO Server.Serve() 是否在启动服务的时候 还要处理其他的事情呢 可以在这里添加
	//阻塞,否则主Go退出, listenner的go将会退出
	for {
		time.Sleep(10 * time.Second)
	}
}
func (s *Server) AddRouter(msgId uint32, router ziface.IRouter) {
	s.msgHandler.AddRouter(msgId, router)
	fmt.Println("Add Router SUCC!! msgID = ", msgId)
}

/*
创建一个服务器句柄
*/
func NewServer() *Server {
	utils.GlobalObject.Reload()
	s := &Server{
		Name:       utils.GlobalObject.Name,
		IPVersion:  "tcp4",
		IP:         utils.GlobalObject.Host,
		Port:       utils.GlobalObject.TcpPort,
		msgHandler: NewMsgHandle(),   //msgHandler 初始化
		ConnMgr:    NewConnManager(), //创建ConnManage
	}
	return s
}

// 设置该Server的连接创建时Hook函数
func (s *Server) SetOnConnStart(hookFunc func(ziface.IConnection)) {
	s.OnConnStart = hookFunc
}

// 设置该Server的连接断开时的Hook函数
func (s *Server) SetOnConnStop(hookFunc func(ziface.IConnection)) {
	s.OnConnStop = hookFunc
}

// 调用连接OnConnStart Hook函数
func (s *Server) CallOnConnStart(conn ziface.IConnection) {
	if s.OnConnStart != nil {
		fmt.Println("---> CallOnConnStart....")
		s.OnConnStart(conn)
	}
}

// 调用连接OnConnStop Hook函数
func (s *Server) CallOnConnStop(conn ziface.IConnection) {
	if s.OnConnStop != nil {
		fmt.Println("---> CallOnConnStop....")
		s.OnConnStop(conn)
	}
}

客户端

package main

import (
	"datarace/zinx/znet"
	"fmt"
	"io"
	"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 {
		//发封包message消息
		dp := znet.NewDataPack()
		msg, _ := dp.Pack(znet.NewMsgPackage(0, []byte("Zinx V0.8 Client0 Test Message")))
		_, err := conn.Write(msg)
		if err != nil {
			fmt.Println("write error err ", err)
			return
		}
		//先读出流中的head部分
		headData := make([]byte, dp.GetHeadLen())
		_, err = io.ReadFull(conn, headData) //ReadFull 会把msg填充满为止
		if err != nil {
			fmt.Println("read head error")
			break
		}
		//将headData字节流 拆包到msg中
		msgHead, err := dp.Unpack(headData)
		if err != nil {
			fmt.Println("server unpack err:", err)
			return
		}
		if msgHead.GetDataLen() > 0 {
			//msg 是有data数据的,需要再次读取data数据
			msg := msgHead.(*znet.Message)
			msg.Data = make([]byte, msg.GetDataLen())
			//根据dataLen从io中读取字节流
			_, err := io.ReadFull(conn, msg.Data)
			if err != nil {
				fmt.Println("server unpack data err:", err)
				return
			}
			fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
		}
		time.Sleep(1 * time.Second)
	}
}

服务端

package main

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

// ping test 自定义路由
type PingRouter struct {
	znet.BaseRouter
}

// Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
	fmt.Println("Call PingRouter Handle")
	//先读取客户端的数据,再回写ping...ping...ping
	fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
	err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
	if err != nil {
		fmt.Println(err)
	}
}

type HelloZinxRouter struct {
	znet.BaseRouter
}

// HelloZinxRouter Handle
func (this *HelloZinxRouter) Handle(request ziface.IRequest) {
	fmt.Println("Call HelloZinxRouter Handle")
	//先读取客户端的数据,再回写ping...ping...ping
	fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
	err := request.GetConnection().SendBuffMsg(1, []byte("Hello Zinx Router V0.10"))
	if err != nil {
		fmt.Println(err)
	}
}

// 创建连接的时候执行
func DoConnectionBegin(conn ziface.IConnection) {
	fmt.Println("DoConnecionBegin is Called ... ")
	//=============设置两个链接属性,在连接创建之后===========
	fmt.Println("Set conn Name, Home done!")
	conn.SetProperty("Name", "Aceld")
	conn.SetProperty("Home", "https://www.jianshu.com/u/35261429b7f1")
	//===================================================
	err := conn.SendMsg(2, []byte("DoConnection BEGIN..."))
	if err != nil {
		fmt.Println(err)
	}
}

// 连接断开的时候执行
func DoConnectionLost(conn ziface.IConnection) {
	//============在连接销毁之前,查询conn的Name,Home属性=====
	if name, err := conn.GetProperty("Name"); err == nil {
		fmt.Println("Conn Property Name = ", name)
	}
	if home, err := conn.GetProperty("Home"); err == nil {
		fmt.Println("Conn Property Home = ", home)
	}
	//===================================================
	fmt.Println("DoConneciotnLost is Called ... ")
}
func main() {
	//创建一个server句柄
	s := znet.NewServer()
	//注册链接hook回调函数
	s.SetOnConnStart(DoConnectionBegin)
	s.SetOnConnStop(DoConnectionLost)
	//配置路由
	s.AddRouter(0, &PingRouter{})
	s.AddRouter(1, &HelloZinxRouter{})
	//开启服务
	s.Serve()
}


本文地址:https://www.vps345.com/1730.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 ssh deepseek Ollama 模型联网 API CherryStudio javascript 前端 chrome edge 进程 操作系统 进程控制 Ubuntu llama 算法 opencv 自然语言处理 神经网络 语言模型 数据库 centos oracle 关系型 安全 分布式 python MCP macos adb rust http java 网络 开发语言 numpy nginx 监控 自动化运维 阿里云 网络安全 网络协议 harmonyos 华为 typescript 计算机网络 django fastapi flask web3.py ubuntu flutter 笔记 C 环境变量 进程地址空间 react.js 前端面试题 node.js 持续部署 Dell R750XS 科技 ai 人工智能 个人开发 websocket .net 统信 国产操作系统 虚拟机安装 gitlab c++ 多线程服务器 Linux网络编程 pycharm 深度学习 conda pillow spring json html5 firefox android kubernetes 容器 学习方法 经验分享 程序人生 windows 搜索引擎 c语言 github 创意 社区 docker DeepSeek-R1 API接口 Flask FastAPI Waitress Gunicorn uWSGI Uvicorn Hyper-V WinRM TrustedHosts RTSP xop RTP RTSPServer 推流 视频 kvm 无桌面 命令行 YOLOv8 NPU Atlas800 A300I pro asi_bench matlab ecm bpm tcp/ip Deepseek 自动化 蓝耘科技 元生代平台工作流 ComfyUI udp unity golang 后端 IIS .net core Hosting Bundle .NET Framework vs2022 php html 机器学习 chatgpt 大模型 llama3 Chatglm 开源大模型 ollama llm 串口服务器 zotero WebDAV 同步失败 代理模式 YOLO pytorch vue.js audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 sql KingBase 银河麒麟 kylin v10 麒麟 v10 mcp mcp-proxy mcp-inspector fastapi-mcp agent sse spring boot tomcat ESP32 LDAP ide nuxt3 vue3 实时音视频 filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 面试 性能优化 jdk intellij-idea 架构 spring cloud kafka hibernate 运维开发 TRAE vim kylin 编辑器 根服务器 重启 排查 系统重启 日志 原因 计算机视觉 kind maven 游戏程序 医疗APP开发 app开发 uni-app AI编程 AIGC ffmpeg 音视频 AI大模型 大模型入门 大模型教程 webrtc vscode remote-ssh sqlserver bash Linux ukui 麒麟kylinos openeuler 微服务 远程工作 docker compose mysql android studio ruoyi springboot Nuxt.js jenkins 云原生 ci/cd git 嵌入式硬件 单片机 温湿度数据上传到服务器 Arduino HTTP 需求分析 规格说明书 big data express okhttp CORS 跨域 雨云 NPS apache 孤岛惊魂4 恒源云 java-ee tcp 博客 qt microsoft https Linux PID oneapi 大模型微调 open webui 学习 MQTT 消息队列 fpga开发 远程登录 telnet 报错 pdf asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 华为认证 网络工程师 交换机 开源 爬虫 Headless Linux mongodb debian JAVA Java live555 rtsp rtp shell visualstudio k8s c# zookeeper 驱动开发 硬件工程 嵌入式实习 电脑 Samba SWAT 配置文件 服务管理 网络共享 WSL win11 无法解析服务器的名称或地址 大数据 v10 镜像源 软件 armbian u-boot Cline 企业微信 Linux24.04 deepin ecmascript nextjs react reactjs 流式接口 URL AI Agent ftp stm32 eureka web安全 LLM Web APP Streamlit hadoop opensearch helm ssrf 失效的访问控制 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 MI300x DeepSeek openwrt ux 多线程 Python 网络编程 聊天服务器 套接字 TCP 客户端 Socket 小程序 redis svn 向日葵 xrdp 远程桌面 远程连接 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 开发环境 SSL证书 excel odoo 服务器动作 Server action 能力提升 面试宝典 技术 IT信息化 源码剖析 rtsp实现步骤 流媒体开发 腾讯云 rsyslog 智能路由器 prometheus 银河麒麟操作系统 国产化 rpc 远程过程调用 Windows环境 机器人 直播推流 C语言 ipython 物联网 FTP服务器 媒体 微信公众平台 3d 数学建模 网络结构图 服务器繁忙 联想开天P90Z装win10 DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 jmeter 软件测试 av1 电视盒子 机顶盒ROM 魔百盒刷机 gitee ollama下载加速 统信UOS 麒麟 bonding 链路聚合 压力测试 mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 权限 课程设计 cursor MCP server C/S LLM windows日志 安全架构 游戏服务器 Minecraft H3C 命名管道 客户端与服务端通信 其他 agi Cursor ansible playbook gpu算力 Dify es jvm 目标检测 华为云 springsecurity6 oauth2 授权服务器 前后端分离 go 硬件架构 系统架构 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 pygame 小游戏 五子棋 FunASR ASR 佛山戴尔服务器维修 佛山三水服务器维修 交互 file server http server web server ssl 集成学习 集成测试 openEuler jar arm Docker Hub docker pull daemon.json 代码调试 ipdb minio rdp 实验 负载均衡 僵尸进程 UOS 统信操作系统 yum oceanbase rc.local 开机自启 systemd MNN Qwen 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 深度优先 图论 并集查找 换根法 树上倍增 ddos DeepSeek行业应用 Heroku 网站部署 xss 缓存 数据集 ios pppoe radius hugo arm开发 flash-attention gaussdb next.js 部署 部署next.js AI agent 思科模拟器 思科 Cisco googlecloud X11 Xming IDEA Reactor 设计模式 C++ EMQX 通信协议 弹性计算 虚拟化 KVM 计算虚拟化 弹性裸金属 idm 漏洞 宝塔面板 同步 备份 建站 安全威胁分析 vscode 1.86 GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 unity3d 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 SSH 豆瓣 追剧助手 迅雷 nas 微信 内存 postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 virtualenv aws 备选 网站 api 调用 示例 银河麒麟桌面操作系统 Kylin OS elasticsearch IIS服务器 IIS性能 日志监控 intellij idea 监控k8s集群 集群内prometheus 华为od sqlite dubbo TCP服务器 qt项目 qt项目实战 qt教程 openssl 密码学 模拟退火算法 国标28181 视频监控 监控接入 语音广播 流程 SIP SDP mosquitto 外网访问 内网穿透 端口映射 r语言 数据挖掘 数据可视化 数据分析 word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 Docker Compose docker-compose transformer 华为机试 nac 802.1 portal proxy模式 AISphereButler CH340 串口驱动 CH341 uart 485 HAProxy 5G 3GPP 卫星通信 kamailio sip VoIP 大数据平台 虚拟局域网 银河麒麟高级服务器 外接硬盘 Kylin echarts 信息可视化 网页设计 中间件 gradle c 框架搭建 系统安全 显卡驱动 回显服务器 UDP的API使用 vSphere vCenter llama.cpp VMware安装mocOS VMware macOS系统安装 Java Applet URL操作 服务器建立 Socket编程 网络文件读取 mac bcompare Beyond Compare 模拟器 教程 ESXi Dell HPE 联想 浪潮 tcpdump ros rust腐蚀 devops 微信小程序 dify ceph Python基础 Python教程 Python技巧 web GCC Linux环境 升级 CVE-2024-7347 云服务器 VPS 无人机 .net mvc断点续传 gateway 实战案例 序列化反序列化 gcc 数据结构 zabbix iBMC UltraISO Claude 测试工具 autodl 软件定义数据中心 sddc 深度求索 私域 知识库 RTMP 应用层 jupyter 反向代理 AnythingLLM AnythingLLM安装 虚幻 游戏引擎 线程 状态模式 智能手机 矩阵 飞书 wsl web3 程序员 EMUI 回退 降级 环境配置 信息与通信 中兴光猫 换光猫 网络桥接 自己换光猫 传统数据库升级 银行 大语言模型 LLMs springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 职场和发展 单一职责原则 IPMITOOL BMC 硬件管理 ArkUI 多端开发 智慧分发 应用生态 鸿蒙OS opcua opcda KEPServer安装 P2P HDLC 工业4.0 k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm frp IMM QT 5.12.12 QT开发环境 Ubuntu18.04 双系统 GRUB引导 Linux技巧 apt 腾讯云大模型知识引擎 rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos 1024程序员节 微信开放平台 微信公众号配置 gitea 鸿蒙 鸿蒙系统 前端框架 游戏开发 hexo 移动云 可信计算技术 鲲鹏 小智AI服务端 xiaozhi TTS FTP 服务器 RAID RAID技术 磁盘 存储 图像处理 can 线程池 单元测试 功能测试 selenium uniapp 技能大赛 计算机外设 ui SSL 域名 Anolis nginx安装 环境安装 linux插件下载 昇腾 npu mariadb ssh远程登录 自定义客户端 SAS 图形化界面 换源 国内源 Debian 僵尸世界大战 游戏服务器搭建 多进程 远程 命令 执行 sshpass 操作 linux上传下载 健康医疗 互联网医院 nfs wps 安卓 mcu crosstool-ng rabbitmq rnn vmware 卡死 HarmonyOS Next 毕昇JDK wsl2 webstorm Trae IDE AI 原生集成开发环境 Trae AI tensorflow 浏览器开发 AI浏览器 npm linux安装配置 嵌入式 linux驱动开发 Kali Linux 黑客 渗透测试 信息收集 h.264 micropython esp32 mqtt selete 高级IO HarmonyOS 多层架构 解耦 seatunnel RustDesk自建服务器 rustdesk服务器 docker rustdesk yaml Ultralytics 可视化 黑客技术 项目部署到linux服务器 项目部署过程 本地部署 pyqt deekseek ragflow 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 半虚拟化 硬件虚拟化 Hypervisor etl etcd 数据安全 RBAC wireshark EasyConnect vscode1.86 1.86版本 ssh远程连接 田俊楠 SSE rocketmq open Euler dde 迁移指南 网卡的名称修改 eth0 ens33 微信分享 Image wxopensdk .netcore ue4 着色器 ue5 cpp-httplib 网工 postgresql pgpool 开机自启动 rag ragflow 源码启动 grafana 视觉检测 adobe Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 elk bug clickhouse outlook 信号 jina TrinityCore 魔兽世界 cuda cudnn anaconda mamba Vmamba rclone AList webdav fnOS sysctl.conf vm.nr_hugepages dash 正则表达式 ip 小艺 Pura X IMX317 MIPI H265 VCU visual studio code 群晖 文件分享 软件工程 iis Linux的基础指令 W5500 OLED u8g2 chfs ubuntu 16.04 token sas 环境迁移 lio-sam SLAM 服务器管理 配置教程 服务器安装 网站管理 崖山数据库 YashanDB composer 视频编解码 pip 宠物 毕业设计 免费学习 宠物领养 宠物平台 Ubuntu 24.04.1 轻量级服务器 产测工具框架 IMX6ULL 管理框架 matplotlib python3.11 cpu 实时 使用 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 历史版本 下载 安装 NFS 毕设 相差8小时 UTC 时间 远程控制 远程看看 远程协助 HiCar CarLife+ CarPlay QT RK3588 yolov8 Node-Red 编程工具 流编程 知识图谱 网络穿透 CPU safari Mac 系统 VR手套 数据手套 动捕手套 动捕数据手套 dns nvidia 低代码 unix 三级等保 服务器审计日志备份 程序 编程 性能分析 bat OD机试真题 华为OD机试真题 服务器能耗统计 bootstrap 策略模式 单例模式 软考 linux 命令 sed 命令 css 7z 智能音箱 智能家居 输入法 多个客户端访问 IO多路复用 TCP相关API 实时互动 prompt easyui langchain list 模拟实现 tailscale derp derper 中转 主板 电源 网卡 网络攻击模型 线性代数 电商平台 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors XCC Lenovo 自动驾驶 压测 ECS 繁忙 解决办法 替代网站 汇总推荐 AI推理 n8n 工作流 workflow fd 文件描述符 CDN Clion Nova ResharperC++引擎 Centos7 远程开发 dba embedding IPv4 子网掩码 公网IP 私有IP Windows SSH 密钥生成 SSH 公钥 私钥 生成 Qwen2.5-coder 离线部署 Linux find grep wsgiref Web 服务器网关接口 cocoapods xcode threejs 3D skynet SenseVoice DOIT 四博智联 yum源切换 更换国内yum源 防火墙 NAT转发 NAT Server Unity Dedicated Server Host Client 无头主机 stm32项目 LORA NLP SSH 服务 SSH Server OpenSSH Server ardunio BLE 端口测试 合成模型 扩散模型 图像生成 iDRAC R720xd iperf3 带宽测试 常用命令 文本命令 目录命令 ShenTong 计算机 thingsboard 磁盘监控 iot dell服务器 iventoy VmWare OpenEuler css3 DevEco Studio OpenHarmony 真机调试 XFS xfs文件系统损坏 I_O error 服务器主板 AI芯片 Jellyfin sdkman 服务器配置 生物信息学 Wi-Fi 超融合 prometheus数据采集 prometheus数据模型 prometheus特点 Spring Security 我的世界 我的世界联机 数码 相机 我的世界服务器搭建 asm AI-native Docker Desktop 王者荣耀 IPMI 带外管理 软链接 硬链接 db 硬件 设备 GPU PCI-Express WebUI DeepSeek V3 jetty undertow tidb GLIBC 虚拟机 ISO镜像作为本地源 mysql离线安装 ubuntu22.04 mysql8.0 云服务 文件系统 路径解析 云电竞 云电脑 todesk 源码 webgl Erlang OTP gen_server 热代码交换 事务语义 Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 流水线 脚本式流水线 efficientVIT YOLOv8替换主干网络 TOLOv8 centos-root /dev/mapper yum clean all df -h / du -sh 考研 onlyoffice 在线office sqlite3 g++ g++13 cnn DenseNet 基础入门 CrewAI ruby log4j hive Hive环境搭建 hive3环境 Hive远程模式 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 redhat freebsd Xinference RAGFlow glibc rustdesk Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 dns是什么 如何设置电脑dns dns应该如何设置 chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 VMware安装Ubuntu Ubuntu安装k8s 测试用例 AI写作 AI作画 QQ 聊天室 epoll 信号处理 ocr WSL2 IM即时通讯 剪切板对通 HTML FORMAT MySql RAGFLOW 移动魔百盒 cd 目录切换 Radius USB转串口 飞牛NAS 飞牛OS MacBook Pro camera Arduino 电子信息 harmonyOS面试题 GoogLeNet muduo Typore 个人博客 邮件APP 免费软件 KylinV10 麒麟操作系统 Vmware Ubuntu Server Ubuntu 22.04.5 银河麒麟服务器操作系统 系统激活 k8s集群资源管理 云原生开发 算力 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 aarch64 编译安装 HPC 数据库架构 数据管理 数据治理 数据编织 数据虚拟化 烟花代码 烟花 元旦 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 显示管理器 lightdm gdm 树莓派 VNC Open WebUI 阻塞队列 生产者消费者模型 服务器崩坏原因 laravel Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 vue less 直流充电桩 充电桩 deepseek r1 junit xpath定位元素 自动化测试 性能测试 交叉编译 SEO dity make netty p2p make命令 makefile文件 sentinel 边缘计算 Xterminal iphone NAS Termux RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 具身智能 firewall AD域 致远OA OA服务器 服务器磁盘扩容 镜像 游戏机 抗锯齿 Netty 即时通信 NIO 实习 HTTP 服务器控制 ESP32 DeepSeek uv Kali vasp安装 智能硬件 查询数据库服务IP地址 SQL Server 加解密 Yakit yaklang 语音识别 AutoDL navicat HCIE 数通 gpt 技术共享 ROS wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 MS Materials 金仓数据库 2025 征文 数据库平替用金仓 eclipse 业界资讯 MacOS录屏软件 code-server SVN Server tortoise svn SysBench 基准测试 流量运营 强化学习 数据库系统 dock 加速 C# MQTTS 双向认证 emqx 政务 分布式系统 监控运维 Prometheus Grafana docker命令大全 cmos ai小智 语音助手 ai小智配网 ai小智教程 esp32语音助手 diy语音助手 RAG 检索增强生成 文档解析 大模型垂直应用 做raid 装系统 gpt-3 文心一言 Ubuntu共享文件夹 共享目录 Linux共享文件夹 人工智能生成内容 金融 网络用户购物行为分析可视化平台 大数据毕业设计 火绒安全 内网服务器 内网代理 内网通信 EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 eNSP 网络规划 VLAN 企业网络 大模型面经 大模型学习 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 neo4j kali 共享文件夹 嵌入式Linux IPC 拓扑图 剧本 gnu docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 IO VS Code 匿名管道 基础环境 开发 ubuntu20.04 开机黑屏 LInux springcloud wpf 灵办AI VSCode 链表 沙盒 word AD 域管理 spark HistoryServer Spark YARN jobhistory 多路转接 网站搭建 serv00 项目部署 grub 版本升级 扩容 CentOS Stream CentOS edge浏览器 USB网络共享 Playwright 元服务 应用上架 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 软件需求 AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 大大通 第三代半导体 碳化硅 ai工具 java-rocketmq ldap OpenSSH 蓝桥杯 YOLOv12 minecraft trae GIS 遥感 WebGIS ssh漏洞 ssh9.9p2 CVE-2025-23419 内网环境 IO模型 Cookie 分布式训练 Kylin-Server AI代码编辑器 seleium chromedriver 目标跟踪 OpenVINO 推理应用 WebRTC Redis Desktop win服务器架设 windows server 分析解读 ABAP perf 风扇控制软件 DBeaver 数据仓库 kerberos SRS 流媒体 直播 存储维护 NetApp存储 EMC存储 系统开发 binder 车载系统 framework 源码环境 openstack Xen 雨云服务器 TCP协议 MacMini 迷你主机 mini Apple 服务器部署ai模型 Logstash 日志采集 软负载 CLion milvus AI Agent 字节智能运维 swoole curl wget 端口 查看 ss 程序员创富 risc-v firewalld docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 西门子PLC 通讯 ubuntu24.04.1 热榜 ip命令 新增网卡 新增IP 启动网卡 北亚数据恢复 oracle数据恢复 visual studio sonoma 自动更新 xshell termius iterm2 rpa 数据库开发 database triton 模型分析 PX4 fast 大模型应用 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 自动化任务管理 宕机切换 服务器宕机 keepalived ArcTS 登录 ArcUI GridItem arkUI 服务网格 istio js flink 代理 飞牛nas fnos chrome devtools 离线部署dify 企业网络规划 华为eNSP PVE vr 鸿蒙开发 移动开发 mq Unity插件 语法 x64 SIGSEGV xmm0 黑苹果 sequoiaDB TrueLicense Linux的权限 捆绑 链接 谷歌浏览器 youtube google gmail 图形渲染 李心怡 trea idea Google pay Apple pay DNS UDP docker部署Python Invalid Host allowedHosts alias unalias 别名 混合开发 JDK regedit 开机启动 VMware创建虚拟机 键盘 办公自动化 自动化生成 pdf教程 服务器时间 arcgis 影刀 #影刀RPA# 本地化部署 网络爬虫 大模型推理 搭建个人相关服务器 minicom 串口调试工具 音乐服务器 Navidrome 音流 京东云 ping++ 嵌入式系统开发 产品经理 MDK 嵌入式开发工具 论文笔记 sublime text 代理服务器 私有化 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 运维监控 玩机技巧 软件分享 软件图标 增强现实 沉浸式体验 应用场景 技术实现 案例分析 AR pyautogui 架构与原理 bot Docker 虚幻引擎 leetcode 推荐算法 DocFlow figma ubuntu24 vivado24 远程服务 conda配置 conda镜像源 阿里云ECS 论文阅读 自动化编程 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 大模型部署 社交电子 ros2 moveit 机器人运动 Deepseek-R1 私有化部署 推理模型 欧标 OCPP lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 物联网开发 lua vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 域名服务 DHCP 符号链接 配置 docker run 数据卷挂载 交互模式 音乐库 飞牛 实用教程 裸金属服务器 弹性裸金属服务器 本地知识库部署 DeepSeek R1 模型 粘包问题 deep learning searxng midjourney 网络药理学 生信 PPI String Cytoscape CytoHubba iftop 网络流量监控 rime linux环境变量 状态管理的 UDP 服务器 Arduino RTOS 信创 信创终端 中科方德 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 nlp 干货分享 黑客工具 密码爆破 Windsurf Ark-TS语言 聚类 mybatis Attention hosts EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 执法记录仪 智能安全帽 smarteye xml