使用 Go 语言实现完整且轻量级高性能的 MQTT Broker

MQTT(Message Queuing Telemetry Transport)是一种轻量级的发布/订阅消息传输协议。但是目前虽然mqtt的客户端很多,但是服务端着实不多,常见的服务端如mosquitto或emqx。但是golang语言的实现几乎找不到。golang的轻量级部署和高并发高性能,很合适做mqtt Broker。本文将详细介绍如何使用 Go 语言实现一个简单轻量级且高性能的 MQTT Broker,并涵盖MQTT3.1.1协议的核心特性和完整功能。

1. 需求分析

本文选择golang语言实现一个完整的 MQTT 3.1.1 Broker,不涉及集群支持和协议版本检测。简单且轻量级,不但可以替代mosquitto,后续还可以灵活的做扩展,如增加webUI的管理界面。且部署也很简单,一个exe可执行文件。

完整项目开源地址:https://github.com/yangyongzhen/goang-mqtt-broker

gitee: https://gitee.com/yyz116/goang-mqtt-broker

可执行文件在release目录下。

1.1 实现效果截图

服务启动:

客户端发布:

客户端订阅:

使用mosquitto客户端测试效果:


优化增加基于redis的持久化存储:

可在etc/config.yaml文件中配置是否启用redis的持久化。默认基于内存。

windows下的可执行文件仅有7M左右大小,简单小巧。且代码开源方便定制。可以作为替代mosquitto的另外一种选择。

1.2 功能特性
1.2.1核心功能
  • 完整的 MQTT 3.1.1 协议支持
  • QoS 0, 1, 2 消息传递保证
  • 会话管理(持久会话和清理会话)
  • 保留消息(Retained Messages)
  • 遗嘱消息(Last Will and Testament)
  • 主题通配符(+ 和 # 通配符支持)
  • 客户端认证(用户名/密码)
  • 保活机制(Keep Alive)
  • 并发安全
1.2.2 架构特性
  • 🏗️ 模块化设计,易于扩展
  • 🔌 可插拔存储接口
  • 🔒 线程安全的并发处理
  • 📊 内置监控指标
  • 🐳 Docker 支持

2. 项目架构设计

架构设计

数据流

客户端连接 → TCP Server 接受连接

协议解析 → Client 解析 MQTT 数据包

认证验证 → Auth 模块验证用户凭据

会话管理 → Storage 加载/保存会话信息

消息路由 → Broker 根据订阅关系路由消息

主题匹配 → Topic Manager 处理通配符匹配

2.1 目录结构
复制代码
mqtt-broker/
├── README.md
├── Makefile
├── Dockerfile
├── go.mod
├── go.sum
├── cmd/
│   ├── broker/
│   │   └── main.go
│   └── test-client/
│       └── main.go
├── internal/
│   ├── auth/
│   │   └── auth.go
│   ├── broker/
│   │   ├── broker.go
│   │   ├── client.go
│   │   └── topic.go
│   ├── protocol/
│   │   ├── common/
│   │   │   └── types.go
│   │   └── mqtt311/
│   │       └── packet.go
│   └── storage/
│       ├── interface.go
│       └── memory/
│           └── store.go
└── pkg/
    └── mqtt/
        └── packet.go
2.2 主要模块
  • cmd/broker/main.go:程序入口。
  • internal/broker/:Broker 核心逻辑,包括连接管理、消息路由等。
  • internal/storage/:存储接口和内存实现。
  • pkg/mqtt/packet.go:MQTT 数据包编码和解码。

3. 核心实现

3.1 存储接口

internal/storage/interface.go 文件中定义存储接口:

go 复制代码
package storage

import (
    "github.com/yangyongzhen/mqtt-broker/internal/protocol/common"
)

type Store interface {
    SaveSession(clientID string, session *Session) error
    LoadSession(clientID string) (*Session, error)
    DeleteSession(clientID string) error
    SaveMessage(clientID string, message *common.Message) error
    LoadMessages(clientID string) ([]*common.Message, error)
    DeleteMessage(clientID string, packetID uint16) error
    SaveRetainedMessage(topic string, message *common.Message) error
    LoadRetainedMessage(topic string) (*common.Message, error)
    DeleteRetainedMessage(topic string) error
    SaveSubscription(clientID string, subscription *common.Subscription) error
    LoadSubscriptions(clientID string) ([]*common.Subscription, error)
    DeleteSubscription(clientID string, topic string) error
}

type Session struct {
    ClientID      string
    CleanSession  bool
    Subscriptions map[string]*common.Subscription
    PendingAcks   map[uint16]*common.Message
    LastSeen      time.Time
}
3.2 内存存储实现

internal/storage/memory/store.go 文件中实现内存存储:

go 复制代码
package memory

import (
    "sync"
    "github.com/yangyongzhen/mqtt-broker/internal/storage"
    "github.com/yangyongzhen/mqtt-broker/internal/protocol/common"
)

type MemoryStore struct {
    sessions        map[string]*storage.Session
    retainedMsgs    map[string]*common.Message
    clientMessages  map[string][]*common.Message
    mu              sync.RWMutex
}

func NewMemoryStore() *MemoryStore {
    return &MemoryStore{
        sessions:       make(map[string]*storage.Session),
        retainedMsgs:   make(map[string]*common.Message),
        clientMessages: make(map[string][]*common.Message),
    }
}

func (m *MemoryStore) SaveSession(clientID string, session *storage.Session) error {
    m.mu.Lock()
    defer m.mu.Unlock()
    m.sessions[clientID] = session
    return nil
}

func (m *MemoryStore) LoadSession(clientID string) (*storage.Session, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    session, exists := m.sessions[clientID]
    if !exists {
        return nil, nil
    }
    return session, nil
}

// 其他方法省略...
3.3 客户端连接管理

internal/broker/client.go 文件中实现客户端连接管理:

go 复制代码
package broker

import (
    "bufio"
    "fmt"
    "net"
    "sync"
    "time"
    "github.com/yangyongzhen/mqtt-broker/internal/protocol/common"
    "github.com/yangyongzhen/mqtt-broker/internal/protocol/mqtt311"
    "github.com/yangyongzhen/mqtt-broker/internal/storage"
    "github.com/yangyongzhen/mqtt-broker/pkg/mqtt"
)

type Client struct {
    conn           net.Conn
    clientID       string
    info           *common.ClientInfo
    session        *storage.Session
    broker         *Broker
    packetReader   *mqtt.PacketReader
    writeChan      chan []byte
    closeChan      chan struct{}
    keepAliveTimer *time.Timer
    mu             sync.RWMutex
    connected      bool
    nextPacketID   uint16
    pendingAcks    map[uint16]*PendingMessage
}

type PendingMessage struct {
    Message   *common.Message
    Timestamp time.Time
    Retries   int
}

func NewClient(conn net.Conn, broker *Broker) *Client {
    return &Client{
        conn:         conn,
        broker:       broker,
        packetReader: mqtt.NewPacketReader(conn),
        writeChan:    make(chan []byte, 1000),
        closeChan:    make(chan struct{}),
        pendingAcks:  make(map[uint16]*PendingMessage),
        nextPacketID: 1,
    }
}

func (c *Client) Start() {
    go c.readLoop()
    go c.writeLoop()
    go c.retryLoop()
}

func (c *Client) readLoop() {
    defer c.Close()
    for {
        select {
        case <-c.closeChan:
            return
        default:
            packet, err := c.packetReader.ReadPacket()
            if err != nil {
                fmt.Printf("Read packet error: %v\n", err)
                return
            }
            c.handlePacket(packet)
        }
    }
}

func (c *Client) writeLoop() {
    defer c.Close()
    for {
        select {
        case data := <-c.writeChan:
            if _, err := c.conn.Write(data); err != nil {
                fmt.Printf("Write error: %v\n", err)
                return
            }
        case <-c.closeChan:
            return
        }
    }
}

func (c *Client) retryLoop() {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            c.retryPendingMessages()
        case <-c.closeChan:
            return
        }
    }
}

func (c *Client) handlePacket(packet common.Packet) {
    switch p := packet.(type) {
    case *mqtt311.ConnectPacket:
        c.handleConnect(p)
    case *mqtt311.PublishPacket:
        c.handlePublish(p)
    case *mqtt311.SubscribePacket:
        c.handleSubscribe(p)
    case *mqtt311.UnsubscribePacket:
        c.handleUnsubscribe(p)
    case *mqtt311.PingreqPacket:
        c.handlePingReq()
    case *mqtt311.DisconnectPacket:
        c.handleDisconnect()
    }
}

// handleConnect, handlePublish 等其他方法省略...
3.4 主 Broker 实现

internal/broker/broker.go 文件中实现主 Broker 的逻辑:

go 复制代码
package broker

import (
    "fmt"
    "net"
    "sync"
    "time"
    "github.com/yangyongzhen/mqtt-broker/internal/auth"
    "github.com/yangyongzhen/mqtt-broker/internal/protocol/common"
    "github.com/yangyongzhen/mqtt-broker/internal/storage"
)

type Broker struct {
    listener      net.Listener
    clients       map[string]*Client
    topicManager  *TopicManager
    store         storage.Store
    auth          auth.Authenticator
    mu            sync.RWMutex
    running       bool
    config        *Config
}

type Config struct {
    MaxConnections   int
    MaxMessageSize   int
    RetainedMsgLimit int
    SessionExpiry    time.Duration
    MessageExpiry    time.Duration
}

func NewBroker(store storage.Store, authenticator auth.Authenticator) *Broker {
    return &Broker{
        clients:      make(map[string]*Client),
        topicManager: NewTopicManager(),
        store:        store,
        auth:         authenticator,
        config: &Config{
            MaxConnections:   10000,
            MaxMessageSize:   1024 * 1024,
            RetainedMsgLimit: 10000,
            SessionExpiry:    24 * time.Hour,
            MessageExpiry:    24 * time.Hour,
        },
    }
}

func (b *Broker) Start(address string) error {
    listener, err := net.Listen("tcp", address)
    if err != nil {
        return err
    }

    b.listener = listener
    b.running = true

    fmt.Printf("MQTT Broker started on %s\n", address)

    for b.running {
        conn, err := listener.Accept()
        if err != nil {
            if b.running {
                fmt.Printf("Accept error: %v\n", err)
            }
            continue
        }

        client := NewClient(conn, b)
        go client.Start()
    }

    return nil
}

func (b *Broker) Stop() {
    b.running = false
    if b.listener != nil {
        b.listener.Close()
    }

    b.mu.Lock()
    defer b.mu.Unlock()

    for _, client := range b.clients {
        client.Close()
    }
}

// AddClient, RemoveClient, PublishMessage 等其他方法省略...
3.5 主程序入口

cmd/broker/main.go 文件中定义主程序入口:

go 复制代码
package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"
    "github.com/yangyongzhen/mqtt-broker/internal/auth"
    "github.com/yangyongzhen/mqtt-broker/internal/broker"
    "github.com/yangyongzhen/mqtt-broker/internal/storage/memory"
)

func main() {
    addr := flag.String("addr", ":1883", "MQTT broker address")
    flag.Parse()

    authenticator := auth.NewSimpleAuthenticator() // 示例认证器,需要自行实现
    store := memory.NewMemoryStore()

    b := broker.NewBroker(store, authenticator)
    go func() {
        if err := b.Start(*addr); err != nil {
            log.Fatalf("Failed to start MQTT broker: %v", err)
        }
    }()

    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    <-sigChan
    b.Stop()
    fmt.Println("MQTT broker stopped")
}

以上代码是实现一个简单的 MQTT Broker 的基础框架,更多详细功能和性能优化可以根据实际需求进行扩展和改进。

安装和运行

  1. 克隆项目
bash 复制代码
git clone <your-repo-url>
cd mqtt-broker

go mod tidy

安装依赖

bash 复制代码
go mod tidy

构建项目

bash 复制代码
make build
或者
bash 复制代码
go build -o bin/mqtt-broker cmd/broker/main.go
运行 Broker
bash 复制代码
make run
或者
bash 复制代码
./bin/mqtt-broker -addr=:1883 -debug

使用 Docker

构建镜像
bash 复制代码
docker build -t mqtt-broker .
#### 运行容器
docker run -p 1883:1883 mqtt-broker
#### 使用示例

**启动 Broker**
#### 默认端口 1883
go run cmd/broker/main.go
##### 自定义端口和调试模式
go run cmd/broker/main.go -addr=:1883 -debug

测试客户端

项目包含一个简单的测试客户端,可以用来测试 broker 功能:

订阅消息:

bash 复制代码
go run cmd/test-client/main.go -mode=sub -topic=test/hello -client=subscriber1

发布消息:

bash 复制代码
go run cmd/test-client/main.go -mode=pub -topic=test/hello -msg="Hello MQTT!" -client=publisher1

使用第三方客户端

你也可以使用任何标准的 MQTT 客户端连接到 broker:

使用 mosquitto 客户端:

订阅
bash 复制代码
mosquitto_sub -h localhost -p 1883 -t "test/topic"
发布
bash 复制代码
mosquitto_pub -h localhost -p 1883 -t "test/topic" -m "Hello World"

使用认证:

默认用户:

admin/password, test/test123

mosquitto_pub -h localhost -p 1883 -u admin -P password -t "test/topic" -m "Authenticated message"

配置说明

命令行参数

参数 默认值 说明

-addr :1883 Broker 监听地址

-debug false 启用调试日志

内置用户

Broker 默认创建了以下测试用户:

用户名 密码

admin password

test test123

项目开源地址:

https://github.com/yangyongzhen/goang-mqtt-broker

gitee: https://gitee.com/yyz116/goang-mqtt-broker

作者

作者csdn猫哥,转载请注明出处: https://blog.csdn.net/yyz_1987

相关推荐
菥菥爱嘻嘻1 小时前
JS手写代码篇---Pomise.race
开发语言·前端·javascript
南瓜胖胖1 小时前
【R语言编程绘图-调色】
开发语言·r语言
lanbing2 小时前
非常适合初学者的Golang教程
开发语言·后端·golang
stormsha3 小时前
GO语言进阶:掌握进程OS操作与高效编码数据转换
开发语言·数据库·后端·golang·go语言·源代码管理
B1nna3 小时前
SpringBoot项目快速打包与部署,War包⽅式打包部署与Jar包⽅式打包部署两种方式
spring boot·后端·jar
老神在在0014 小时前
javaEE1
java·开发语言·学习·java-ee
魔道不误砍柴功4 小时前
《接口和抽象类到底怎么选?设计原则与经典误区解析》
java·开发语言
我是李武涯5 小时前
C++ 条件变量虚假唤醒问题的解决
开发语言·c++·算法
编码小笨猪6 小时前
[ Qt ] | 常用控件(三):
开发语言·qt
Bioinfo Guy6 小时前
R包安装报错解决案例系列|R包使用及ARM架构解决data.table安装错误问题
开发语言·arm开发·r语言