frp源码剖析-frp中的mux模块
前言
frp几乎所有的连接处理都是构建在mux模块之上的,重要性不必多说,来看一下这是个啥吧
ps: 安装方法
go get "github.com/fatedier/golib/net/mux"
该模块很小,不到300行,分为两个文件:mux.go和rule.go。
因为rule.go文件相对简单一些,我们先来看这个。
role.go文件
首先看其中所命名的函数类型MatchFunc:
type MatchFunc func(data []byte) (match bool)
该类型的函数用来判断data属于什么协议。
那么具体如何判断呢,这里也实现了三个例子:
var (
	HttpsNeedBytesNum uint32 = 1
	HttpNeedBytesNum  uint32 = 3
	YamuxNeedBytesNum uint32 = 2
)
var HttpsMatchFunc MatchFunc = func(data []byte) bool {
	if len(data) < int(HttpsNeedBytesNum) {
		return false
	}
	if data[0] == 0x16 {
		return true
	} else {
		return false
	}
}
// From https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
var httpHeadBytes = map[string]struct{}{
	"GET": struct{}{},
	"HEA": struct{}{},
	"POS": struct{}{},
	"PUT": struct{}{},
	"DEL": struct{}{},
	"CON": struct{}{},
	"OPT": struct{}{},
	"TRA": struct{}{},
	"PAT": struct{}{},
}
var HttpMatchFunc MatchFunc = func(data []byte) bool {
	if len(data) < int(HttpNeedBytesNum) {
		return false
	}
	_, ok := httpHeadBytes[string(data[:3])]
	return ok
}
// From https://github.com/hashicorp/yamux/blob/master/spec.md
var YamuxMatchFunc MatchFunc = func(data []byte) bool {
	if len(data) < int(YamuxNeedBytesNum) {
		return false
	}
	if data[0] == 0 && data[1] >= 0x0 && data[1] <= 0x3 {
		return true
	}
	return false
}
这三个函数分别实现了区分HTTPS,HTTP以及go中特有的yamux(实际上这是一个库,可以参考Go中的I/O多路复用)。
mux.go文件
先来看其中的struct,第一个是Mux第二个是listener,这里先来看一下较为简单的listener。
listener结构体
type listener struct {
	mux *Mux
	priority     int
	needBytesNum uint32
	matchFn      MatchFunc
	c  chan net.Conn
	mu sync.RWMutex
}
// Accept waits for and returns the next connection to the listener.
func (ln *listener) Accept() (net.Conn, error) {
	...
}
// Close removes this listener from the parent mux and closes the channel.
func (ln *listener) Close() error {
	...
}
func (ln *listener) Addr() net.Addr {
	...
}
刚看到这个结构体我们可能很迷惑,不知道都是干啥的,而且网络编程中一般listener这种东西要绑定在一个套接字上,但很明显listener没有,不过其唯一跟套接字相关的可能是其c字段,其是一个由net包中的Conn接口组成的chanel;然后mu字段就是读写锁了,这个很简单;然后mux字段则是上面提到的两个结构体中的另一个结构体Mux的指针;接下来到了priority字段上,顾名思义,这个似乎跟优先级有关系,暂且存疑;needBytesNum则更有些蒙了,不过感觉其是跟读取byte的数量有关系,最后是matchFn。
好,初步认识了这个结构体的结构后,我们看看其方法。三个方法的listener实现了net模块中的Listener接口:
// A Listener is a generic network listener for stream-oriented protocols.
//
// Multiple goroutines may invoke methods on a Listener simultaneously.
type Listener interface {
	// Accept waits for and returns the next connection to the listener.
	Accept() (Conn, error)
	// Close closes the listener.
	// Any blocked Accept operations will be unblocked and return errors.
	Close() error
	// Addr returns the listener's network address.
	Addr() Addr
}
然后先来分析其Accept方法:
func (ln *listener) Accept() (net.Conn, error) {
	conn, ok := <-ln.c
	if !ok {
		return nil, fmt.Errorf("network connection closed")
	}
	return conn, nil
}
该方法很简单,就是从c这个由Conn组成的channel中,获取Conn对象,好这里我们就明白了,这个listener和普通的不一样,他很特别,普通的listener监听的是套接字,而他监听的是channel,另外,肯定有某个地方在不停的往c这个channel中放Conn。
接下来是Close方法:
func (ln *listener) Close() error {
	if ok := ln.mux.release(ln); ok {
		// Close done to signal to any RLock holders to release their lock.
		close(ln.c)
	}
	return nil
}
我们暂且先把这个ln.mux.release(ln)放到一边,因为还不知道这个东西干了啥,暂且只需关注close(ln.c),我们知道这个函数是用来关闭channel的,go推荐由发送端调用,但这里似乎listener是一个消费端,可以看一下如何优雅的关闭Go Channel,看来重点在于ln.mux.release(ln)这里,我们暂且存疑[1],留待下面解决。
最后是Addr方法:
func (ln *listener) Addr() net.Addr {
	if ln.mux == nil {
		return nil
	}
	ln.mux.mu.RLock()
	defer ln.mux.mu.RUnlock()
	if ln.mux.ln == nil {
		return nil
	}
	return ln.mux.ln.Addr()
}
在这里,mu字段就用上了,加读锁,然后返回mux字段中的ln字段的Addr方法。也就是这句return ln.mux.ln.Addr()。
Mux结构体
字段以及相关函数
Mux结构体则相对来说复杂很多,先来看一下他的字段定义:
type Mux struct {
	ln net.Listener
	defaultLn *listener
	// sorted by priority
	lns             []*listener
	maxNeedBytesNum uint32
	mu sync.RWMutex
}
好,第一个字段ln是一个Listener接口;然后defaultLn是一个listener的指针;lns则是由listener的指针组成的切片,根据注释// sorted by priority,我们终于知道listener的priority字段是干啥的了;接下来是maxNeedBytesNum字段,好奇怪,比起listener的needBytesNum多了个“Max”,所以我们推测这个值取得是lns以及defaultLn字段中所有listener中needBytesNum值最大的;最后的mu字段我们就不说了。
需要注意的是:我们可能会发现Mux和listener存在相互引用,但在Go中我们倒也不用太担心,因为Go采用“标记-回收”或者其变种的垃圾回收算法,感兴趣可以参考Golang 垃圾回收剖析
在mux.go文件中定义了Mux的生成函数NewMux:
func NewMux(ln net.Listener) (mux *Mux) {
	mux = &Mux{
		ln:  ln,
		lns: make([]*listener, 0),
	}
	return
}
很简单,需要注意的是ln字段存储的一般不是listener这样的非常规Listener,一般是TCPListener这样具体的绑定了套接字的监听器。
Mux方法
接下来看Mux结构体的方法,首先看Listen和copyLns
// priority
func (mux *Mux) Listen(priority int, needBytesNum uint32, fn MatchFunc) net.Listener {
    // 1
	ln := &listener{
		c:            make(chan net.Conn),
		mux:          mux,
		priority:     priority,
		needBytesNum: needBytesNum,
		matchFn:      fn,
	}
	mux.mu.Lock()
	defer mux.mu.Unlock()
	// 2
	if needBytesNum > mux.maxNeedBytesNum {
		mux.maxNeedBytesNum = needBytesNum
	}
    // 3
	newlns := append(mux.copyLns(), ln)
	sort.Slice(newlns, func(i, j int) bool {
		if newlns[i].priority == newlns[j].priority {
			return newlns[i].needBytesNum < newlns[j].needBytesNum
		}
		return newlns[i].priority < newlns[j].priority
	})
	mux.lns = newlns
	return ln
}
func (mux *Mux) copyLns() []*listener {
	lns := make([]*listener, 0, len(mux.lns))
	for _, l := range mux.lns {
		lns = append(lns, l)
	}
	return lns
}
copyLns方法很简单,就是跟名字的含义一样,生成一个lns字段的副本并返回。
Listen基本做了三步:
- 生成一个
listener结构体实例,并获取互斥锁 - 根据情况更新
needBytesNum字段 - 将新生成的
listener实例按照优先级放入lns字段对应的slice中 
接下来是ListenHttp和ListenHttps方法:
func (mux *Mux) ListenHttp(priority int) net.Listener {
	return mux.Listen(priority, HttpNeedBytesNum, HttpMatchFunc)
}
func (mux *Mux) ListenHttps(priority int) net.Listener {
	return mux.Listen(priority, HttpsNeedBytesNum, HttpsMatchFunc)
}
这两个差不多,所以放到一起说,基本都是专门写了一个方法让我们能方便的创建处理Http或者Https的listener。
再来看DefaultListener方法:
func (mux *Mux) DefaultListener() net.Listener {
	mux.mu.Lock()
	defer mux.mu.Unlock()
	if mux.defaultLn == nil {
		mux.defaultLn = &listener{
			c:   make(chan net.Conn),
			mux: mux,
		}
	}
	return mux.defaultLn
}
这个方法很简单,基本就是有则返回没有则生成然后返回的套路。不过我们要注意defaultLn字段中的listener是不放入lns字段中的。
接下来是Server方法:
// Serve handles connections from ln and multiplexes then across registered listeners.
func (mux *Mux) Serve() error {
	for {
		// Wait for the next connection.
		// If it returns a temporary error then simply retry.
		// If it returns any other error then exit immediately.
		conn, err := mux.ln.Accept()
		if err, ok := err.(interface {
			Temporary() bool
		}); ok && err.Temporary() {
			continue
		}
		if err != nil {
			return err
		}
		go mux.handleConn(conn)
	}
}
一般来说,当我们调用NewMux函数以后,接下来就会调用Server方法,该方法基本上就是阻塞监听某个套接字,当有连接建立成功后立即另起一个goroutine调用handleConn方法;当连接建立失败根据err是否含有Temporary方法,如果有则执行并忽略错误,没有则返回错误。
现在我们看看handleConn方法干了些啥:
func (mux *Mux) handleConn(conn net.Conn) {
    // 1
	mux.mu.RLock()
	maxNeedBytesNum := mux.maxNeedBytesNum
	lns := mux.lns
	defaultLn := mux.defaultLn
	mux.mu.RUnlock()
    // 2
	sharedConn, rd := gnet.NewSharedConnSize(conn, int(maxNeedBytesNum))
	data := make([]byte, maxNeedBytesNum)
	conn.SetReadDeadline(time.Now().Add(DefaultTimeout))
	_, err := io.ReadFull(rd, data)
	if err != nil {
		conn.Close()
		return
	}
	conn.SetReadDeadline(time.Time{})
    // 3
	for _, ln := range lns {
		if match := ln.matchFn(data); match {
			err = errors.PanicToError(func() {
				ln.c <- sharedConn
			})
			if err != nil {
				conn.Close()
			}
			return
		}
	}
	// No match listeners
	if defaultLn != nil {
		err = errors.PanicToError(func() {
			defaultLn.c <- sharedConn
		})
		if err != nil {
			conn.Close()
		}
		return
	}
	// No listeners for this connection, close it.
	conn.Close()
	return
}
handleConn方法也不算复杂,大体可以分为三步:
- 获取当前状态
 - 从
conn中读取数据,注意:shareConn和rd存在单向关系,如果从rd中读取数据的话,数据也会复制一份放到shareConn中,反过来就不成立了 - 读取到的数据会被遍历,最终选出
与matchFunc匹配的最高优先级的listener,并将shareConn放入该listener的c字段中,如果没有匹配到则放到defaultLn中的c字段中,如果defaultLn是nil的话就不处理,直接关闭conn。 
最后来到了release方法了:
func (mux *Mux) release(ln *listener) bool {
	result := false
	mux.mu.Lock()
	defer mux.mu.Unlock()
	lns := mux.copyLns()
	for i, l := range lns {
		if l == ln {
			lns = append(lns[:i], lns[i+1:]...)
			result = true
			break
		}
	}
	mux.lns = lns
	return result
}
release方法意思很明确:把对应的listener从lns中移除,并把结果返回,整个过程有互斥锁,我们回到存疑1,尽管有互斥锁,但在这种情况下:当某个goroutine运行到handleConn已经执行到了第三阶段的开始状态(也就是还没有找到匹配的listener)时,且Go运行在多核状态下,当另一个goroutine运行完listener的Close方法时,这时就可能发生往一个已经关闭的channel中send数据,但请注意handleConn的第三步的这段代码:
err = errors.PanicToError(func() { // 就是这里了
	ln.c <- sharedConn
})
if err != nil {
	conn.Close()
}
这个PanicToError是这样的:
func PanicToError(fn func()) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("Panic error: %v", r)
		}
	}()
	fn()
	return
}
基本上就是执行了recover然后将错误打印出来,结合下面的对err的判断,就会将send失败的conn关闭。
总结
Mux中包含了一个初始监听器,基本上所有的事件(比如说新的连接建立,之所以叫事件是因为我实在想不出更精确的词语了)都起源于此listener实现了net.Listener接口,可以作为二级监听器使用(比如传给net/http.Server结构体的Server方法进行处理)。Mux包含了一个由listener组成的有序slice,当有事件产生时就会遍历这个slice找出合适的listener并将事件传给他。
讲到这里基本上是完事了。整个mux模块还是比较简单的,起码是由一个个简单的东西组合而成。那么一起来意淫一下整体流程吧。
假如我要实现这么一个网络程序:
- 绑定监听一个基于tcp的套接字
 - 我们允许其应用层可支持多个(比如说支持http https这两个吧,尽管http和https可以说是一个协议。。),不同的应用层协议对应不同的处理函数
 
就这么两个很简单的要求,不难吧。
那么我们一起来实现吧:
type HandleFunc func(c net.Conn) (n int, err error) 
type MyServer struct {
    l net.Listener
    hFunc HandleFunc
}
func (h *MyServer) Server() (err error) {
    for {
        conn, err := h.l.Accept()
        if err != nil {
            return
        }
        go h.hFunc(conn)
    }
}
func HandleHttp(c net.Conn)(n int, err error){
    n, err = c.Write([]byte("Get Off! Don't you know that it is not safe?"))
}
func HandleHttps(c net.Conn)(n int, err error){
    n, err = c.Write([]byte("Get Off! Don't you know that this is more complicated than http?"))
}
func main() (err error){
    ln, err := net.Listen("tcp", "0.0.0.0:12345")
	if err != nil {
		err = fmt.Errorf("Create server listener error, %v", err)
		return
	}
	muxer = mux.NewMux(ln)
	var lHttp, lHttps net.Listener
	lHttp = muxer.ListenHttp(1)
	httpServer := *MyServer{lHttp, HandleHttp}
	lHttps = muxer.ListenHttps(2)
	httpsServer := *MyServer{lHttps, HandleHttps}
	go httpServer.Server()
	go httpsServer.Server()
	err = muxer.Serve()
}
												
											frp源码剖析-frp中的mux模块的更多相关文章
- frp源码剖析-frp中的log模块
		
前言&引入 一个好的log模块可以帮助我们排错,分析,统计 一般来说log中需要有时间.栈信息(比如说文件名行号等),这些东西一般某些底层log模块已经帮我们做好了.但在业务中还有很多我们需要 ...
 - 跨站请求伪造(csrf),django的settings源码剖析,django的auth模块
		
目录 一.跨站请求伪造(csrf) 1. 什么是csrf 2. 钓鱼网站原理 3. 如何解决csrf (1)思路: (2)实现方法 (3)实现的具体代码 3. csrf相关的装饰器 (1)csrf_p ...
 - Django对中间件的调用思想、csrf中间件详细介绍、Django settings源码剖析、Django的Auth模块
		
目录 使用Django对中间件的调用思想完成自己的功能 功能要求 importlib模块介绍 功能的实现 csrf中间件详细介绍 跨站请求伪造 Django csrf中间件 form表单 ajax c ...
 - STL源码剖析读书笔记之vector
		
STL源码剖析读书笔记之vector 1.vector概述 vector是一种序列式容器,我的理解是vector就像数组.但是数组有一个很大的问题就是当我们分配 一个一定大小的数组的时候,起初也许我们 ...
 - 《STL源码剖析》学习之traits编程
		
侯捷老师在<STL源码剖析>中说到:了解traits编程技术,就像获得“芝麻开门”的口诀一样,从此得以一窥STL源码的奥秘.如此一说,其重要性就不言而喻了. 之前已经介绍过迭代器 ...
 - 【Python源码剖析】对象模型概述
		
Python 是一门 面向对象 语言,实现了一个完整的面向对象体系,简洁而优雅. 与其他面向对象编程语言相比, Python 有自己独特的一面. 这让很多开发人员在学习 Python 时,多少有些无所 ...
 - petite-vue源码剖析-逐行解读@vue-reactivity之effect
		
当我们通过effect将副函数向响应上下文注册后,副作用函数内访问响应式对象时即会自动收集依赖,并在相应的响应式属性发生变化后,自动触发副作用函数的执行. // ./effect.ts export ...
 - 《python解释器源码剖析》第13章--python虚拟机中的类机制
		
13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象 ...
 - 《python解释器源码剖析》第12章--python虚拟机中的函数机制
		
12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...
 
随机推荐
- Flask、Celery、RabbitMQ学习计划
			
Flask (9.16-9.23) 相关组件了解 (9.16-17) WSGI:Werkzeug 数据库:SQLAlchemy *重点查看 urls和视图 (9.18-19) session和co ...
 - OSG中距离转像素公式(PIXEL_SIZE_ON_SCREEN)
			
osgearth_computerangecallback.cpp 中 下面的代码假设:range模式是PIXEL_SIZE_ON_SCREEN,根据距视点的距离计算在屏幕中的像素大小. 像素大小转距 ...
 - Genymotion安装总结
			
周末的时候为了测试论文中的Almond虚拟助手软件,所以要去Google Play上去下载. 但是我的两个安卓模拟器:夜神和海马玩模拟器的安卓版本太低了,导致无法使用 谷歌服务,所以连商店都进不去. ...
 - [自学]Docker system 命令 查看docker镜像磁盘占用情况 Docker volume 相关
			
内容From https://docs.docker.com/engine/reference/commandline/system_df/ docker的image和docker的container ...
 - HTTP协议整理
			
一.概念 1.HTTP协议:即超文本传输协议(Hypertext transfer protocol).是一种详细规定了浏览器和Web服务器之间互相通信的规则,它允许将超文本标记语言(HTML)文档从 ...
 - 学习笔记之csrf
			
CBV 添加 csrf 第一种: 指定方法方面添加 装饰器 @memethod_decorator(xxx) 第二种 全部添加: 注意 在类名前: @method_decorator(xxx,name ...
 - BZOJ5415[Noi2018]归程——kruskal重构树+倍增+堆优化dijkstra
			
题目描述 本题的故事发生在魔力之都,在这里我们将为你介绍一些必要的设定. 魔力之都可以抽象成一个 n 个节点.m 条边的无向连通图(节点的编号从 1 至 n).我们依次用 l,a 描述一条边的长度.海 ...
 - BZOJ1926[Sdoi2010]粟粟的书架——二分答案+主席树
			
题目描述 幸福幼儿园 B29 班的粟粟是一个聪明机灵.乖巧可爱的小朋友,她的爱好是画画和读书,尤其喜欢 Thomas H. Co rmen 的文章.粟粟家中有一个 R行C 列的巨型书架,书架的每一个位 ...
 - ubuntu16.04 NFS系统挂载
			
一:服务器端 step1:关闭防火墙 sudo ufw disable step2:安装nfs sudo apt-get install nfs-kernel-server step3: 打开/etc ...
 - MySQL将一张表的某些列数据,复制到另外一张表,并且修改某些内容
			
MySQL将一张表的某些列数据,复制到另外一张表 INSERT INTO t_topic_content(content,topicId) SELECT content,id FROM t_topi ...