从0到1开发go-tcp框架【3-读写协程分离、引入消息队列、进入连接管理器、引入连接属性】【基础篇完结】

news2024/12/22 23:35:07

从0到1开发go-tcp框架【3-读写协程分离、引入消息队列、进入连接管理器、引入连接属性】

1 读写协程分离[v0.7]

  1. 添加一个Reader和Writer之间通信的channel
  2. 添加一个Writer goroutine
  3. Reader由之前直接发送给客户端改为发送给通信channel
  4. 启动Reader和Writer一起工作

zinx/znet/connection.go

package znet

import (
	"fmt"
	"github.com/kataras/iris/v12/x/errors"
	"io"
	"net"
)

type Connection struct {
	Conn       *net.TCPConn
	ConnID     uint32
	isClosed   bool
	msgChannel chan []byte
	//告知当前的连接已经退出/停止(由Reader告知writer退出)
	ExitChan   chan bool
	MsgHandler *MsgHandle
}

func NewConnection(conn *net.TCPConn, connID uint32, msgHandle *MsgHandle) *Connection {
	c := &Connection{
		Conn:       conn,
		ConnID:     connID,
		MsgHandler: msgHandle,
		isClosed:   false,
		msgChannel: make(chan []byte),
		ExitChan:   make(chan bool, 1),
	}
	return c
}

func (c *Connection) StartWriter() {
	fmt.Println("[Writer Goroutine is running]")
	defer fmt.Println("[conn Writer  goroutine exit!]", c.RemoteAddr().String())
	//不断的阻塞等待channel的消息,然后将channel中的消息写给客户端
	for {
		select {
		case data := <-c.msgChannel:
			//有数据写给客户端
			if _, err := c.Conn.Write(data); err != nil {
				fmt.Println("Send data error , ", err)
				return
			}
		case <-c.ExitChan:
			//代表reader已经退出,此时writer也需要退出
			return
		}
	}
}

func (c *Connection) StartReader() {
	fmt.Println("reader goroutine is running...")
	defer fmt.Println("[Reader goroutine is exit] connID=", c.ConnID, " remote addr is ", c.RemoteAddr().String())
	defer c.Stop()
	//读取数据
	for {
		//创建一个拆包对象
		dp := NewDataPack()
		//读取客户端的msg Head 二进制流 8字节
		headData := make([]byte, dp.GetHeadLen())
		if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
			fmt.Println("read msg head err ", err)
			break
		}
		//拆包,将读取到的headData封装为msg
		msg, err := dp.UnPack(headData)
		if err != nil {
			fmt.Println("unpack msg err ", err)
			break
		}
		//根据dataLen,再次读取Data,放在msg.Data中,
		var data []byte
		//如果数据包中有数据,则读取
		if msg.GetMsgLen() > 0 {
			data = make([]byte, msg.GetMsgLen())
			//将切片data读满
			if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
				fmt.Println("read msg data err ", err)
				break
			}
		}
		msg.SetData(data)

		//封装请求,改为router处理
		r := Request{
			conn: c,
			msg:  msg,
		}
		go c.MsgHandler.DoMsgHandler(&r)
	}
}

//启动连接
func (c *Connection) Start() {
	fmt.Printf("ConnID %d is Start...", c.ConnID)
	//开启读、写
	go c.StartReader()
	go c.StartWriter()
}

//停止连接
func (c *Connection) Stop() {
	fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)
	if c.isClosed {
		return
	}
	c.isClosed = true
	c.Conn.Close()
	c.ExitChan <- true
	close(c.msgChannel)
	close(c.ExitChan)
}

//获取当前连接的Conn对象
func (c *Connection) GetTCPConnection() *net.TCPConn {
	return c.Conn
}

//获取当前连接模块的id
func (c *Connection) GetConnectionID() uint32 {
	return c.ConnID
}

//获取远程客户端的TCP状态 IP:Port
func (c *Connection) RemoteAddr() net.Addr {
	return c.Conn.RemoteAddr()
}

//发送数据
func (c *Connection) SendMsg(msgId uint32, data []byte) error {
	if c.isClosed {
		return errors.New("connection closed\n")
	}
	//将data进行封包
	dp := NewDataPack()
	binaryMsg, err := dp.Pack(NewMessage(msgId, data))
	if err != nil {
		fmt.Println("Pack error msg id=", msgId)
		return errors.New("pack error msg")
	}
	//将数据发送给客户端
	if _, err := c.Conn.Write(binaryMsg); err != nil {
		fmt.Println("write msg id ", msgId, " error ", err)
		return errors.New("conn write err ")
	}
	return nil
}

测试

myDemo/ZinxV0.7/client.go

  • client0.go
package main

import (
	"fmt"
	"io"
	"myTest/zinx/znet"
	"net"
	"time"
)

/*
模拟客户端
*/
func main() {
	fmt.Println("client start...")
	time.Sleep(time.Second * 1)
	//1 创建服务器连接
	conn, err := net.Dial("tcp", "127.0.0.1:8092")
	if err != nil {
		fmt.Println("client start err ", err)
		return
	}
	for {
		//发送封装后的数据包
		dp := znet.NewDataPack()
		binaryMsg, err := dp.Pack(znet.NewMessage(0, []byte("Zinx client0 test msg")))
		if err != nil {
			fmt.Println("client pack msg err ", err)
			return
		}
		if _, err := conn.Write(binaryMsg); err != nil {
			fmt.Println("client write err ", err)
			return
		}
		//服务器应该给我们回复一个message数据,msgId为1,内容为ping...ping...

		//1 先读取流中的head部分,得到Id和dataLen
		binaryHead := make([]byte, dp.GetHeadLen())
		if _, err := io.ReadFull(conn, binaryHead); err != nil {
			fmt.Println("client read head err ", err)
			break
		}
		//将二进制的head拆包到msg中
		msgHead, err := dp.UnPack(binaryHead)
		if err != nil {
			fmt.Println("client unpack msgHead err ", err)
			break
		}
		if msgHead.GetMsgLen() > 0 {
			//2 有数据, 再根据dataLen进行二次读取,将data读出来
			msg := msgHead.(*znet.Message)
			msg.Data = make([]byte, msg.GetMsgLen())
			if _, err := io.ReadFull(conn, msg.Data); err != nil {
				fmt.Println("read msg data error ", err)
				return
			}
			fmt.Println("--------> Receive Server msg , ID=", msg.Id, " ,len=", msg.DataLen, " ,data=", string(msg.Data))
		}

		//cpu阻塞,让出cpu时间片,避免无限for循环导致其他程序无法获取cpu时间片
		time.Sleep(time.Second * 1)
	}
}
  • client1.go

在这里插入图片描述

myDemo/ZinxV0.7/server.go

package main

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

//自定义一个Router,测试路由功能
type PingRouter struct {
	znet.BaseRouter
}

func (pr *PingRouter) Handler(request ziface.IRequest) {
	fmt.Println("call router handler...")
	//先读取客户端数据,再回写ping...ping...ping...
	fmt.Println("receive from client msgId=", request.GetMsgID(),
		"data=", string(request.GetData()))

	//回写ping
	err := request.GetConnection().SendMsg(0, []byte("ping...ping...ping..."))
	if err != nil {
		fmt.Println(err)
	}
}

//定义第二个Router
type HelloRouter struct {
	znet.BaseRouter
}

func (hr *HelloRouter) Handler(request ziface.IRequest) {
	fmt.Println("receive from client msgId=", request.GetMsgID(),
		"data=", string(request.GetData()))
	err := request.GetConnection().SendMsg(1, []byte("hello zinx, I'm the other handler"))
	if err != nil {
		fmt.Println(err)
	}
}

func main() {
	s := znet.NewServer("[Zinx v0.7]")
	//添加自定义路由(PingRouter和HelloRouter)
	router0 := &PingRouter{}
	s.AddRouter(0, router0)
	router1 := &HelloRouter{}
	s.AddRouter(1, router1)
	s.Serve()
}

结果:
在这里插入图片描述

  • 接受多个客户端也可以
    在这里插入图片描述
  • 当client0退出时,不会影响client1
    在这里插入图片描述

2 创建消息队列及多任务[v0.8]

  1. 创建一个消息队列,MsgHandler消息管理模块增加:TaskQueue、WorkerPoolSize
  2. 创还能多任务worker的工作池并且启动
  3. 将之前发送的消息,全部改为把消息发送给消息队列和worker工作池来处理

在这里插入图片描述

实现消息队列机制和工作池机制(集成到自定义框架)

  1. 创建一个消息队列:MsgHandler消息管理模块
  2. 创建多任务worker的工作池并启动
  3. 将之前发送的消息,全部改为把消息发送给消息队列和worker工作池来处理
  4. 将消息队列机制集成到Zinx框架中
  • 开启并调用消息队列及worker工作池
  • 将从客户端处理的消息,发送给当前Worker的工作池来处理

zinx/znet/server.go

package znet

import (
	"fmt"
	"myTest/zinx/util"
	"myTest/zinx/ziface"
	"net"
)

type Server struct {
	Name       string
	IPVersion  string
	IP         string
	Port       int
	MsgHandler *MsgHandle
}

func NewServer(name string) *Server {
	s := &Server{
		Name:       name,
		IPVersion:  "tcp4",
		IP:         util.GlobalObject.Host,
		Port:       util.GlobalObject.TcpPort,
		MsgHandler: NewMsgHandle(),
	}
	return s
}

func (s *Server) Start() {
	//启动服务监听端口
	fmt.Printf("[Zinx] Server Name :%s , listen IP :%v , Port: %d is starting \n", s.Name, s.IP, s.Port)
	fmt.Printf("[Zinx] Version :%s , MaxConn:%v , MaxPackageSize: %d \n", util.GlobalObject.Version, util.GlobalObject.MaxConn, util.GlobalObject.MaxPackageSize)
	var cid uint32 = 0
	go func() {
		//0 开启消息队列及Worker工作池
		s.MsgHandler.StartWorkerPool()

		addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
		if err != nil {
			fmt.Printf("resolve tcp addr error %v\n", err)
			return
		}
		listener, err := net.ListenTCP(s.IPVersion, addr)
		if err != nil {
			fmt.Println("listen ", s.IPVersion, " err ", err)
			return
		}
		fmt.Println("[start] Zinx server success ", s.Name, "Listening...")
		//阻塞连接,处理业务
		for {
			conn, err := listener.AcceptTCP()
			if err != nil {
				fmt.Println("Accept err ", err)
				continue
			}
			dealConn := NewConnection(conn, cid, s.MsgHandler)
			cid++
			//开启goroutine处理启动当前conn
			go dealConn.Start()
		}
	}()
}

func (s *Server) Stop() {

}

func (s *Server) Serve() {
	s.Start()
	//阻塞,一直读取客户端所发送过来的消息
	select {}
}

func (s *Server) AddRouter(msgId uint32, router ziface.IRouter) {
	s.MsgHandler.AddRouter(msgId, router)
}

zinx/znet/connection.go

package znet

import (
	"fmt"
	"github.com/kataras/iris/v12/x/errors"
	"io"
	"myTest/zinx/util"
	"net"
)

type Connection struct {
	Conn       *net.TCPConn
	ConnID     uint32
	isClosed   bool
	msgChannel chan []byte
	//告知当前的连接已经退出/停止(由Reader告知writer退出)
	ExitChan   chan bool
	MsgHandler *MsgHandle
}

func NewConnection(conn *net.TCPConn, connID uint32, msgHandle *MsgHandle) *Connection {
	c := &Connection{
		Conn:       conn,
		ConnID:     connID,
		MsgHandler: msgHandle,
		isClosed:   false,
		msgChannel: make(chan []byte),
		ExitChan:   make(chan bool, 1),
	}
	return c
}

func (c *Connection) StartWriter() {
	fmt.Println("[Writer Goroutine is running]")
	defer fmt.Println("[conn Writer  goroutine exit!]", c.RemoteAddr().String())
	//不断的阻塞等待channel的消息,然后将channel中的消息写给客户端
	for {
		select {
		case data := <-c.msgChannel:
			//有数据写给客户端
			if _, err := c.Conn.Write(data); err != nil {
				fmt.Println("Send data error , ", err)
				return
			}
		case <-c.ExitChan:
			//代表reader已经退出,此时writer也需要退出
			return
		}
	}
}

func (c *Connection) StartReader() {
	fmt.Println("reader goroutine is running...")
	defer fmt.Println("[Reader goroutine is exit] connID=", c.ConnID, " remote addr is ", c.RemoteAddr().String())
	defer c.Stop()
	//读取数据
	for {
		//创建一个拆包对象
		dp := NewDataPack()
		//读取客户端的msg Head 二进制流 8字节
		headData := make([]byte, dp.GetHeadLen())
		if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
			fmt.Println("read msg head err ", err)
			break
		}
		//拆包,将读取到的headData封装为msg
		msg, err := dp.UnPack(headData)
		if err != nil {
			fmt.Println("unpack msg err ", err)
			break
		}
		//根据dataLen,再次读取Data,放在msg.Data中,
		var data []byte
		//如果数据包中有数据,则读取
		if msg.GetMsgLen() > 0 {
			data = make([]byte, msg.GetMsgLen())
			//将切片data读满
			if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
				fmt.Println("read msg data err ", err)
				break
			}
		}
		msg.SetData(data)

		//封装请求,改为router处理
		r := Request{
			conn: c,
			msg:  msg,
		}
		//判断是否开启workerPool,如果没有开启则直接创建协程处理;如果开启则通过workerPool处理
		if util.GlobalObject.WorkerPoolSize > 0 {
			c.MsgHandler.SendMsgToTaskQueue(&r)
		} else {
			go c.MsgHandler.DoMsgHandler(&r)
		}
	}
}

//启动连接
func (c *Connection) Start() {
	fmt.Printf("ConnID %d is Start...", c.ConnID)
	//开启读、写
	go c.StartReader()
	go c.StartWriter()
}

//停止连接
func (c *Connection) Stop() {
	fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)
	if c.isClosed {
		return
	}
	c.isClosed = true
	c.Conn.Close()
	c.ExitChan <- true
	close(c.msgChannel)
	close(c.ExitChan)
}

//获取当前连接的Conn对象
func (c *Connection) GetTCPConnection() *net.TCPConn {
	return c.Conn
}

//获取当前连接模块的id
func (c *Connection) GetConnectionID() uint32 {
	return c.ConnID
}

//获取远程客户端的TCP状态 IP:Port
func (c *Connection) RemoteAddr() net.Addr {
	return c.Conn.RemoteAddr()
}

//发送数据
func (c *Connection) SendMsg(msgId uint32, data []byte) error {
	if c.isClosed {
		return errors.New("connection closed\n")
	}
	//将data进行封包
	dp := NewDataPack()
	binaryMsg, err := dp.Pack(NewMessage(msgId, data))
	if err != nil {
		fmt.Println("Pack error msg id=", msgId)
		return errors.New("pack error msg")
	}
	//将数据发送给客户端
	if _, err := c.Conn.Write(binaryMsg); err != nil {
		fmt.Println("write msg id ", msgId, " error ", err)
		return errors.New("conn write err ")
	}
	return nil
}

zinx/znet/msgHandler.go

package znet

import (
	"fmt"
	"myTest/zinx/util"
	"myTest/zinx/ziface"
	"strconv"
)

type MsgHandle struct {
	//msgId与对应的router对应
	Api map[uint32]ziface.IRouter
	//负责worker取任务的消息队列
	TaskQueue []chan ziface.IRequest
	//业务工作worker池的goroutine数量
	WorkerPoolSize uint32
}

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

func (mh *MsgHandle) DoMsgHandler(request ziface.IRequest) {
	//判断是否有对应的router
	if _, ok := mh.Api[request.GetMsgID()]; !ok {
		fmt.Println("msgId ", request.GetMsgID(), "does not exist handler, need to add router")
		return
	}
	//call handler
	router := mh.Api[request.GetMsgID()]
	router.PreHandle(request)
	router.Handler(request)
	router.PostHandler(request)
}

func (mh *MsgHandle) AddRouter(msgId uint32, router ziface.IRouter) {
	if _, ok := mh.Api[msgId]; ok {
		//如果已经存在了对应的router,则提示
		panic("repeat api, msgId = " + strconv.Itoa(int(msgId)))
	}
	mh.Api[msgId] = router
	fmt.Println("msgId ", msgId, "Add router success ")
}

//启动一个worker工作池(开启工作池的动作只能发生一次,一个zinx框架只能有一个worker工作池)
func (mh *MsgHandle) StartWorkerPool() {
	for i := 0; i < int(mh.WorkerPoolSize); i++ {
		//开辟任务队列
		mh.TaskQueue[i] = make(chan ziface.IRequest, util.GlobalObject.MaxWorkerTaskLen)
		//启动worker
		go mh.startOneWorker(i, mh.TaskQueue[i])
	}
}

func (mh *MsgHandle) startOneWorker(workerId int, taskQueue chan ziface.IRequest) {
	fmt.Println("Worker ID=", workerId, " is started...")
	for {
		select {
		//从任务队列中取消息(如果有消息过来,出列的就是request,然后执行该request所绑定的业务)
		case request := <-taskQueue:
			mh.DoMsgHandler(request)
		}
	}
}

//将消息交给taskQueue,由Worker进行处理
func (mh *MsgHandle) SendMsgToTaskQueue(request ziface.IRequest) {
	//通过取余数的方式来达到负载均衡
	workID := request.GetConnection().GetConnectionID() % util.GlobalObject.WorkerPoolSize
	fmt.Println("Add ConnID=", request.GetConnection().GetConnectionID(),
		" requestID=", request.GetMsgID(),
		" workID=", workID)
	//将消息发送给对应worker的任务队列
	mh.TaskQueue[workID] <- request
}

zinx/ziface/imsgHandler.go

package ziface

type IMsgHandler interface {
	DoMsgHandler(request IRequest)
	AddRouter(msgId uint32, router IRouter)
	StartWorkerPool()
	SendMsgToTaskQueue(request IRequest)
}

测试

myDemo/ZinxV0.8/Server.go

同myDemo/ZinxV0.7/Server.go,修改一下NewServer时候所传的Zinx的名称即可

myDemo/ZinxV0.8/Client.go

同myDemo/ZinxV0.7/Client.go

myDemo/ZinxV0.8/zinx.json

{
  "Name": "Zinx Server Application",
  "Version": "V0.8",
  "Host": "0.0.0.0",
  "TcpPort": 8092,
  "MaxConn": 30,
  "MaxPackageSize": 1024,
  "WorkerPoolSize": 10
}

在这里插入图片描述

在这里插入图片描述

3 连接管理器(connManager)[v0.9]

3.1 连接管理器(conn)的定义与实现

创建一个连接管理模块ConnManager

  • 添加连接
  • 删除连接
  • 根据连接ID查找对应的连接
  • 总连接个数
  • 清理全部的连接

3.2 将连接管理模块集成到Zinx框架中

  1. 给server添加一个ConnMgr属性
  2. 修改NewServer方法,加入ConnMgr初始化
  3. 判断当前连接数是否超出最大值MaxConn
  4. 当server停止的时候(调用server.Stop方法),应该加入ConnMgr.ClearConn()

3.3 提供创建连接/销毁连之前所需的Hook函数

给我们自定义框架Zinx提供创建连接之后/销毁连接之前所要处理的一些业务。提供给用户能够注册的Hook函数

  • 添加OnConnStart()
  • 添加OnConnStop()

zinx/ziface/iserver.go

package ziface

type IServer interface {
	Start()
	Stop()
	Serve()
	AddRouter(msgId uint32, router IRouter)
	GetConnMgr() IConnManager
	//注册创OnConnStart钩子函数
	SetOnConnStart(func(conn IConnection))
	SetOnConnStop(func(conn IConnection))
	//调用OnConnStart钩子函数
	CallOnConnStart(conn IConnection)
	CallOnConnStop(conn IConnection)
}

zinx/ziface/iconnmanager.go

package ziface

type IConnManager interface {
	Add(conn IConnection)
	Remove(conn IConnection)
	Get(connID uint32) (IConnection, error)
	Len() int
	ClearConn()
}

zinx/znet/connmanager.go

package znet

import (
	"fmt"
	"github.com/kataras/iris/v12/x/errors"
	"myTest/zinx/util"
	"myTest/zinx/ziface"
	"sync"
)

type ConnManager struct {
	connections map[uint32]ziface.IConnection //管理的连接集合
	connLock    sync.RWMutex                  //保护连接集合的读写锁
}

func NewConnManager() *ConnManager {
	return &ConnManager{
		connections: make(map[uint32]ziface.IConnection, util.GlobalObject.MaxConn),
	}
}
func (cm *ConnManager) Add(conn ziface.IConnection) {
	//添加写锁
	cm.connLock.Lock()
	defer cm.connLock.Unlock()
	cm.connections[conn.GetConnectionID()] = conn
	fmt.Println("connectionID=", conn.GetConnectionID(), " add to ConnManager success, conn num=", cm.Len())
}

func (cm *ConnManager) Remove(conn ziface.IConnection) {
	//保护共享资源map
	cm.connLock.Lock()
	defer cm.connLock.Unlock()
	delete(cm.connections, conn.GetConnectionID())
	fmt.Println("connectionID=", conn.GetConnectionID(), " remote from ConnManager success, conn num=", cm.Len())
}

func (cm *ConnManager) Get(connID uint32) (ziface.IConnection, error) {
	cm.connLock.RLock()
	defer cm.connLock.RUnlock()
	if conn, ok := cm.connections[connID]; ok {
		return conn, nil
	} else {
		return nil, errors.New("connection NOT FOUND")
	}
}

func (cm *ConnManager) Len() int {
	return len(cm.connections)
}

func (cm *ConnManager) ClearConn() {
	cm.connLock.Lock()
	defer cm.connLock.Unlock()
	for connID, conn := range cm.connections {
		//停止连接
		conn.Stop()
		//删除连接
		delete(cm.connections, connID)
	}
	fmt.Println("Clear All connections success! conn num=", cm.Len())
}

zinx/znet/connection.go

package znet

import (
	"fmt"
	"github.com/kataras/iris/v12/x/errors"
	"io"
	"myTest/zinx/util"
	"myTest/zinx/ziface"
	"net"
)

type Connection struct {
	Conn       *net.TCPConn
	ConnID     uint32
	isClosed   bool
	msgChannel chan []byte
	//告知当前的连接已经退出/停止(由Reader告知writer退出)
	ExitChan   chan bool
	MsgHandler *MsgHandle
	TcpServer  ziface.IServer
}

func NewConnection(server ziface.IServer, conn *net.TCPConn, connID uint32, msgHandle *MsgHandle) *Connection {
	c := &Connection{
		Conn:       conn,
		ConnID:     connID,
		MsgHandler: msgHandle,
		isClosed:   false,
		msgChannel: make(chan []byte),
		ExitChan:   make(chan bool, 1),
		TcpServer:  server,
	}
	//将conn添加到connMgr中
	c.TcpServer.GetConnMgr().Add(c)
	return c
}

func (c *Connection) StartWriter() {
	fmt.Println("[Writer Goroutine is running]")
	defer fmt.Println("[conn Writer  goroutine exit!]", c.RemoteAddr().String())
	//不断的阻塞等待channel的消息,然后将channel中的消息写给客户端
	for {
		select {
		case data := <-c.msgChannel:
			//有数据写给客户端
			if _, err := c.Conn.Write(data); err != nil {
				fmt.Println("Send data error , ", err)
				return
			}
		case <-c.ExitChan:
			//代表reader已经退出,此时writer也需要退出
			return
		}
	}
}

func (c *Connection) StartReader() {
	fmt.Println("reader goroutine is running...")
	defer fmt.Println("[Reader goroutine is exit] connID=", c.ConnID, " remote addr is ", c.RemoteAddr().String())
	defer c.Stop()
	//读取数据
	for {
		//创建一个拆包对象
		dp := NewDataPack()
		//读取客户端的msg Head 二进制流 8字节
		headData := make([]byte, dp.GetHeadLen())
		if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
			fmt.Println("read msg head err ", err)
			break
		}
		//拆包,将读取到的headData封装为msg
		msg, err := dp.UnPack(headData)
		if err != nil {
			fmt.Println("unpack msg err ", err)
			break
		}
		//根据dataLen,再次读取Data,放在msg.Data中,
		var data []byte
		//如果数据包中有数据,则读取
		if msg.GetMsgLen() > 0 {
			data = make([]byte, msg.GetMsgLen())
			//将切片data读满
			if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
				fmt.Println("read msg data err ", err)
				break
			}
		}
		msg.SetData(data)

		//封装请求,改为router处理
		r := Request{
			conn: c,
			msg:  msg,
		}
		//判断是否开启workerPool,如果没有开启则直接创建协程处理;如果开启则通过workerPool处理
		if util.GlobalObject.WorkerPoolSize > 0 {
			c.MsgHandler.SendMsgToTaskQueue(&r)
		} else {
			go c.MsgHandler.DoMsgHandler(&r)
		}
	}
}

//启动连接
func (c *Connection) Start() {
	fmt.Printf("ConnID %d is Start...", c.ConnID)
	//开启读、写
	go c.StartReader()
	go c.StartWriter()
	//执行钩子函数
	c.TcpServer.CallOnConnStart(c)
}

//停止连接
func (c *Connection) Stop() {
	fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)
	if c.isClosed {
		return
	}
	c.isClosed = true
	//连接关闭之前执行hook关闭的钩子函数
	c.TcpServer.CallOnConnStop(c)
	c.Conn.Close()
	c.ExitChan <- true

	//连接conn关闭时,需要从连接管理模块中移除
	c.TcpServer.GetConnMgr().Remove(c)
	close(c.msgChannel)
	close(c.ExitChan)
}

//获取当前连接的Conn对象
func (c *Connection) GetTCPConnection() *net.TCPConn {
	return c.Conn
}

//获取当前连接模块的id
func (c *Connection) GetConnectionID() uint32 {
	return c.ConnID
}

//获取远程客户端的TCP状态 IP:Port
func (c *Connection) RemoteAddr() net.Addr {
	return c.Conn.RemoteAddr()
}

//发送数据
func (c *Connection) SendMsg(msgId uint32, data []byte) error {
	if c.isClosed {
		return errors.New("connection closed\n")
	}
	//将data进行封包
	dp := NewDataPack()
	binaryMsg, err := dp.Pack(NewMessage(msgId, data))
	if err != nil {
		fmt.Println("Pack error msg id=", msgId)
		return errors.New("pack error msg")
	}
	//将数据发送给客户端
	if _, err := c.Conn.Write(binaryMsg); err != nil {
		fmt.Println("write msg id ", msgId, " error ", err)
		return errors.New("conn write err ")
	}
	return nil
}

zinx/znet/server.go

package znet

import (
	"fmt"
	"myTest/zinx/util"
	"myTest/zinx/ziface"
	"net"
)

type Server struct {
	Name       string
	IPVersion  string
	IP         string
	Port       int
	MsgHandler *MsgHandle
	ConnMgr    *ConnManager
	//创建连接之前的Hook函数
	OnConnStart func(conn ziface.IConnection)
	OnConnStop  func(conn ziface.IConnection)
}

func NewServer(name string) *Server {
	s := &Server{
		Name:       name,
		IPVersion:  "tcp4",
		IP:         util.GlobalObject.Host,
		Port:       util.GlobalObject.TcpPort,
		MsgHandler: NewMsgHandle(),
		ConnMgr:    NewConnManager(),
	}
	return s
}

func (s *Server) Start() {
	//启动服务监听端口
	fmt.Printf("[Zinx] Server Name :%s , listen IP :%v , Port: %d is starting \n", s.Name, s.IP, s.Port)
	fmt.Printf("[Zinx] Version :%s , MaxConn:%v , MaxPackageSize: %d \n", util.GlobalObject.Version, util.GlobalObject.MaxConn, util.GlobalObject.MaxPackageSize)
	var cid uint32 = 0
	go func() {
		//0 开启消息队列及Worker工作池
		s.MsgHandler.StartWorkerPool()

		addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
		if err != nil {
			fmt.Printf("resolve tcp addr error %v\n", err)
			return
		}
		listener, err := net.ListenTCP(s.IPVersion, addr)
		if err != nil {
			fmt.Println("listen ", s.IPVersion, " err ", err)
			return
		}
		fmt.Println("[start] Zinx server success ", s.Name, "Listening...")
		//阻塞连接,处理业务
		for {
			conn, err := listener.AcceptTCP()
			if err != nil {
				fmt.Println("Accept err ", err)
				continue
			}
			//判断当前连接数是否超过最大连接数,如果超过则关闭新创建的连接
			if s.ConnMgr.Len() >= util.GlobalObject.MaxConn {
				//TODO 给客户端返回一个超出最大连接的错误包
				fmt.Println("-----------------》 Tcp Conn exceed, conn num=", util.GlobalObject.MaxConn)
				conn.Close()
				//关闭当前连接,等待下一次连接【如果当前连接数小于最大连接数】
				continue
			}
			dealConn := NewConnection(s, conn, cid, s.MsgHandler)
			cid++
			//开启goroutine处理启动当前conn
			go dealConn.Start()
		}
	}()
}

func (s *Server) Stop() {
	//释放相关资源
	fmt.Println("[STOP] Zinx server name ", s.Name)
	s.ConnMgr.ClearConn()
}

func (s *Server) Serve() {
	s.Start()
	//阻塞,一直读取客户端所发送过来的消息
	select {}
}

func (s *Server) GetConnMgr() ziface.IConnManager {
	return s.ConnMgr
}

func (s *Server) AddRouter(msgId uint32, router ziface.IRouter) {
	s.MsgHandler.AddRouter(msgId, router)
}

//注册创OnConnStart钩子函数
func (s *Server) SetOnConnStart(hookFunc func(conn ziface.IConnection)) {
	s.OnConnStart = hookFunc
}

func (s *Server) SetOnConnStop(hookFunc func(conn ziface.IConnection)) {
	s.OnConnStop = hookFunc
}

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

func (s *Server) CallOnConnStop(conn ziface.IConnection) {
	if s.OnConnStop != nil {
		fmt.Println("----------> call OnConnStop()")
		s.OnConnStop(conn)
	}
}

测试

myDemo/ZinxV0.9/Server.go
package main

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

//自定义一个Router,测试路由功能
type PingRouter struct {
	znet.BaseRouter
}

func (pr *PingRouter) Handler(request ziface.IRequest) {
	fmt.Println("call router handler...")
	//先读取客户端数据,再回写ping...ping...ping...
	fmt.Println("receive from client msgId=", request.GetMsgID(),
		"data=", string(request.GetData()))

	//回写ping
	err := request.GetConnection().SendMsg(0, []byte("ping...ping...ping..."))
	if err != nil {
		fmt.Println(err)
	}
}

//定义第二个Router
type HelloRouter struct {
	znet.BaseRouter
}

func (hr *HelloRouter) Handler(request ziface.IRequest) {
	fmt.Println("receive from client msgId=", request.GetMsgID(),
		"data=", string(request.GetData()))
	err := request.GetConnection().SendMsg(1, []byte("hello zinx, I'm the other handler"))
	if err != nil {
		fmt.Println(err)
	}
}

//连接创建成功之后需要执行的逻辑
func DoConnBegin(conn ziface.IConnection) {
	fmt.Println("=====>Do Conn Begin...")
	if err := conn.SendMsg(202, []byte("do connection begin...")); err != nil {
		fmt.Println("err")
	}
}

//连接断开之前要执行的逻辑
func DoConnLost(conn ziface.IConnection) {
	fmt.Println("=====>Do Conn Lost...")
	fmt.Println("connID=", conn.GetConnectionID(), " is Lost....")
}

func main() {
	s := znet.NewServer("[Zinx v0.9]")
	//添加自定义路由(PingRouter和HelloRouter)
	router0 := &PingRouter{}
	s.AddRouter(0, router0)
	router1 := &HelloRouter{}
	s.AddRouter(1, router1)
	//注册hook钩子函数
	s.SetOnConnStart(DoConnBegin)
	s.SetOnConnStop(DoConnLost)
	s.Serve()
}

测试代码中的myDemo/ZinxV0.9/Client.go和myDemo/ZinxV0.8/Client.go一样。

  • 为了方便测试超过最大连接数的报错信息,我们可以修改配置文件
    在这里插入图片描述
//将最大连接数设置为2,然后我们复制Client.go,可以多起几个Client来进行测试
{
  "Name": "Zinx Server Application",
  "Version": "V0.9",
  "Host": "0.0.0.0",
  "TcpPort": 8092,
  "MaxConn": 2,
  "MaxPackageSize": 1024,
  "WorkerPoolSize": 10
}

测试最大连接数与连接管理:
在这里插入图片描述

测试钩子函数:
在这里插入图片描述

4 添加连接属性并测试【v0.10】

通过map[string]interface{}来存储连接的属性值,通过RWLock来保证读写connection属性值安全

  • 设置连接属性
  • 获取连接属性
  • 移除连接属性

zinx/ziface/iconnection.go

package ziface

import "net"

type IConnection interface {
	//启动连接
	Start()
	//停止连接
	Stop()
	//获取当前连接的Conn对象
	GetTCPConnection() *net.TCPConn
	//获取当前连接模块的id
	GetConnectionID() uint32
	//获取远程客户端的TCP状态 IP:Port
	RemoteAddr() net.Addr
	//发送数据
	SendMsg(msgId uint32, data []byte) error
	SetProperty(key string, value interface{})
	GetProperty(key string) (interface{}, error)
	RemoveProperty(key string)
}

//定义一个处理连接业务的方法
type HandleFunc func(*net.TCPConn, []byte, int) error

zinx/znet/connection.go

package znet

import (
	"fmt"
	"github.com/kataras/iris/v12/x/errors"
	"io"
	"myTest/zinx/util"
	"myTest/zinx/ziface"
	"net"
	"sync"
)

type Connection struct {
	Conn       *net.TCPConn
	ConnID     uint32
	isClosed   bool
	msgChannel chan []byte
	//告知当前的连接已经退出/停止(由Reader告知writer退出)
	ExitChan     chan bool
	MsgHandler   *MsgHandle
	TcpServer    ziface.IServer
	property     map[string]interface{}
	propertyLock sync.RWMutex
}

func NewConnection(server ziface.IServer, conn *net.TCPConn, connID uint32, msgHandle *MsgHandle) *Connection {
	c := &Connection{
		Conn:       conn,
		ConnID:     connID,
		MsgHandler: msgHandle,
		isClosed:   false,
		msgChannel: make(chan []byte),
		ExitChan:   make(chan bool, 1),
		TcpServer:  server,
		property: make(map[string]interface{}),
	}
	//将conn添加到connMgr中
	c.TcpServer.GetConnMgr().Add(c)
	return c
}

func (c *Connection) StartWriter() {
	fmt.Println("[Writer Goroutine is running]")
	defer fmt.Println("[conn Writer  goroutine exit!]", c.RemoteAddr().String())
	//不断的阻塞等待channel的消息,然后将channel中的消息写给客户端
	for {
		select {
		case data := <-c.msgChannel:
			//有数据写给客户端
			if _, err := c.Conn.Write(data); err != nil {
				fmt.Println("Send data error , ", err)
				return
			}
		case <-c.ExitChan:
			//代表reader已经退出,此时writer也需要退出
			return
		}
	}
}

func (c *Connection) StartReader() {
	fmt.Println("reader goroutine is running...")
	defer fmt.Println("[Reader goroutine is exit] connID=", c.ConnID, " remote addr is ", c.RemoteAddr().String())
	defer c.Stop()
	//读取数据
	for {
		//创建一个拆包对象
		dp := NewDataPack()
		//读取客户端的msg Head 二进制流 8字节
		headData := make([]byte, dp.GetHeadLen())
		if _, err := io.ReadFull(c.GetTCPConnection(), headData); err != nil {
			fmt.Println("read msg head err ", err)
			break
		}
		//拆包,将读取到的headData封装为msg
		msg, err := dp.UnPack(headData)
		if err != nil {
			fmt.Println("unpack msg err ", err)
			break
		}
		//根据dataLen,再次读取Data,放在msg.Data中,
		var data []byte
		//如果数据包中有数据,则读取
		if msg.GetMsgLen() > 0 {
			data = make([]byte, msg.GetMsgLen())
			//将切片data读满
			if _, err := io.ReadFull(c.GetTCPConnection(), data); err != nil {
				fmt.Println("read msg data err ", err)
				break
			}
		}
		msg.SetData(data)

		//封装请求,改为router处理
		r := Request{
			conn: c,
			msg:  msg,
		}
		//判断是否开启workerPool,如果没有开启则直接创建协程处理;如果开启则通过workerPool处理
		if util.GlobalObject.WorkerPoolSize > 0 {
			c.MsgHandler.SendMsgToTaskQueue(&r)
		} else {
			go c.MsgHandler.DoMsgHandler(&r)
		}
	}
}

//启动连接
func (c *Connection) Start() {
	fmt.Printf("ConnID %d is Start...", c.ConnID)
	//开启读、写
	go c.StartReader()
	go c.StartWriter()
	//执行钩子函数
	c.TcpServer.CallOnConnStart(c)
}

//停止连接
func (c *Connection) Stop() {
	fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)
	if c.isClosed {
		return
	}
	c.isClosed = true
	//连接关闭之前执行hook关闭的钩子函数
	c.TcpServer.CallOnConnStop(c)
	c.Conn.Close()
	c.ExitChan <- true

	//连接conn关闭时,需要从连接管理模块中移除
	c.TcpServer.GetConnMgr().Remove(c)
	close(c.msgChannel)
	close(c.ExitChan)
}

//获取当前连接的Conn对象
func (c *Connection) GetTCPConnection() *net.TCPConn {
	return c.Conn
}

//获取当前连接模块的id
func (c *Connection) GetConnectionID() uint32 {
	return c.ConnID
}

//获取远程客户端的TCP状态 IP:Port
func (c *Connection) RemoteAddr() net.Addr {
	return c.Conn.RemoteAddr()
}

//发送数据
func (c *Connection) SendMsg(msgId uint32, data []byte) error {
	if c.isClosed {
		return errors.New("connection closed\n")
	}
	//将data进行封包
	dp := NewDataPack()
	binaryMsg, err := dp.Pack(NewMessage(msgId, data))
	if err != nil {
		fmt.Println("Pack error msg id=", msgId)
		return errors.New("pack error msg")
	}
	//将数据发送给客户端
	if _, err := c.Conn.Write(binaryMsg); err != nil {
		fmt.Println("write msg id ", msgId, " error ", err)
		return errors.New("conn write err ")
	}
	return nil
}

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

测试

myDemo/ZinxV0.10/Server.go
package main

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

//自定义一个Router,测试路由功能
type PingRouter struct {
	znet.BaseRouter
}

func (pr *PingRouter) Handler(request ziface.IRequest) {
	fmt.Println("call router handler...")
	//先读取客户端数据,再回写ping...ping...ping...
	fmt.Println("receive from client msgId=", request.GetMsgID(),
		"data=", string(request.GetData()))

	//回写ping
	err := request.GetConnection().SendMsg(0, []byte("ping...ping...ping..."))
	if err != nil {
		fmt.Println(err)
	}
}

//定义第二个Router
type HelloRouter struct {
	znet.BaseRouter
}

func (hr *HelloRouter) Handler(request ziface.IRequest) {
	fmt.Println("receive from client msgId=", request.GetMsgID(),
		"data=", string(request.GetData()))
	err := request.GetConnection().SendMsg(1, []byte("hello zinx, I'm the other handler"))
	if err != nil {
		fmt.Println(err)
	}
}

//连接创建成功之后需要执行的逻辑
func DoConnBegin(conn ziface.IConnection) {
	fmt.Println("=====>Do Conn Begin...")
	if err := conn.SendMsg(202, []byte("do connection begin...")); err != nil {
		fmt.Println("err")
	}
	//给conn设置属性
	conn.SetProperty("Name", "ziyi")
	conn.SetProperty("士兵突击", "https://www.bilibili.com/video/BV1Lk4y1N7tC/")
}

//连接断开之前要执行的逻辑
func DoConnLost(conn ziface.IConnection) {
	fmt.Println("=====>Do Conn Lost...")
	fmt.Println("connID=", conn.GetConnectionID(), " is Lost....")
	//读取属性
	property, _ := conn.GetProperty("Name")
	fmt.Println("Get Property Name=", property)
	property, _ = conn.GetProperty("士兵突击")
	fmt.Println("Get Property 士兵突击=", property)
}

func main() {
	s := znet.NewServer("[Zinx v0.10]")
	//添加自定义路由(PingRouter和HelloRouter)
	router0 := &PingRouter{}
	s.AddRouter(0, router0)
	router1 := &HelloRouter{}
	s.AddRouter(1, router1)
	//注册hook钩子函数
	s.SetOnConnStart(DoConnBegin)
	s.SetOnConnStop(DoConnLost)
	s.Serve()
}
myDemo/ZinxV0.10/Client.go
package main

import (
	"fmt"
	"io"
	"myTest/zinx/znet"
	"net"
	"time"
)

/*
模拟客户端
*/
func main() {
	fmt.Println("client start...")
	time.Sleep(time.Second * 1)
	//1 创建服务器连接
	conn, err := net.Dial("tcp", "127.0.0.1:8092")
	if err != nil {
		fmt.Println("client start err ", err)
		return
	}
	for {
		//发送封装后的数据包
		dp := znet.NewDataPack()
		binaryMsg, err := dp.Pack(znet.NewMessage(0, []byte("Zinx client0 test msg")))
		if err != nil {
			fmt.Println("client pack msg err ", err)
			return
		}
		if _, err := conn.Write(binaryMsg); err != nil {
			fmt.Println("client write err ", err)
			return
		}
		//服务器应该给我们回复一个message数据,msgId为1,内容为ping...ping...

		//1 先读取流中的head部分,得到Id和dataLen
		binaryHead := make([]byte, dp.GetHeadLen())
		if _, err := io.ReadFull(conn, binaryHead); err != nil {
			fmt.Println("client read head err ", err)
			break
		}
		//将二进制的head拆包到msg中
		msgHead, err := dp.UnPack(binaryHead)
		if err != nil {
			fmt.Println("client unpack msgHead err ", err)
			break
		}
		if msgHead.GetMsgLen() > 0 {
			//2 有数据, 再根据dataLen进行二次读取,将data读出来
			msg := msgHead.(*znet.Message)
			msg.Data = make([]byte, msg.GetMsgLen())
			if _, err := io.ReadFull(conn, msg.Data); err != nil {
				fmt.Println("read msg data error ", err)
				return
			}
			fmt.Println("--------> Receive Server msg , ID=", msg.Id, " ,len=", msg.DataLen, " ,data=", string(msg.Data))
		}

		//cpu阻塞,让出cpu时间片,避免无限for循环导致其他程序无法获取cpu时间片
		time.Sleep(time.Second * 1)
	}
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/828158.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TPlink DDNS 内网穿透?外网访问设置方法

有很多小伙伴都想知道&#xff1a;TPlink路由器怎么设置DDNS内网穿透&#xff1f;今天&#xff0c;小编就给大家分享一下TPlink DDNS 外网访问设置方法&#xff0c;下面是图文教程&#xff0c;帮助新手快速入门DDNS设置。 本文介绍的是云路由器TP-LINK DDNS的设置方法。TP-LIN…

【算法提高:动态规划】1.6 区间DP

文章目录 前言例题列表1068. 环形石子合并&#xff08;前缀和 区间DP 环形转换成线性⭐&#xff09;如何把环转换成区间&#xff1f;⭐实现代码补充&#xff1a;相关题目——282. 石子合并 320. 能量项链&#xff08;另一种计算价值的石子合并&#xff09;479. 加分二叉树&am…

企业电子招标采购系统源码Spring Boot + Mybatis + Redis + Layui + 前后端分离 构建企业电子招采平台之立项流程图 tbms

&#xfeff; 项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&am…

VBA技术资料MF38:VBA_在Excel中隐藏公式

【分享成果&#xff0c;随喜正能量】佛祖也无能为力的四件事&#xff1a;第一&#xff0c;因果不可改&#xff0c;自因自果&#xff0c;别人是代替不了的&#xff1b;第二&#xff0c;智慧不可赐&#xff0c;任何人要开智慧&#xff0c;离不开自身的磨练&#xff1b;第三&#…

Stable Diffusion - SDXL 1.0 全部样式设计与艺术家风格的配置与提示词

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132072482 来源于 Anna Dittmann 安娜迪特曼&#xff0c;艺术家风格的图像&#xff0c;融合幻想、数字艺术、纹理等样式。 SDXL 是 Stable Diffus…

星戈瑞 | DSPE-PEG-CY3在生物医学研究中的作用

DSPE-PEG-CY3纳米颗粒在生物医学研究中具有多种重要作用&#xff0c;主要包括以下方面&#xff1a; 1. 荧光成像&#xff1a; DSPE-PEG-CY3纳米颗粒具有花菁染料CY3的荧光特性&#xff0c;可以被用作生物标记物&#xff0c;在细胞和生物体内进行荧光成像。这种荧光成像技术可以…

基于fpga_EP4CE6F17C8_秒表计数器

文章目录 前言实验手册一、实验目的二、实验原理1&#xff0e;理论原理2&#xff0e;硬件原理 三、系统架构设计四、模块说明1&#xff0e;模块端口信号列表dig_driver(数码管驱动模块)key(按键消抖模块)top(顶层模块) 2&#xff0e;状态转移图3&#xff0e;时序图五、仿真波形…

iOS数字转为图片

根据数字&#xff0c;转成对应的图片 - (void)viewDidLoad {[super viewDidLoad];[self testNum2String:10086]; }/// 根据数字&#xff0c;显示对应的图片 数字用特定的图片显示 - (void)testNum2String:(NSInteger)num {UIView *numContentView [[UIView alloc] initWithFr…

多分支git合并流程

阅读摘要 推荐一个git合并步骤,开发分支可能会多次提交合并到dev/master主干分支也会显示很多个提交点,这样不方便代码分支管理和回溯发布记录,所以推荐如下方法,不出意外,这也是个新手教程 git 合并步骤 本地开发分支建立格式建议 feature_功能_开始时间(示例 feature_test_…

Java枚举解析:掌握枚举的绝佳指南!

申明&#xff1a;本人于公众号Java筑基期&#xff0c;CSDN先后发当前文章&#xff0c;标明原创&#xff0c;转载二次发文请注明转载公众号&#xff0c;另外请不要再标原创 &#xff0c;注意违规 枚举 在Java中&#xff0c;枚举&#xff08;Enumeration&#xff09;是一种特殊的…

MES管理系统解决方案,助力汽配行业打造数字化工厂

汽配企业的生产与供应链体系必须与整车厂协同&#xff0c;才能确保品质和交期的要求。随着竞争的加剧&#xff0c;车企不断追求更精益化的管理&#xff0c;以应对市场挑战&#xff0c;而高端客户对品质、成本、交期也提出了更高的要求。因此&#xff0c;以合理的价格提供最佳质…

Kubernetes架构和工作流程

目录 一、kubernetes简介 1.k8s的由来 2.为什么用 k8s &#xff1f; 3.k8s主要功能 二、k8s集群架构与组件 1.Master 组件 1.1Kube-apiserver 1.2Kube-controller-manager 1.3Kube-scheduler 2.Node组件 2.1Kubelet 2.2Kube-Proxy 2.3docker 或 rocket 3.配置存储中…

C++中数据的输入输出介绍

C中数据的输入输出介绍 C中数据的输入输出涉及到的文件 <iostream>&#xff1a;这是C标准库中最常用的头文件之一&#xff0c;包含了进行标准输入输出操作的类和对象&#xff0c;如std::cin、std::cout、std::endl等。 <iomanip>&#xff1a;该头文件提供了一些用…

算法与数据结构(五)--树【1】树与二叉树是什么

一.树的定义 树是一个具有层次结构的集合&#xff0c;是由一个有限集和集合上定义的一种层次结构关系构成的。不同于线性表&#xff0c;树并不是线性的&#xff0c;而是有分支的。 树&#xff08;Tree&#xff09;是n&#xff08;n>0&#xff09;个结点的有限集。 若n0&…

改变C++中私有变量成员的值

1、没有引用的情况&#xff1a; #include <iostream> #include <queue> using namespace std; class Person { public:queue<int>que; public:queue<int> getQueue(){return que;}void push(int a){que.push(a);}void pop(){que.pop();} };int main()…

RS232自由转Profinet网关扫码枪连接电脑操作

你是否曾经遇到过这样的问题&#xff1a;如何在不编写复杂代码的情况下&#xff0c;将条形码数据上传到PLC&#xff1f;今天&#xff0c;我们将为你揭示一个简单的解决方案&#xff01; 让我们来看看这个神奇的组合&#xff1a;捷米的JM-RS485/232-PN (rs232转Profient网关)和…

背景图片及精灵图

.picture {width: 48px;height: 48px;background-image: url(../images/精灵图-侧边功能.png); }为一个有宽高的div设置了背景图片&#xff0c;背景图片只作用在div的content区域内&#xff0c;不作用在padding和border上。 知识点&#xff1a; 背景图使用精灵图&#xff08;…

13-5_Qt 5.9 C++开发指南_基于信号量的线程同步_Semaphore

文章目录 1. 信号量的原理2. 双缓冲区数据采集和读取线程类设计3. QThreadDAQ和QThreadShow 的使用4. 源码4.1 可视化UI设计框架4.2 qmythread.h4.3 qmythread.cpp4.4 dialog.h4.5 dialog.cpp 1. 信号量的原理 信号量(Semaphore)是另一种限制对共享资源进行访问的线程同步机制…

2023年8月美团外卖3-18元红包优惠券天天领取活动日历及美团外卖红包领取使用

2023年8月美团外卖3-18元红包天天领取活动日历 根据上图美团外卖红包领取活动时间表以下时间可以天天领取3-18元美团外卖红包优惠券&#xff1a; 1、2023年8月18日 可领取美团外卖18元神券节红包&#xff1b; 2、2023年8月每周六、周日每天可领取12元美团外卖节红包&#xff…

聊聊工程化 Docker 的最新趋势以及最佳实践

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…