package client import ( "errors" "io" "strings" "sync" "time" "github.com/yosssi/gmq/mqtt" "github.com/yosssi/gmq/mqtt/packet" ) // Minimum and maximum Packet Identifiers const ( minPacketID uint16 = 1 maxPacketID uint16 = 65535 ) // Error values var ( ErrAlreadyConnected = errors.New("the Client has already connected to the Server") ErrNotYetConnected = errors.New("the Client has not yet connected to the Server") ErrCONNACKTimeout = errors.New("the CONNACK Packet was not received within a reasonalbe amount of time") ErrPINGRESPTimeout = errors.New("the PINGRESP Packet was not received within a reasonalbe amount of time") ErrPacketIDExhaused = errors.New("Packet Identifiers are exhausted") ErrInvalidPINGRESP = errors.New("invalid PINGRESP Packet") ErrInvalidSUBACK = errors.New("invalid SUBACK Packet") ) // Client represents a Client. type Client struct { // muConn is the Mutex for the Network Connection. muConn sync.RWMutex // conn is the Network Connection. conn *connection // muSess is the Mutex for the Session. muSess sync.RWMutex // sess is the Session. sess *session // wg is the Wait Group for the goroutines // which are launched by the New method. wg sync.WaitGroup // disconnc is the channel which handles the signal // to disconnect the Network Connection. disconnc chan struct{} // disconnEndc is the channel which ends the goroutine // which disconnects the Network Connection. disconnEndc chan struct{} // errorHandler is the error handler. errorHandler ErrorHandler } // Connect establishes a Network Connection to the Server and // sends a CONNECT Packet to the Server. func (cli *Client) Connect(opts *ConnectOptions) error { // Lock for the connection. cli.muConn.Lock() // Unlock. defer cli.muConn.Unlock() // Return an error if the Client has already connected to the Server. if cli.conn != nil { return ErrAlreadyConnected } // Initialize the options. if opts == nil { opts = &ConnectOptions{} } // Establish a Network Connection. conn, err := newConnection(opts.Network, opts.Address, opts.TLSConfig) if err != nil { return err } // Set the Network Connection to the Client. cli.conn = conn // Lock for reading and updating the Session. cli.muSess.Lock() // Create a Session or reuse the current Session. if opts.CleanSession || cli.sess == nil { // Create a Session and set it to the Client. cli.sess = newSession(opts.CleanSession, opts.ClientID) } else { // Reuse the Session and set its Client Identifier to the options. opts.ClientID = cli.sess.clientID } // Unlock. cli.muSess.Unlock() // Send a CONNECT Packet to the Server. err = cli.sendCONNECT(&packet.CONNECTOptions{ ClientID: opts.ClientID, UserName: opts.UserName, Password: opts.Password, CleanSession: opts.CleanSession, KeepAlive: opts.KeepAlive, WillTopic: opts.WillTopic, WillMessage: opts.WillMessage, WillQoS: opts.WillQoS, WillRetain: opts.WillRetain, }) if err != nil { // Close the Network Connection. cli.conn.Close() // Clean the Network Connection and the Session if necessary. cli.clean() return err } // Launch a goroutine which waits for receiving the CONNACK Packet. cli.conn.wg.Add(1) go cli.waitPacket(cli.conn.connack, opts.CONNACKTimeout, ErrCONNACKTimeout) // Launch a goroutine which receives a Packet from the Server. cli.conn.wg.Add(1) go cli.receivePackets() // Launch a goroutine which sends a Packet to the Server. cli.conn.wg.Add(1) go cli.sendPackets(time.Duration(opts.KeepAlive), opts.PINGRESPTimeout) // Resend the unacknowledged PUBLISH and PUBREL Packets to the Server // if the Clean Session is false. if !opts.CleanSession { // Lock for reading and updating the Session. cli.muSess.Lock() // Unlock. defer cli.muSess.Unlock() for id, p := range cli.sess.sendingPackets { // Extract the MQTT Control MQTT Control Packet type. ptype, err := p.Type() if err != nil { return err } switch ptype { case packet.TypePUBLISH: // Set the DUP flag of the PUBLISH Packet to true. p.(*packet.PUBLISH).DUP = true // Resend the PUBLISH Packet to the Server. cli.conn.send <- p case packet.TypePUBREL: // Resend the PUBREL Packet to the Server. cli.conn.send <- p default: // Delete the Packet from the Session. delete(cli.sess.sendingPackets, id) } } } return nil } // Disconnect sends a DISCONNECT Packet to the Server and // closes the Network Connection. func (cli *Client) Disconnect() error { // Lock for the disconnection. cli.muConn.Lock() // Return an error if the Client has not yet connected to the Server. if cli.conn == nil { // Unlock. cli.muConn.Unlock() return ErrNotYetConnected } // Send a DISCONNECT Packet to the Server. // Ignore the error returned by the send method because // we proceed to the subsequent disconnecting processing // even if the send method returns the error. cli.send(packet.NewDISCONNECT()) // Close the Network Connection. if err := cli.conn.Close(); err != nil { // Unlock. cli.muConn.Unlock() return err } // Change the state of the Network Connection to disconnected. cli.conn.disconnected = true // Send the end signal to the goroutine via the channels. select { case cli.conn.sendEnd <- struct{}{}: default: } // Unlock. cli.muConn.Unlock() // Wait until all goroutines end. cli.conn.wg.Wait() // Lock for cleaning the Network Connection. cli.muConn.Lock() // Lock for cleaning the Session. cli.muSess.Lock() // Clean the Network Connection and the Session. cli.clean() // Unlock. cli.muSess.Unlock() // Unlock. cli.muConn.Unlock() return nil } // Publish sends a PUBLISH Packet to the Server. func (cli *Client) Publish(opts *PublishOptions) error { // Lock for reading. cli.muConn.RLock() // Unlock. defer cli.muConn.RUnlock() // Check the Network Connection. if cli.conn == nil { return ErrNotYetConnected } // Initialize the options. if opts == nil { opts = &PublishOptions{} } // Create a PUBLISH Packet. p, err := cli.newPUBLISHPacket(opts) if err != nil { return err } // Send the Packet to the Server. cli.conn.send <- p return nil } // Subscribe sends a SUBSCRIBE Packet to the Server. func (cli *Client) Subscribe(opts *SubscribeOptions) error { // Lock for reading and updating. cli.muConn.Lock() // Unlock. defer cli.muConn.Unlock() // Check the Network Connection. if cli.conn == nil { return ErrNotYetConnected } // Check the existence of the options. if opts == nil || len(opts.SubReqs) == 0 { return packet.ErrInvalidNoSubReq } // Define a Packet Identifier. var packetID uint16 // Define an error. var err error // Lock for updating the Session. cli.muSess.Lock() defer cli.muSess.Unlock() // Generate a Packet Identifer. if packetID, err = cli.generatePacketID(); err != nil { return err } // Create subscription requests for the SUBSCRIBE Packet. var subReqs []*packet.SubReq for _, s := range opts.SubReqs { subReqs = append(subReqs, &packet.SubReq{ TopicFilter: s.TopicFilter, QoS: s.QoS, }) } // Create a SUBSCRIBE Packet. p, err := packet.NewSUBSCRIBE(&packet.SUBSCRIBEOptions{ PacketID: packetID, SubReqs: subReqs, }) if err != nil { return err } // Set the Packet to the Session. cli.sess.sendingPackets[packetID] = p // Set the subscription information to // the Network Connection. for _, s := range opts.SubReqs { cli.conn.unackSubs[string(s.TopicFilter)] = s.Handler } // Send the Packet to the Server. cli.conn.send <- p return nil } // Unsubscribe sends an UNSUBSCRIBE Packet to the Server. func (cli *Client) Unsubscribe(opts *UnsubscribeOptions) error { // Lock for reading and updating. cli.muConn.Lock() // Unlock. defer cli.muConn.Unlock() // Check the Network Connection. if cli.conn == nil { return ErrNotYetConnected } // Check the existence of the options. if opts == nil || len(opts.TopicFilters) == 0 { return packet.ErrNoTopicFilter } // Define a Packet Identifier. var packetID uint16 // Define an error. var err error // Lock for updating the Session. cli.muSess.Lock() defer cli.muSess.Unlock() // Generate a Packet Identifer. if packetID, err = cli.generatePacketID(); err != nil { return err } // Create an UNSUBSCRIBE Packet. p, err := packet.NewUNSUBSCRIBE(&packet.UNSUBSCRIBEOptions{ PacketID: packetID, TopicFilters: opts.TopicFilters, }) if err != nil { return err } // Set the Packet to the Session. cli.sess.sendingPackets[packetID] = p // Send the Packet to the Server. cli.conn.send <- p return nil } // Terminate ternimates the Client. func (cli *Client) Terminate() { // Send the end signal to the disconnecting goroutine. cli.disconnEndc <- struct{}{} // Wait until all goroutines end. cli.wg.Wait() } // send sends an MQTT Control Packet to the Server. func (cli *Client) send(p packet.Packet) error { // Return an error if the Client has not yet connected to the Server. if cli.conn == nil { return ErrNotYetConnected } // Write the Packet to the buffered writer. if _, err := p.WriteTo(cli.conn.w); err != nil { return err } // Flush the buffered writer. return cli.conn.w.Flush() } // sendCONNECT creates a CONNECT Packet and sends it to the Server. func (cli *Client) sendCONNECT(opts *packet.CONNECTOptions) error { // Initialize the options. if opts == nil { opts = &packet.CONNECTOptions{} } // Create a CONNECT Packet. p, err := packet.NewCONNECT(opts) if err != nil { return err } // Send a CONNECT Packet to the Server. return cli.send(p) } // receive receives an MQTT Control Packet from the Server. func (cli *Client) receive() (packet.Packet, error) { // Return an error if the Client has not yet connected to the Server. if cli.conn == nil { return nil, ErrNotYetConnected } // Get the first byte of the Packet. b, err := cli.conn.r.ReadByte() if err != nil { return nil, err } // Create the Fixed header. fixedHeader := packet.FixedHeader([]byte{b}) // Get and decode the Remaining Length. var mp uint32 = 1 // multiplier var rl uint32 // the Remaining Length for { // Get the next byte of the Packet. b, err = cli.conn.r.ReadByte() if err != nil { return nil, err } fixedHeader = append(fixedHeader, b) rl += uint32(b&0x7F) * mp if b&0x80 == 0 { break } mp *= 128 } // Create the Remaining (the Variable header and the Payload). remaining := make([]byte, rl) if rl > 0 { // Get the remaining of the Packet. if _, err = io.ReadFull(cli.conn.r, remaining); err != nil { return nil, err } } // Create and return a Packet. return packet.NewFromBytes(fixedHeader, remaining) } // clean cleans the Network Connection and the Session if necessary. func (cli *Client) clean() { // Clean the Network Connection. cli.conn = nil // Clean the Session if the Clean Session is true. if cli.sess != nil && cli.sess.cleanSession { cli.sess = nil } } // waitPacket waits for receiving the Packet. func (cli *Client) waitPacket(packetc <-chan struct{}, timeout time.Duration, errTimeout error) { defer cli.conn.wg.Done() var timeoutc <-chan time.Time if timeout > 0 { timeoutc = time.After(timeout * time.Second) } select { case <-packetc: case <-timeoutc: // Handle the timeout error. cli.handleErrorAndDisconn(errTimeout) } } // receivePackets receives Packets from the Server. func (cli *Client) receivePackets() { defer func() { // Close the channel which handles a signal which // notifies the arrival of the CONNACK Packet. close(cli.conn.connack) cli.conn.wg.Done() }() for { // Receive a Packet from the Server. p, err := cli.receive() if err != nil { // Handle the error and disconnect // the Network Connection. cli.handleErrorAndDisconn(err) // End the goroutine. return } // Handle the Packet. if err := cli.handlePacket(p); err != nil { // Handle the error and disconnect // the Network Connection. cli.handleErrorAndDisconn(err) // End the goroutine. return } } } // handlePacket handles the Packet. func (cli *Client) handlePacket(p packet.Packet) error { // Get the MQTT Control Packet type. ptype, err := p.Type() if err != nil { return err } switch ptype { case packet.TypeCONNACK: cli.handleCONNACK() return nil case packet.TypePUBLISH: return cli.handlePUBLISH(p) case packet.TypePUBACK: return cli.handlePUBACK(p) case packet.TypePUBREC: return cli.handlePUBREC(p) case packet.TypePUBREL: return cli.handlePUBREL(p) case packet.TypePUBCOMP: return cli.handlePUBCOMP(p) case packet.TypeSUBACK: return cli.handleSUBACK(p) case packet.TypeUNSUBACK: return cli.handleUNSUBACK(p) case packet.TypePINGRESP: return cli.handlePINGRESP() default: return packet.ErrInvalidPacketType } } // handleCONNACK handles the CONNACK Packet. func (cli *Client) handleCONNACK() { // Notify the arrival of the CONNACK Packet if possible. select { case cli.conn.connack <- struct{}{}: default: } } // handlePUBLISH handles the PUBLISH Packet. func (cli *Client) handlePUBLISH(p packet.Packet) error { // Get the PUBLISH Packet. publish := p.(*packet.PUBLISH) switch publish.QoS { case mqtt.QoS0: // Lock for reading. cli.muConn.RLock() // Unlock. defer cli.muConn.RUnlock() // Handle the Application Message. cli.handleMessage(publish.TopicName, publish.Message) return nil case mqtt.QoS1: // Lock for reading. cli.muConn.RLock() // Unlock. defer cli.muConn.RUnlock() // Handle the Application Message. cli.handleMessage(publish.TopicName, publish.Message) // Create a PUBACK Packet. puback, err := packet.NewPUBACK(&packet.PUBACKOptions{ PacketID: publish.PacketID, }) if err != nil { return err } // Send the Packet to the Server. cli.conn.send <- puback return nil default: // Lock for update. cli.muSess.Lock() // Unlock. defer cli.muSess.Unlock() // Validate the Packet Identifier. if _, exist := cli.sess.receivingPackets[publish.PacketID]; exist { return packet.ErrInvalidPacketID } // Set the Packet to the Session. cli.sess.receivingPackets[publish.PacketID] = p // Create a PUBREC Packet. pubrec, err := packet.NewPUBREC(&packet.PUBRECOptions{ PacketID: publish.PacketID, }) if err != nil { return err } // Send the Packet to the Server. cli.conn.send <- pubrec return nil } } // handlePUBACK handles the PUBACK Packet. func (cli *Client) handlePUBACK(p packet.Packet) error { // Lock for update. cli.muSess.Lock() // Unlock. defer cli.muSess.Unlock() // Extract the Packet Identifier of the Packet. id := p.(*packet.PUBACK).PacketID // Validate the Packet Identifier. if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypePUBLISH); err != nil { return err } // Delete the PUBLISH Packet from the Session. delete(cli.sess.sendingPackets, id) return nil } // handlePUBREC handles the PUBREC Packet. func (cli *Client) handlePUBREC(p packet.Packet) error { // Lock for update. cli.muSess.Lock() // Unlock. defer cli.muSess.Unlock() // Extract the Packet Identifier of the Packet. id := p.(*packet.PUBREC).PacketID // Validate the Packet Identifier. if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypePUBLISH); err != nil { return err } // Create a PUBREL Packet. pubrel, err := packet.NewPUBREL(&packet.PUBRELOptions{ PacketID: id, }) if err != nil { return err } // Set the PUBREL Packet to the Session. cli.sess.sendingPackets[id] = pubrel // Send the Packet to the Server. cli.conn.send <- pubrel return nil } // handlePUBREL handles the PUBREL Packet. func (cli *Client) handlePUBREL(p packet.Packet) error { // Lock for update. cli.muSess.Lock() // Unlock. defer cli.muSess.Unlock() // Extract the Packet Identifier of the Packet. id := p.(*packet.PUBREL).PacketID // Validate the Packet Identifier. if err := cli.validatePacketID(cli.sess.receivingPackets, id, packet.TypePUBLISH); err != nil { return err } // Get the Packet from the Session. publish := cli.sess.receivingPackets[id].(*packet.PUBLISH) // Lock for reading. cli.muConn.RLock() // Handle the Application Message. cli.handleMessage(publish.TopicName, publish.Message) // Unlock. cli.muConn.RUnlock() // Delete the Packet from the Session delete(cli.sess.receivingPackets, id) // Create a PUBCOMP Packet. pubcomp, err := packet.NewPUBCOMP(&packet.PUBCOMPOptions{ PacketID: id, }) if err != nil { return err } // Send the Packet to the Server. cli.conn.send <- pubcomp return nil } // handlePUBCOMP handles the PUBCOMP Packet. func (cli *Client) handlePUBCOMP(p packet.Packet) error { // Lock for update. cli.muSess.Lock() // Unlock. defer cli.muSess.Unlock() // Extract the Packet Identifier of the Packet. id := p.(*packet.PUBCOMP).PacketID // Validate the Packet Identifier. if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypePUBREL); err != nil { return err } // Delete the PUBREL Packet from the Session. delete(cli.sess.sendingPackets, id) return nil } // handleSUBACK handles the SUBACK Packet. func (cli *Client) handleSUBACK(p packet.Packet) error { // Lock for update. cli.muConn.Lock() cli.muSess.Lock() // Unlock. defer cli.muConn.Unlock() defer cli.muSess.Unlock() // Extract the Packet Identifier of the Packet. id := p.(*packet.SUBACK).PacketID // Validate the Packet Identifier. if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypeSUBSCRIBE); err != nil { return err } // Get the subscription requests of the SUBSCRIBE Packet. subreqs := cli.sess.sendingPackets[id].(*packet.SUBSCRIBE).SubReqs // Delete the SUBSCRIBE Packet from the Session. delete(cli.sess.sendingPackets, id) // Get the Return Codes of the SUBACK Packet. returnCodes := p.(*packet.SUBACK).ReturnCodes // Check the lengths of the Return Codes. if len(returnCodes) != len(subreqs) { return ErrInvalidSUBACK } // Set the subscriptions to the Network Connection. for i, code := range returnCodes { // Skip if the Return Code is failure. if code == packet.SUBACKRetFailure { continue } // Get the Topic Filter. topicFilter := string(subreqs[i].TopicFilter) // Move the subscription information from // unackSubs to ackedSubs. cli.conn.ackedSubs[topicFilter] = cli.conn.unackSubs[topicFilter] delete(cli.conn.unackSubs, topicFilter) } return nil } // handleUNSUBACK handles the UNSUBACK Packet. func (cli *Client) handleUNSUBACK(p packet.Packet) error { // Lock for update. cli.muConn.Lock() cli.muSess.Lock() // Unlock. defer cli.muConn.Unlock() defer cli.muSess.Unlock() // Extract the Packet Identifier of the Packet. id := p.(*packet.UNSUBACK).PacketID // Validate the Packet Identifier. if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypeUNSUBSCRIBE); err != nil { return err } // Get the Topic Filters of the UNSUBSCRIBE Packet. topicFilters := cli.sess.sendingPackets[id].(*packet.UNSUBSCRIBE).TopicFilters // Delete the UNSUBSCRIBE Packet from the Session. delete(cli.sess.sendingPackets, id) // Delete the Topic Filters from the Network Connection. for _, topicFilter := range topicFilters { delete(cli.conn.ackedSubs, string(topicFilter)) } return nil } // handlePINGRESP handles the PINGRESP Packet. func (cli *Client) handlePINGRESP() error { // Lock for reading and updating pingrespcs. cli.conn.muPINGRESPs.Lock() // Check the length of pingrespcs. if len(cli.conn.pingresps) == 0 { // Unlock. cli.conn.muPINGRESPs.Unlock() // Return an error if there is no channel in pingrespcs. return ErrInvalidPINGRESP } // Get the first channel in pingrespcs. pingrespc := cli.conn.pingresps[0] // Remove the first channel from pingrespcs. cli.conn.pingresps = cli.conn.pingresps[1:] // Unlock. cli.conn.muPINGRESPs.Unlock() // Notify the arrival of the PINGRESP Packet if possible. select { case pingrespc <- struct{}{}: default: } return nil } // handleError handles the error and disconnects // the Network Connection. func (cli *Client) handleErrorAndDisconn(err error) { // Lock for reading. cli.muConn.RLock() // Ignore the error and end the process // if the Network Connection has already // been disconnected. if cli.conn == nil || cli.conn.disconnected { // Unlock. cli.muConn.RUnlock() return } // Unlock. cli.muConn.RUnlock() // Handle the error. if cli.errorHandler != nil { cli.errorHandler(err) } // Send a disconnect signal to the goroutine // via the channel if possible. select { case cli.disconnc <- struct{}{}: default: } } // sendPackets sends Packets to the Server. func (cli *Client) sendPackets(keepAlive time.Duration, pingrespTimeout time.Duration) { defer func() { // Lock for reading and updating pingrespcs. cli.conn.muPINGRESPs.Lock() // Close the channels which handle a signal which // notifies the arrival of the PINGREQ Packet. for _, pingresp := range cli.conn.pingresps { close(pingresp) } // Initialize pingrespcs cli.conn.pingresps = make([]chan struct{}, 0) // Unlock. cli.conn.muPINGRESPs.Unlock() cli.conn.wg.Done() }() for { var keepAlivec <-chan time.Time if keepAlive > 0 { keepAlivec = time.After(keepAlive * time.Second) } select { case p := <-cli.conn.send: // Lock for sending the Packet. cli.muConn.RLock() // Send the Packet to the Server. err := cli.send(p) // Unlock. cli.muConn.RUnlock() if err != nil { // Handle the error and disconnect the Network Connection. cli.handleErrorAndDisconn(err) // End this function. return } case <-keepAlivec: // Lock for sending the Packet. cli.muConn.RLock() // Send a PINGREQ Packet to the Server. err := cli.send(packet.NewPINGREQ()) // Unlock. cli.muConn.RUnlock() if err != nil { // Handle the error and disconnect the Network Connection. cli.handleErrorAndDisconn(err) // End this function. return } // Create a channel which handles the signal to notify the arrival of // the PINGRESP Packet. pingresp := make(chan struct{}) // Lock for appending the channel to pingrespcs. cli.conn.muPINGRESPs.Lock() // Append the channel to pingrespcs. cli.conn.pingresps = append(cli.conn.pingresps, pingresp) // Unlock. cli.conn.muPINGRESPs.Unlock() // Launch a goroutine which waits for receiving the PINGRESP Packet. cli.conn.wg.Add(1) go cli.waitPacket(pingresp, pingrespTimeout, ErrPINGRESPTimeout) case <-cli.conn.sendEnd: // End this function. return } } } // generatePacketID generates and returns a Packet Identifier. func (cli *Client) generatePacketID() (uint16, error) { // Define a Packet Identifier. id := minPacketID for { // Find a Packet Identifier which does not used. if _, exist := cli.sess.sendingPackets[id]; !exist { // Return the Packet Identifier. return id, nil } if id == maxPacketID { break } id++ } // Return an error if available ids are not found. return 0, ErrPacketIDExhaused } // newPUBLISHPacket creates and returns a PUBLISH Packet. func (cli *Client) newPUBLISHPacket(opts *PublishOptions) (packet.Packet, error) { // Define a Packet Identifier. var packetID uint16 if opts.QoS != mqtt.QoS0 { // Lock for reading and updating the Session. cli.muSess.Lock() defer cli.muSess.Unlock() // Define an error. var err error // Generate a Packet Identifer. if packetID, err = cli.generatePacketID(); err != nil { return nil, err } } // Create a PUBLISH Packet. p, err := packet.NewPUBLISH(&packet.PUBLISHOptions{ QoS: opts.QoS, Retain: opts.Retain, TopicName: opts.TopicName, PacketID: packetID, Message: opts.Message, }) if err != nil { return nil, err } if opts.QoS != mqtt.QoS0 { // Set the Packet to the Session. cli.sess.sendingPackets[packetID] = p } // Return the Packet. return p, nil } // validateSendingPacketID checks if the Packet which has // the Packet Identifier and the MQTT Control Packet type // specified by the parameters exists in the Session's // sendingPackets. func (cli *Client) validatePacketID(packets map[uint16]packet.Packet, id uint16, ptype byte) error { // Extract the Packet. p, exist := packets[id] if !exist { // Return an error if there is no Packet which has the Packet Identifier // specified by the parameter. return packet.ErrInvalidPacketID } // Extract the MQTT Control Packet type of the Packet. t, err := p.Type() if err != nil { return err } if t != ptype { // Return an error if the Packet's MQTT Control Packet type does not // equal to one specified by the parameter. return packet.ErrInvalidPacketID } return nil } // handleMessage handles the Application Message. func (cli *Client) handleMessage(topicName, message []byte) { // Get the string of the Topic Name. topicNameStr := string(topicName) for topicFilter, handler := range cli.conn.ackedSubs { if handler == nil || !match(topicNameStr, topicFilter) { continue } // Execute the handler. go handler(topicName, message) } } // New creates and returns a Client. func New(opts *Options) *Client { // Initialize the options. if opts == nil { opts = &Options{} } // Create a Client. cli := &Client{ disconnc: make(chan struct{}, 1), disconnEndc: make(chan struct{}), errorHandler: opts.ErrorHandler, } // Launch a goroutine which disconnects the Network Connection. cli.wg.Add(1) go func() { defer func() { cli.wg.Done() }() for { select { case <-cli.disconnc: if err := cli.Disconnect(); err != nil { if cli.errorHandler != nil { cli.errorHandler(err) } } case <-cli.disconnEndc: // End the goroutine. return } } }() // Return the Client. return cli } // match checks if the Topic Name matches the Topic Filter. func match(topicName, topicFilter string) bool { // Tokenize the Topic Name. nameTokens := strings.Split(topicName, "/") nameTokensLen := len(nameTokens) // Tolenize the Topic Filter. filterTokens := strings.Split(topicFilter, "/") for i, t := range filterTokens { switch t { case "#": return i != 0 || !strings.HasPrefix(nameTokens[0], "$") case "+": if i == 0 && strings.HasPrefix(nameTokens[0], "$") { return false } if nameTokensLen <= i { return false } default: if nameTokensLen <= i || t != nameTokens[i] { return false } } } return len(filterTokens) == nameTokensLen }