从0到1开发go-tcp框架【2-实现Message模块、解决TCP粘包问题、实现多路由机制】

从0到1开发go-tcp框架【2-实现Message模块、解决TCP粘包问题、实现多路由机制】

1 实现\封装Message模块

zinx/ziface/imessage.go

package zifacetype IMessage interface {GetMsdId() uint32GetMsgLen() uint32GetMsgData() []byteSetMsgId(uint32)SetData([]byte)SetDataLen(uint32)
}

zinx/znet/message.go

package znettype Message struct {//消息idId uint32//消息长度DataLen uint32//消息内容Data []byte
}func (m *Message) GetMsdId() uint32 {return m.Id
}
func (m *Message) GetMsgLen() uint32 {return m.DataLen
}
func (m *Message) GetMsgData() []byte {return m.Data
}func (m *Message) SetMsgId(id uint32) {m.Id = id
}
func (m *Message) SetData(data []byte) {m.Data = data
}
func (m *Message) SetDataLen(len uint32) {m.DataLen = len
}

2 解决TCP粘包问题(TLV方式)

2.1 解决思路

大家都知道TCP是一种流式传输(所谓流式,也就是没有截止,因此会出现粘包的问题,因为我们不知道读多少数据结束一个包)
解决思路:TLV:type、length、value

  • 每个数据包都封装上TLV,告诉对方我们消息的类型,我们消息的长度(设定为占固定长度,如8字节)。
  • 这样对方在接受的时候,每次先读8字节,拿到类型和长度,最后再根据类型和长度读取对应数量的数据

2.2 封包拆包过程实现

①zinx/ziface/idatapack.go

package zifacetype IDataPack interface {//获取包头的长度GetHeadLen() uint32//封包方法1Pack(msg IMessage) ([]byte, error)//拆包UnPack([]byte) (IMessage, error)
}

②zinx/znet/datapack.go

实现封包,拆包方法

  • 写入数据头
package znetimport ("bytes""encoding/binary""github.com/kataras/iris/v12/x/errors""myTest/zinx/util""myTest/zinx/ziface"
)type DataPack struct {
}func NewDataPack() *DataPack {return &DataPack{}
}//获取包头的长度
func (dp *DataPack) GetHeadLen() uint32 {//DataLen uint32 4字节 + ID uint32 4字节,固定包头的长度return 8
}//封包方法
func (dp *DataPack) Pack(msg ziface.IMessage) ([]byte, error) {//创建一个存放bytes字节的缓冲dataBuf := bytes.NewBuffer([]byte{})//包的格式【包长度、包Id、包数据】//1 先写dataLen写入dataBuf中,采用小端写if err := binary.Write(dataBuf, binary.LittleEndian, msg.GetMsgLen()); err != nil {return nil, err}//2 写入msgIdif err := binary.Write(dataBuf, binary.LittleEndian, msg.GetMsdId()); err != nil {return nil, err}//3 写入具体数据if err := binary.Write(dataBuf, binary.LittleEndian, msg.GetMsgData()); err != nil {return nil, err}return dataBuf.Bytes(), nil
}//拆包:将包的head信息都提取出来(包的id、长度),然后再根据包的长度一次性读取数据
func (dp *DataPack) UnPack(binaryData []byte) (ziface.IMessage, error) {dataBuf := bytes.NewReader(binaryData)//先解压head信息,得到dataLen和msgIdmsg := &Message{}//dataLenif err := binary.Read(dataBuf, binary.LittleEndian, &msg.DataLen); err != nil {return nil, err}//msgIdif err := binary.Read(dataBuf, binary.LittleEndian, &msg.Id); err != nil {return nil, err}//判断dataLen是否已经超过了我们在zinx.json配置文件中所允许的包最大长度if util.GlobalObject.MaxPackageSize > 0 && msg.DataLen > util.GlobalObject.MaxPackageSize {return nil, errors.New("too large msg data receive")}//msg中只包含:dataLen和dataIdreturn msg, nil
}

③测试:zinx/znet/datapack_test.go

在测试的时候可以先把util/globalobj.go中GlobalObject.Reload()注释掉,因为我们是通过go自带的test框架测试,所以会读取不到配置文件

zinx/znet/datapack_test.go
注意:go的test文件名必须是xxxx_test.go

package znetimport ("fmt""io""net""testing"
)//测试dataPack的拆包、封包
func TestDataPack(t *testing.T) {/*1 模拟服务器*/listener, err := net.Listen("tcp", "127.0.0.1:7777")if err != nil {fmt.Println("server listen err ", err)return}//启动协程,用于处理客户端的业务go func() {//2 从客户端读取数据,进行拆包conn, err := listener.Accept()if err != nil {fmt.Println("server accept err ", err)return}go func(conn net.Conn) {//处理客户端的请求//>-----拆包过程------<dp := NewDataPack()for {// ①第一次从conn中读,将包中的head读取出来[我们定义的headLen默认是8字节]headData := make([]byte, dp.GetHeadLen())_, err := io.ReadFull(conn, headData)if err != nil {fmt.Println("read head err ", err)return}//解析headDatamsgHead, err := dp.UnPack(headData)if err != nil {fmt.Println("server unpack err ", err)return}if msgHead.GetMsgLen() > 0 {//msg中是有数据的,需要进行第二次读取//②第二次读取,是根据head中的dataLen来读取data内容msg := msgHead.(*Message)//根据数据包中的数据长度创建对应的切片msg.Data = make([]byte, msg.GetMsgLen())_, err := io.ReadFull(conn, msg.Data)if err != nil {fmt.Println("server unpack err ", err)return}//完整的一个消息已经读取完毕fmt.Println("----->Receive MsgID:", msg.Id, "dataLen=", msg.DataLen, ",/data=", string(msg.Data))}}}(conn)}()/*模拟客户端发送数据包*/conn, err := net.Dial("tcp", "127.0.0.1:7777")if err != nil {fmt.Println("client dial err ", err)return}//创建一个封包对象dp := NewDataPack()//模拟粘包过程,封装两个msg一同发送msg1 := &Message{Id:      1,DataLen: 4,Data:    []byte{'z', 'i', 'n', 'x'},}msg2 := &Message{Id:      2,DataLen: 8,Data:    []byte{'h', 'e', 'l', 'l', 'o', ' ', 'y', 'a'},}//将两个数据包粘在一起[将数据进行打包],打包最后的结果还是一个[]byte切片sendData1, err := dp.Pack(msg1)if err != nil {fmt.Println("Client pack msg1 err ", err)return}sendData2, err := dp.Pack(msg2)if err != nil {fmt.Println("Client pack msg2 err ", err)return}//需要使用sendData2,将数据打散,否则会成为切片中嵌套切片sendData1 = append(sendData1, sendData2...)//一次性将全部数据发送给服务端conn.Write(sendData1)//阻塞,查看控制台打印结果是否正确select {}
}

在这里插入图片描述

2.3 zinx框架集成消息封装机制

将消息封装机制集成到我们自定义的zinx框架中

  • 将zinx/znet/connection.go中的StartReader方法使用封装后的消息实现
    在这里插入图片描述
  • 将zinx/znet/request.go中的data改为IMessage
  • 在zinx/znet/message.go中添加一个NewMessage的方法
  • 在zinx/znet/connection.go中新增SendMsg方法

①zinx/ziface/iconnection.go

package zifaceimport "net"type IConnection interface {//启动连接Start()//停止连接Stop()//获取当前连接的Conn对象GetTCPConnection() *net.TCPConn//获取当前连接模块的idGetConnectionID() uint32//获取远程客户端的TCP状态 IP:PortRemoteAddr() net.Addr//发送数据SendMsg(msgId uint32, data []byte) error
}//定义一个处理连接业务的方法
type HandleFunc func(*net.TCPConn, []byte, int) error

②zinx/znet/connection.go

package znetimport ("fmt""github.com/kataras/iris/v12/x/errors""io""myTest/zinx/ziface""net"
)type Connection struct {Conn     *net.TCPConnConnID   uint32isClosed bool//告知当前的连接已经退出ExitChan chan boolRouter   ziface.IRouter
}func NewConnection(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection {c := &Connection{Conn:     conn,ConnID:   connID,Router:   router,isClosed: false,ExitChan: make(chan bool, 1),}return c
}func (c *Connection) StartReader() {fmt.Println("reader goroutine is running...")defer fmt.Println("connID=", c.ConnID, "Reader is exit, remote addr is ", c.RemoteAddr().String())defer c.Stop()//读取数据for {//buf := make([]byte, util.GlobalObject.MaxPackageSize)//_, err := c.Conn.Read(buf)//if err != nil {//	fmt.Printf("connID %d receive buf err %s\n", c.ConnID, err)//	continue//}//创建一个拆包对象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封装为msgmsg, 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.Conn,msg:  msg,}go func(request ziface.IRequest) {c.Router.PreHandle(request)c.Router.Handler(request)c.Router.PostHandler(request)}(&r)}
}//启动连接
func (c *Connection) Start() {fmt.Printf("ConnID %d is Start...", c.ConnID)go c.StartReader()
}//停止连接
func (c *Connection) Stop() {fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)if c.isClosed {return}c.isClosed = truec.Conn.Close()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
}

2.4 zinx测试集成消息封装机制

注意:之前irequest.go和request.go代码有误,修改为以下即可

  • 修改部分主要为:将GetConnection更换为我们自定义的connection

/zinx/ziface/irequest.go:

package zifacetype IRequest interface {GetConnection() IConnectionGetData() []byteGetMsgID() uint32
}

/zinx/znet/request.go:

package znetimport ("myTest/zinx/ziface"
)type Request struct {conn ziface.IConnectionmsg  ziface.IMessage
}func (r *Request) GetConnection() ziface.IConnection {return r.conn
}func (r *Request) GetData() []byte {return r.msg.GetMsgData()
}func (r *Request) GetMsgID() uint32 {return r.msg.GetMsdId()
}

①client.go

package mainimport ("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 v0.5 client 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和dataLenbinaryHead := 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)}
}

②server.go

package mainimport ("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()))//回写pingerr := request.GetConnection().SendMsg(1, []byte("ping...ping...ping..."))if err != nil {fmt.Println(err)}
}func main() {s := znet.NewServer("[Zinx v5.0]")//添加自定义路由router := &PingRouter{}s.AddRouter(router)s.Serve()
}

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

2.5 消息管理模块(支持多路由)MsgHandler

①zinx/ziface/imsgHandler.go

package zifacetype IMsgHandler interface {DoMsgHandler(request IRequest)AddRouter(msgId uint32, router IRouter)
}

②zinx/znet/msgHandler.go

package znetimport ("fmt""myTest/zinx/ziface""strconv"
)type MsgHandle struct {//msgId与对应的router对应Api map[uint32]ziface.IRouter
}func NewMsgHandle() *MsgHandle {return &MsgHandle{Api: make(map[uint32]ziface.IRouter),}
}func (mh *MsgHandle) DoMsgHandler(request ziface.IRequest) {//判断是否有对应的routerif _, ok := mh.Api[request.GetMsgID()]; !ok {fmt.Println("msgId ", request.GetMsgID(), "does not exist handler, need to add router")return}//call handlerrouter := 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] = routerfmt.Println("msgId ", msgId, "Add router success ")
}

2.6 消息管理模块集成到Zinx框架中[V0.6]

  1. 将server模块中的Router属性替换为MsgHandler
  2. 将server之前的AddRouter修改为调用MsgHandler的AddRouter
  3. 将connection模块中的Router属性修改为MsgHandler
  4. Connection中之前调度Router的业务替换为MsgHandler调度

①zinx/znet/server.go

package znetimport ("fmt""myTest/zinx/util""myTest/zinx/ziface""net"
)type Server struct {Name       stringIPVersion  stringIP         stringPort       intMsgHandler *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)go func() {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}var cid uint32 = 0dealConn := NewConnection(conn, cid, s.MsgHandler)cid++//开启goroutine处理启动当前conngo 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 znetimport ("fmt""github.com/kataras/iris/v12/x/errors""io""net"
)type Connection struct {Conn     *net.TCPConnConnID   uint32isClosed bool//告知当前的连接已经退出ExitChan   chan boolMsgHandler *MsgHandle
}func NewConnection(conn *net.TCPConn, connID uint32, msgHandle *MsgHandle) *Connection {c := &Connection{Conn:       conn,ConnID:     connID,MsgHandler: msgHandle,isClosed:   false,ExitChan:   make(chan bool, 1),}return c
}func (c *Connection) StartReader() {fmt.Println("reader goroutine is running...")defer fmt.Println("connID=", c.ConnID, "Reader is exit, 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封装为msgmsg, 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()
}//停止连接
func (c *Connection) Stop() {fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)if c.isClosed {return}c.isClosed = truec.Conn.Close()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.6/Client0.go

第一个客户端

package mainimport ("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和dataLenbinaryHead := 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)}
}
myDemo/ZinxV0.6/Client1.go
package mainimport ("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(1, []byte("Zinx client1 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和dataLenbinaryHead := 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)}
}
myDemo/ZinxV0.6/Server.go
package mainimport ("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()))//回写pingerr := 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.6]")//添加自定义路由(PingRouter和HelloRouter)router0 := &PingRouter{}s.AddRouter(0, router0)router1 := &HelloRouter{}s.AddRouter(1, router1)s.Serve()
}

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

Zinx正确接受了不同客户端的请求,并根据不同的请求做出了不同的处理

  • 根据msgId和注册handler来对应处理不同请求

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker容器互联详解

目录 docker容器互联详解 一、容器互联概述&#xff1a; 二、案例实验&#xff1a; 1、用户自定义的网络&#xff1a; 2、查看当前的IP信息&#xff1a; 3、启动第三个容器&#xff1a; 4、查看三个容器内部的网络&#xff1a; 5、Ping测试&#xff1a; Ps备注&#x…

wpf画刷学习1

在这2篇博文有提到wpf画刷&#xff0c; https://blog.csdn.net/bcbobo21cn/article/details/109699703 https://blog.csdn.net/bcbobo21cn/article/details/107133703 下面单独学习一下画刷&#xff1b; wpf有五种画刷&#xff0c;也可以自定义画刷&#xff0c;画刷的基类都…

web服务

静态网页与动态网页的区别 在网站设计中&#xff0c;静态网页是网站建设的基础&#xff0c;纯粹 HTML 格式的网页通常被称为“静态网页”&#xff0c;静态网页是标准的 HTML 文件&#xff0c;它的文件扩展名是 .htm、.html&#xff0c;可以包含文本、图像、声音、FLASH 动画、…

使用ffplay播放scrcpy server 视频流

使用ffplay播放scrcpy server 视频流 以windows平台为例 1 下载scrcpy windows平台安装包并解压 下载连接 2 确认版本 .\scrcpy.exe -v3 push server到Android设备 adb push scrcpy-server /data/local/tmp/scrcpy-server-manual.jar4 forward 端口 adb forward tcp:12…

局域网部署,用WorkPlus视频会议保密又安全

用户采用私有化部署视频会议软件的情况主要有以下几种因素&#xff1a; 1. 针对机密性高的会议&#xff1a;如果有涉及高度机密的商业谈判或敏感信息交流等重要会议&#xff0c;政府、军工、企业等用户会选择局域网内部署视频会议软件&#xff0c;以保证信息安全。 2. 频繁进…

iPhone 7透明屏的显示效果怎么样?

iPhone 7是苹果公司于2016年推出的一款智能手机&#xff0c;它采用了4.7英寸的Retina HD显示屏&#xff0c;分辨率为1334x750像素。 虽然iPhone 7的屏幕并不是透明的&#xff0c;但是苹果公司在设计上采用了一些技术&#xff0c;使得用户在使用iPhone 7时可以有一种透明的感觉…

自然语言处理学习笔记(一)————概论

目录 1.自然语言处理概念 2.自然语言与编程语言的比较 &#xff08;1&#xff09;词汇量&#xff1a; &#xff08;2&#xff09;结构化&#xff1a; &#xff08;3&#xff09;歧义性&#xff1a; &#xff08;4&#xff09;容错性&#xff1a; &#xff08;5&#xff0…

Docker 安装 MySQL5.6

方法一、docker pull mysql 查找Docker Hub上的mysql镜像 #docker search mysql 这里我们拉取官方的镜像,标签为5.6 #docker pull mysql:5.6 &#xff08;第一次启动Docker-MySql主要是查看Docker里面MySQL的默认配置&#xff0c;数据位置&#xff0c;日志位置&#xff0c;配…

【C++】开源:Linux端V4L2视频设备库

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍Linux端V4L2视频设备库。 无专精则不能成&#xff0c;无涉猎则不能通。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下…

解决一个Yarn异常:Alerts for Timeline service 2.0 Reader

【背景】 环境是用Ambari搭建的大数据环境&#xff0c;版本是2.7.3&#xff0c;Hdp是3.1.0&#xff1b;我们用这一套组件搭建了好几个环境&#xff0c;都有这个异常告警&#xff0c;但hive、spark都运行正常&#xff0c;可以正常使用&#xff0c;所以也一直没有去费时间解决这…

斯坦福大学提出在类别层级对多零件多关节三维拼装新方法

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 paper&#xff1a;https://arxiv.org/pdf/2303.06163.pdf 背景&#xff1a; 形状装配通过排列一组简单或基本的零件几何图形来组成复杂的形状几何图形。许多重要的任务和应用都依赖于形状装配算法。 计算机…

棱镜七彩正式加入龙蜥社区安全联盟(OASA)

近日&#xff0c;龙蜥社区安全联盟&#xff08;OASA&#xff09;正式成立&#xff0c;棱镜七彩成为该联盟成员单位。 龙蜥社区安全联盟是促进产业合作的非营利组织&#xff0c;致力于打造中立开放、聚焦操作系统信息安全的交流平台&#xff0c;推进龙蜥社区乃至整个产业安全生态…

js实现原型链污染,沙箱绕过

一、沙箱绕过 1.概念 沙箱绕过"是指攻击者利用各种方法和技术来规避或绕过应用程序或系统中的沙箱&#xff08;sandbox&#xff09;。沙箱是一种安全机制&#xff0c;用于隔离和限制应用程序的执行环境&#xff0c;从而防止恶意代码对系统造成损害。它常被用于隔离不受信…

2023 电赛E题--可能会出现的问题以及解决方法

2023年电赛E题报告模板&#xff08;K210版&#xff09;--可直接使用 本文链接&#xff1a;2023年电赛E题报告模板&#xff08;K210版&#xff09;--可直接使用_皓悦编程记的博客-CSDN博客 解决激光笔在黑色区域无法识别 本文链接&#xff1a; 2023 电赛 E 题 激光笔识别有误-…

Stable Diffusion AI绘画学习指南【插件安装设置】

插件安装的方式 可用列表方式安装&#xff0c;点开Extensions 选项卡&#xff0c;找到如下图&#xff0c;找到Available选项卡&#xff0c;点load from加载可用插件&#xff0c;在可用插件列表中找到要装的插件按install 按扭按装&#xff0c;安装完后(Apply and restart UI)应…

ER系列路由器多网段划分设置指南

ER系列路由器多网段划分设置指南 - TP-LINK 服务支持 TP-LINK ER系列路由器支持划分多网段&#xff0c;可以针对不同的LAN接口划分网段&#xff0c;即每一个或多个LAN接口对应一个网段&#xff1b;也可以通过一个LAN接口与支持划分802.1Q VLAN的交换机进行对接&#xff0c;实现…

【八】mybatis 日志模块设计

mybatis 日志模块设计 简介&#xff1a;闲来无事阅读一下mybatis的日志模块设计&#xff0c;学习一下优秀开源框架的设计思路&#xff0c;提升自己的编码能力 模块设计 在Mybatis内部定义了4个级别&#xff1a;Error:错误 、warn:警告、debug:调试、trance&#xff0c;日志优…

windows编译zookeeker动态库供C++链接使用以及遇到的错误处理方法

windows下面C链接zookeeper资料不多&#xff0c;特此记录一下 编译环境VS 2015 一. 相关安装包安装下载 1. zookeeper zookeeper3.6.4 下载zip包解压即可 2. ant apache-ant-1.9.16 将包进行解压D:project\apache-ant-1.9.16&#xff0c;然后配置环境变量 新建 ANT_HOME 系…

Portraiture 4.0.3 for windows/Mac简体中文版(ps人像磨皮滤镜插件)

Imagenomic Portraiture系列插件作为PS磨皮美白必备插件&#xff0c;可以说是最强&#xff0c;今天它更新到了4.0.3版本。但是全网都没有汉化包&#xff0c;经过几个日夜汉化&#xff0c;终于汉化完成可能是全网首个Portraiture 4的汉化包&#xff0c;请大家体验&#xff0c;有…

【Kubernetes】Kubernetes之二进制部署

kubernetes 一、Kubernetes 的安装部署1. 常见的安装部署方式1.1 Minikube1.2 Kubeadm1.3 二进制安装部署2. K8S 部署 二进制与高可用的区别2.1 二进制部署2.2 kubeadm 部署二、Kubernetes 二进制部署过程1. 服务器相关设置以及架构2. 操作系统初始化配置3. 部署 etcd 集群4. 部…