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

从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来对应处理不同请求

相关文章:

从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)SetData…...

Boost开发指南-3.6weak_ptr

weak_ptr weak_ptr是为配合shared_ptr而引入的一种智能指针&#xff0c;它更像是shared_ptr的一个助手而不是智能指针&#xff0c;因为它不具有普通指针的行为&#xff0c;没有重载 operator*和->。它的最大作用在于协助shared_ptr工作&#xff0c;像旁观者那样观测资源的使…...

Swift 周报 第三十三期

文章目录 前言新闻和社区App 内购买项目和订阅即将实行价格与税率调整为家庭提供安全的 App 体验 提案正在审查的提案 Swift论坛推荐博文话题讨论关于我们 前言 本期是 Swift 编辑组自主整理周报的第二十四期&#xff0c;每个模块已初步成型。各位读者如果有好的提议&#xff…...

网络空间安全及计算机领域常见英语单词及短语——网络安全(一)

目录 网络空间安全常见英语单词没事儿读着玩儿相关知识扫盲 CSDN的小伙伴们&#xff0c;我快回来咯&#xff01;网络空间安全常见英语单词 Cybersecurity 网络安全Network security 网络安全Information security 信息安全Data protection 数据保护Threat analysis 威胁分析Ri…...

Go基准测试Benchmark

Go语言自带了一个强大的测试框架&#xff0c;其中包括基准测试&#xff08;Benchmark&#xff09;功能&#xff0c;基准测试用于测量和评估一段代码的性能。 我们可以通过在Go的测试文件中编写特殊格式的函数来创建基准测试。测试文件的命名遵守原函数名称_test.go 的格式。 基…...

docker容器的基本操作

一、查看Docker的版本信息 [roothuyang1 ~]# docker version 二、查看docker的详细信息 [roothuyang1 ~]# docker info 三、Docker镜像操作 Docker创建容器前需要本地存在对应的镜像&#xff0c;如果本地加载不到相关镜像&#xff0c;Docker默认就会尝试从镜像仓库https://hu…...

MySQL绿色安装和配置

1、 从地址http://dev.mysql.com/downloads/mysql/中选择windows的版本下载。 2、 mysql各个版本的简介 &#xff08;1&#xff09; MySQL Community Server 社区版本&#xff0c;开源免费&#xff0c;但不提供官方技术支持。 &#xff08;2&#xff09; MySQL Enterprise Ed…...

《cuda c编程权威指南》03 - cuda小功能汇总

1. 计时 1.1 linux #include <sys/time.h>double cpuSecond() {struct timeval tp;gettimeofday(&tp, NULL);return ((double)tp.tv_sec (double)tp.tv_usec*1e-6); }// 调用 double start cpuSecond(); kernel_name << <grid, block >> > (ar…...

Java:Java程序通过执行系统命令调用Python脚本

本文实现功能&#xff1a;Java程序调用Python脚本 Python脚本 import sysdef add(x, y):return x yif __name__ "__main__":print(add(int(sys.argv[1]), int(sys.argv[2])))直接执行 $ python math.py 1 2 3Java程序调用Python脚本 package io.github.mouday.…...

this is incompatible with sql_mode=only_full_group_by

查看配置 select global.sql_mode 在sql命令行中输入select sql_mode 能够看到sql_mode配置,如果有ONLY_FULL_GROUP_BY&#xff0c;则需要修改 在mysql5.7.5后&#xff0c;ONLY_FULL_GROUP_BY是默认选项&#xff0c;所以就会导致group by的问题 set sql_mode‘复制去掉ONLY_F…...

GCC编译选项

当使用GCC编译器时&#xff0c;可以根据不同的需求选择适当的编译选项来控制编译过程和生成的代码的行为。以下是一些常见的GCC编译选项的归纳&#xff1a; 优化选项&#xff1a; -O0: 不进行优化&#xff0c;保留原始的C代码结构。-O1: 启用基本优化级别&#xff0c;进行简单…...

信息安全战线左移!智能网联汽车安全亟需“治未病”

当汽车由典型的工业机械产品逐步发展成为全新的智能移动终端&#xff0c;汽车的安全边界发生了根本性改变&#xff0c;信息安全风险和挑战不断增加。 面对复杂的异构网络、异构系统及车规级特异性要求&#xff0c;智能智能网联汽车信息安全到底要如何防护&#xff0c;已经成为…...

服务器介绍

本文章转载与b战up主谈三国圈&#xff0c;仅用于学习讨论&#xff0c;如有侵权&#xff0c;请联系博主 机架型服务器 堆出同时服务百万人次机组 刀型服务器 服务器炸了 比如用户访问量暴增 超过机组的峰值处理能力&#xff0c;进而导致卡顿或炸服&#xff0c; 适合企业的塔式…...

Java_25_方法引用

方法引用 方法引用&#xff1a; 方法引用是为了进一步简化Lambda表达式的写法。 方法引用的格式&#xff1a;类型或者对象::引用的方法。 关键语法是&#xff1a;“::” 小结&#xff1a;方法引用可以进一步简化Lambda表达式的写法。关键语法是&#xff1a;“::”范例代码&…...

QT基于TCP协议实现数据传输以及波形绘制——安卓APP及Windows程序双版本

文章代码有非常非常之详细的解析&#xff01;&#xff01;&#xff01;诸位可放心食用 这个玩意我做了两个&#xff0c;一个是安卓app&#xff0c;一个是Windows程序。代码并非全部都是由我从无到有实现&#xff0c;只是实现了我想要的功能。多亏了巨人的肩膀&#xff0c;开源…...

mac 中 brctl 怎么用

mac 中 brctl 怎么用 mac 中 brctl 怎么用1.使用 Homebrew 安装 bridge2.安装完成后&#xff0c;你可以使用 bridge 命令来管理网络桥接。 mac 中 brctl 怎么用 在 macOS 中&#xff0c;没有官方提供的 brctl 命令行工具。但是&#xff0c;你可以使用一个名为 bridge 的开源工…...

20.2 HTML 常用标签

1. head头部标签 <head>标签用于定义网页的头部, 其中的内容是给浏览器读取和解析的, 并不在网页中直接显示给用户. <head>标签通常包含以下一些常见的子标签: - <title>: 定义网页的标题, 在浏览器的标题栏或标签页上显示. - <meta>: 用于设置网页的…...

mysql_2.5——【约束】详解

1、查看约束 SHOW CREATE TABLE table_name 2、主键约束(PRIMARY KEY) 主键约束最显著的特征是主键列中的值是不允许重复(唯一)的&#xff0c;通过主键约束可强制表 的实体完整性。当创建或更改表时可通过定义 primary key 约束来创建主键。一个表只 能有一个primary key约束…...

回归预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络多输入单输出回归预测

回归预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络多输入单输出回归预测 目录 回归预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLA…...

opencv顺时针,逆时针旋转视频并保存视频

原视频 代码 import cv2# 打开视频文件 video cv2.VideoCapture(inference/video/lianzhang.mp4)# 获取原视频的宽度和高度 width int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))# 创建视频编写器并设置输出视频参数 fourcc …...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

国防科技大学计算机基础课程笔记02信息编码

1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制&#xff0c;因此这个了16进制的数据既可以翻译成为这个机器码&#xff0c;也可以翻译成为这个国标码&#xff0c;所以这个时候很容易会出现这个歧义的情况&#xff1b; 因此&#xff0c;我们的这个国…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径

目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

【Java学习笔记】Arrays类

Arrays 类 1. 导入包&#xff1a;import java.util.Arrays 2. 常用方法一览表 方法描述Arrays.toString()返回数组的字符串形式Arrays.sort()排序&#xff08;自然排序和定制排序&#xff09;Arrays.binarySearch()通过二分搜索法进行查找&#xff08;前提&#xff1a;数组是…...

Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务

通过akshare库&#xff0c;获取股票数据&#xff0c;并生成TabPFN这个模型 可以识别、处理的格式&#xff0c;写一个完整的预处理示例&#xff0c;并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务&#xff0c;进行预测并输…...

MMaDA: Multimodal Large Diffusion Language Models

CODE &#xff1a; https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA&#xff0c;它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构&#xf…...

Psychopy音频的使用

Psychopy音频的使用 本文主要解决以下问题&#xff1a; 指定音频引擎与设备&#xff1b;播放音频文件 本文所使用的环境&#xff1a; Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

算法笔记2

1.字符串拼接最好用StringBuilder&#xff0c;不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...