GoMQTT服务器 节点路由树设计

10 篇文章 2 订阅

GoMQTT服务器 节点路由树设计

第二章 节点路由树设计

写的不好,别见怪,有错误希望指出,谢谢大家。



思路

采用节点路由树来管理mqtt消息需要发送到哪些节点上去,当该节点有消息需要转发到其他节点时,查找该表,得到需要转发的节点信息,再转发。
当然,可以为了性能优化:不一定有通配符和没有通配符都采用路由树,可以通过对没有通配符的主题进行hash,来查找相应的节点,这个需要仔细考虑一下,我暂时只是有这个想法。

添加节点到主题上去,采用对应的主题递归查找添加,比如 a/b/c 先查找a->b->c在c对应的RouteTable中的Nodes中添加该node信息。
删除类似。
当遇到 “+” 号时,在当前层的 “+”号对应的RouteTable中继续递归,不在同一层其他RouteTable中添加,降低复杂度。
查找的时候:

  • 当主题是 a/b/c 时,这个最简单,创建一个*map[string]*Node ,取名为rest ,然后先从第一层找到a,然后查看a对应的Children中是否有b,有就从b中继续找c、+和#,有就把这三个的Nodes全部添加到rest中;如果a对应的Children中包括有 +和# 通配符节点,则将 # 中的节点全部添加到rest中,然后在从 + 的Children中查找 c、+和# ,后面步骤差不多,就是添加。
  • 当主题是 a/+/c时,创建一个*map[string]*Node ,取名为rest,然后先从第一层找到a,然后将a中#下的Nodes全部添加,然后从其它节点中找第二层,看是否有c、+、#,有就添加
  • 当主题是a/#/c、a/# ,它会将a节点下所有的子节点Nodes全部添加,包括a下的子节点

一、节点路由树设计

第一层已经限制不能有 + ,# , $ 通配符,就不能让客户端订阅+/… 、#和$sys,这种太危险了。当然也可以在设备接入时限定订阅的主题到数据库中,设备接入时先查询一下再订阅。
在这里插入图片描述

二、代码

主题节点添加、删除与删除

采用读写锁,锁的粒度还是有点大,可以选择一下 sync.Map 来保证并发操作

1.数据设计

import (
	"strings"
	error2 "study_one_day/gotcp/error"
	"sync"
)

/**
全局路由表
*/
type Route struct {
	Table *map[string]*RouteTable
}

/**
节点信息
这个可以只要节点ID这个基本不会改变的信息即可,然后根据ID,从节点表中获取信息,这样更好
*/
type Node struct {
	Ip   string
	Port string
	Name string
}

/**
主题路由表  结构
负责根据该表确定需要发送到哪些节点上去
*/
type RouteTable struct {
	Pic      string                  // 主题名称 要考虑 + # 等通配符
	Nodes    *map[string]*Node       // 当前节点主题下有的节点列表
	Children *map[string]*RouteTable // 子节点列表
}

const (
	SingleLevel = "+"
	MultiLevel  = "#"
	SystemLevel = "$"
)

// 全局路由表
var RouteTables *Route
// 全局节点 里面是Node类型,在获取到节点数据时更新
var AllNodes *sync.Map
// 读写锁
var rwLock *sync.RWMutex

/**
初始化路由表
*/
func init() {
	RouteTables = &Route{&map[string]*RouteTable{}}
	AllNodes = &sync.Map{}
	rwLock = &sync.RWMutex{}
}

2.添加节点到主题下


/**
该节点添加订阅主题时
不推荐直接添加 #主题 , 主题不能包含空格 不能 /1//3/,这样会被修改为 1/3 , 不能包含中文字符
第一个不能是 + # $
需要添加到主题路由表
*/
func (this *Route) AddNodeInTopic(topic string, node *Node) (bool, *error2.TopicError) {
	if len(topic) == 0 {
		return false, error2.CreatTopicError("topic not allow nil")
	}
	topic = strings.TrimSpace(topic)
	topics := strings.Split(topic, "/")
	// 删除 空字符串
	topics = deleteSpace(&topics)
	lens := len(topics)
	if ok,err := checkIllegalChar(&topics);!ok{
		return false,err
	}
	root := this.Table
	// 加锁更新
	rwLock.Lock()
	// defer 解锁
	defer rwLock.Unlock()
	if v, ok := (*root)[topics[0]]; ok { // 存在就继续下一层
		bip := topics[1:]
		//后面没有了,就将node添加到当前RouteTable
		if len(bip) == 0 {
			(*v.Nodes)[node.Name] = node
		} else {
			add(v, bip, node)
		}
	} else { // 不存在就递归创建
		loopCreat(lens, root, node, topics)
	}
	return true, nil
}
/**
添加到当前层
 最底层返回true表示,递归回退的第一次要添加改nodeName
*/
func add(tab *RouteTable, topics []string, node *Node) bool {
	//if len(topics) == 0 { //代码层判断了,这个就不要了
	//	return true
	//}
	root := tab.Children
	lens := len(topics)
	if topics[0] == MultiLevel {
		// # 直接添加
		//addNowNode(tab.Nodes, node)
		if v, ok := (*root)[MultiLevel]; ok {
			addNowNode(v.Nodes, node)
		} else {
			// 没有就创建
			pnod := map[string]*Node{}
			pnod[node.Name] = node
			(*root)[MultiLevel] = &RouteTable{
				Pic:      MultiLevel,
				Nodes:    &pnod,
				Children: &map[string]*RouteTable{},
			}
		}
		return true
	}
	// + 和普通主题
	if v, ok := (*root)[topics[0]]; ok { // 存在就继续下一层
		bip := topics[1:]
		//后面没有了,就将node添加到当前RouteTable
		if len(bip) == 0 {
			(*v.Nodes)[node.Name] = node
		} else {
			add(v, bip, node)
		}
	} else { // 没有就添加
		loopCreat(lens, root, node, topics)
	}
	return true
}

/**
循环创建
*/
func loopCreat(lens int, root *map[string]*RouteTable, node *Node, topics []string) {
	for i := 0; i < lens; i++ {
		cmp := &map[string]*RouteTable{}
		nmp := map[string]*Node{}
		if i == lens-1 {
			// 添加到当前RouteTable的node列表中
			nmp[node.Name] = node
		}
		(*root)[topics[i]] = &RouteTable{
			Pic:      topics[i],
			Nodes:    &nmp,
			Children: cmp,
		}
		root = cmp
	}
}

/**
添加到当前map 中
*/
func addNowNode(v *map[string]*Node, node *Node) {
	// 不存在 就 添加
	//if _,ok := (*(v))[node.Name];!ok{
	//	(*(v))[node.Name] = node
	//}
	// 直接添加,这样还可以更新一下
	(*(v))[node.Name] = node
}
/**
检查数组第一个是否为非法字符 + # $
 */
func checkIllegalChar(topic *[]string) (bool,*error2.TopicError) {
	if len(*topic) == 0{
		return false,error2.CreatTopicError("topic is entity")
	}
	switch (*topic)[0] {
	case MultiLevel:
		return false,error2.CreatTopicError("topic first is \"#\"")
	case SingleLevel:
		return false,error2.CreatTopicError("topic first is \"+\"")
	case "":
		return false,error2.CreatTopicError("topic first is \"\"")

	}
	if strings.HasPrefix((*topic)[0],SystemLevel){
		return false,error2.CreatTopicError("topic first is \"$\" Prefix")
	}
	return true, nil
}
/**
删除数组中的空字符串
 */
func deleteSpace(topics *[]string) []string {
	// 删除 空字符串
	for i, v := range *topics {
		if v == "" {
			*topics = append((*topics)[:i], (*topics)[i+1:]...)
		}
	}
	return *topics
}

3.删除某主题下的某节点

这个应该修改一下,应该判断是否应该直接删除,还是添加一个字段(计数器)表示该节点在这个主题下有多少个客户端订阅了该主题,这样会好一些,每次删除一次,就减一,当为0时再删除该节点。

/**
删除节点在当前主题上的
*/
func (this *Route) DeleteNodeInTopic(topic string, node *Node) (bool, *error2.TopicError) {
	topic = strings.TrimSpace(topic)
	topics := strings.Split(topic, "/")
	lens := len(topics)
	if lens == 0 {
		return false, error2.CreatTopicError("topic is entity or /")
	}
	rwLock.Lock()
	defer rwLock.Unlock()
	if v, ok := (*this.Table)[topics[0]]; ok {
		if lens == 1 {
			// 删除当前
			delete(*v.Nodes, node.Name)
		} else {
			// 往下搜索删除
			deleteNode(v, topics[1:], node)
		}
	}
	// 没有考虑 + #,因为第一层限制不能有这两个
	return true, nil
}
/**
删除节点
*/
func deleteNode(tab *RouteTable, topics []string, node *Node) {
	lens := len(topics)
	if v, ok := (*tab.Children)[topics[0]]; ok {
		if lens == 1 {
			delete(*v.Nodes, node.Name)
		} else {
			deleteNode(v, topics[1:], node)
		}
	}
}

4.查找某主题应该向哪些节点发送消息


/**
根据主题查找节点
 */
func (this *Route) FindNodeByTopic(topic string) ([]string, *error2.TopicError)  {
	tt := strings.TrimSpace(topic)
	topics := strings.Split(tt,"/")
	lens := len(topics)
	if ok,err := checkIllegalChar(&topics);!ok{
		return nil,err
	}
	// 先创建保存结果的map
	rest := map[string]*Node{}
	rwLock.RLock()
	defer rwLock.RUnlock()
	if v, ok := (*this.Table)[topics[0]]; ok {
		if lens == 1 {
			// 只需要在当前第一层Nodes中查找
			// 因为第一层限制了+ # $ 所以不要考虑那么复杂
			for _, v := range *v.Nodes{
				rest[v.Name] = v
			}
		} else {
			// 往下搜索添加
			findNode(v, topics[1:], &rest)
		}
		mapLen := len(rest)
		ret := make([]string,mapLen)
		i := 0
		for k, _ := range rest {
			k1 := k
			ret[i] = k1
			i++
		}
		return ret,nil
	}
	return nil,error2.CreatTopicError("no have this topic")
}
// 查找元素
func findNode(tab *RouteTable, topics []string,rest *map[string]*Node)  {
	// if topic == #  把下面的全部添加,然后返回
	if topics[0] == MultiLevel{
		addMulti(tab,rest)
	}else if topics[0] == SingleLevel{
		// if topic == + 把 + 和 其他主题 下符合条件的添加
		addSingle(tab,topics,rest)
	}else{
		// if topic == 其他主题 把 + 和 其他主题下符合条件的添加,并且添加 # 下的
		addOther(tab,topics,rest)
	}
}
/**
全部添加
 */
func addMulti(tab *RouteTable,rest *map[string]*Node)  {
	addNode(tab.Nodes,rest)
	addMultiLay(tab,rest)
}
/**
这个是当前这一层的nodes不需要添加的 全部添加方法
 */
func addMultiLay(tab *RouteTable,rest *map[string]*Node)  {
	for _, v := range *tab.Children{
		addMulti(v,rest)
	}
}
/**
添加 + 主题的
 */
func addSingle(tab *RouteTable, topics []string,rest *map[string]*Node)  {
	lens := len(topics)
	if lens == 1{
		if topics[0] == SingleLevel || topics[0] == MultiLevel{
			// 只要把当前层所有都添加
			for _,v := range *tab.Children {
				for k1, v1 := range *v.Nodes {
					(*rest)[k1] = v1
				}
			}
		}else {
			// 添加 topics[0] 、+ 、# 的node
			if v, ok := (*tab.Children)[topics[0]]; ok {
				addNode(v.Nodes,rest)
			}
			if topics[0] != SingleLevel {
				if v, ok := (*tab.Children)[SingleLevel]; ok {
					addNode(v.Nodes,rest)
				}
			}
			if topics[0] != MultiLevel{
				if v, ok := (*tab.Children)[MultiLevel]; ok {
					addNode(v.Nodes,rest)
				}
			}
		}
	}else{
		if topics[0] == SingleLevel{
			// 其它的看后面是否符合
			for k, v := range *tab.Children {
				// 把该层 # 下的node加上
				if k == MultiLevel{
					addNode(v.Nodes,rest)
				}else{
					//else if k == topics[1]{
					//	// 看后一层,继续递归搜索
					//	addSingle(v,topics[1:],rest)
					//}
					v1 := v
					addSingle(v1,topics[1:],rest)
				}
			}
		}else if topics[0] == MultiLevel{
			 addMulti(tab,rest)
		}else {
			// 从 + 号后面查找
			if v, ok := (*tab.Children)[SingleLevel]; ok{
				addSingle(v,topics[1:],rest)
			}
			// 从 topics[0] 后面查找
			if v, ok := (*tab.Children)[topics[0]]; ok{
				addSingle(v,topics[1:],rest)
			}
		}
	}
}
/**
添加其它主题
 */
func addOther(tab *RouteTable, topics []string,rest *map[string]*Node)  {
	lens := len(topics)
	if lens == 1 {
		// 添加 topics[0] 、+ 、# 的node

		if topics[0] == SingleLevel {
			for _, v := range *tab.Children{
				v1 := v
				addNode(v1.Nodes,rest)
			}
		}else if topics[0] == MultiLevel{
			for _, v := range *tab.Children{
				v1 := v
				addMulti(v1,rest)
			}
		}else {
			if v, ok := (*tab.Children)[topics[0]]; ok {
				addNode(v.Nodes,rest)
			}
			if v, ok := (*tab.Children)[SingleLevel]; ok {
				addNode(v.Nodes,rest)
			}
			if v, ok := (*tab.Children)[MultiLevel]; ok {
				addNode(v.Nodes,rest)
			}
		}
		return
	}
	if topics[0] == MultiLevel{
		addMulti(tab,rest)
	}else if topics[0] == SingleLevel{
		addSingle(tab,topics,rest)
	}else {
		if v, ok := (*tab.Children)[topics[0]]; ok{
			addOther(v,topics[1:],rest)
		}
		if v, ok := (*tab.Children)[SingleLevel]; ok{
			addOther(v,topics[1:],rest)
		}
		if v, ok := (*tab.Children)[MultiLevel]; ok{
			addMulti(v,rest)
		}
	}
}
func addNode(nodes *map[string]*Node ,rest *map[string]*Node)  {
	for k, v := range *nodes{
		v1 := v
		k1 := k
		(*rest)[k1] = v1
	}
}

三 、测试


import (
	"fmt"
	"study_one_day/gotcp/topic"
)

func main() {
	s := "123456789"
	for _, i := range s {
		_, _ = topic.RouteTables.AddNodeInTopic("a/b/"+string(i), &topic.Node{
			Ip:   "127.0.0.1",
			Port: "8080",
			Name: "test" + string(i),
		})
	}
	_, _ = topic.RouteTables.AddNodeInTopic("a/+/c", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "test+3",
	})
	_, _ = topic.RouteTables.AddNodeInTopic("a/+/#", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "test+2",
	})
	_, _ = topic.RouteTables.AddNodeInTopic("a/+/+/c", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "test+",
	})
	_, _ = topic.RouteTables.AddNodeInTopic("a/b/e", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "test+e",
	})
	_, _ = topic.RouteTables.AddNodeInTopic("c/#", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "test#",
	})
	_, _ = topic.RouteTables.AddNodeInTopic("a/#", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "test##",
	})
	_, _ = topic.RouteTables.AddNodeInTopic("c/+/2", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "##",
	})
	_, _ = topic.RouteTables.AddNodeInTopic("a/2/c", &topic.Node{
		Ip:   "127.0.0.1",
		Port: "8080",
		Name: "123123",
	})
	if rest, err := topic.RouteTables.FindNodeByTopic("a/b/#");err == nil{
		fmt.Printf("%#v",rest)
	}
	fmt.Println()
}

在这里插入图片描述

四、总结

如果发现 bug ,希望各位看官告诉我一下,小弟看着自己写的代码有点迷了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值