本文通過回顧 teleport (https://github.com/henrylee2cn/teleport) 框架的開發(fā)過程,講述Go Socket的開發(fā)實(shí)戰(zhàn)經(jīng)驗(yàn)。
本文的內(nèi)容組織形式:teleport架構(gòu)源碼賞析+相應(yīng)Go技巧分享
期間,我們可以分別從這兩條線進(jìn)行思考與探討。
文中以TP
作為teleport
的簡(jiǎn)稱
文中內(nèi)容針對(duì)具有一定Go語(yǔ)言基礎(chǔ)的開發(fā)者
文中以Go技巧
是指高于語(yǔ)法常識(shí)的一些編程技巧、設(shè)計(jì)模式
為了壓縮篇幅,代碼塊中刪除了一些空行,并使用...
表示省略行
目 錄 |
---|
TP性能測(cè)試 |
第一部分 TP架構(gòu)設(shè)計(jì) |
第二部分 TP關(guān)鍵源碼賞析及相關(guān)Go技巧 |
TP性能測(cè)試
TP與其他使用長(zhǎng)連接的框架的性能對(duì)比:
測(cè)試用例
- 一個(gè)服務(wù)端與一個(gè)客戶端進(jìn)程,在同一臺(tái)機(jī)器上運(yùn)行
- CPU: Intel Xeon E312xx (Sandy Bridge) 16 cores 2.53GHz
- Memory: 16G
- OS: Linux 2.6.32-696.16.1.el6.centos.plus.x86_64, CentOS 6.4
- Go: 1.9.2
- 信息大小: 581 bytes
- 信息編碼:protobuf
- 發(fā)送 1000000 條信息
測(cè)試結(jié)果
- teleport
并發(fā)client | 平均值(ms) | 中位數(shù)(ms) | 最大值(ms) | 最小值(ms) | 吞吐率(TPS) |
---|---|---|---|---|---|
100 | 1 | 0 | 16 | 0 | 75505 |
500 | 9 | 11 | 97 | 0 | 52192 |
1000 | 19 | 24 | 187 | 0 | 50040 |
2000 | 39 | 54 | 409 | 0 | 42551 |
5000 | 96 | 128 | 1148 | 0 | 46367 |
- teleport/socket
并發(fā)client | 平均值(ms) | 中位數(shù)(ms) | 最大值(ms) | 最小值(ms) | 吞吐率(TPS) |
---|---|---|---|---|---|
100 | 0 | 0 | 14 | 0 | 225682 |
500 | 2 | 1 | 24 | 0 | 212630 |
1000 | 4 | 3 | 51 | 0 | 180733 |
2000 | 8 | 6 | 64 | 0 | 183351 |
5000 | 21 | 18 | 651 | 0 | 133886 |
- 與rpcx的對(duì)比
并發(fā)client | 平均值(ms) | 中位數(shù)(ms) | 最大值(ms) | 最小值(ms) | 吞吐率(TPS) |
---|---|---|---|---|---|
100 | 0 | 0 | 50 | 0 | 109217 |
500 | 5 | 4 | 50 | 0 | 88113 |
1000 | 11 | 10 | 1040 | 0 | 87535 |
2000 | 23 | 29 | 3080 | 0 | 80886 |
5000 | 59 | 72 | 7111 | 0 | 78412 |
- rpcx與其他框架的對(duì)比參考(圖片來源于rpcx)
- CPU耗時(shí)火焰圖 teleport/socket
- 堆棧信息火焰圖 teleport/socket
第一部分 TP架構(gòu)設(shè)計(jì)
1 設(shè)計(jì)理念
TP定位于提供socket通信解決方案,遵循以下三點(diǎn)設(shè)計(jì)理念。
- 通用:不做定向深入,專注長(zhǎng)連接通信
- 高效:高性能,低消耗
- 靈活:用法靈活簡(jiǎn)單,易于深入定制
- 可靠:使用接口(
interface
)而非約束說明,規(guī)定框架用法
2 架構(gòu)圖
-
Peer
: 通信端點(diǎn),可以是服務(wù)端或客戶端 -
Plugin
: 貫穿于通信各個(gè)環(huán)節(jié)的插件 -
Handler
: 用于處理推、拉請(qǐng)求的函數(shù) -
Router
: 通過請(qǐng)求信息(如URI)索引響應(yīng)函數(shù)(Handler)的路由器 -
Socket
: 對(duì)net.Conn的封裝,增加自定義包協(xié)議、傳輸管道等功能 -
Session
: 基于Socket封裝的連接會(huì)話,提供的推、拉、回復(fù)、關(guān)閉等會(huì)話操作 -
Context
: 連接會(huì)話中一次通信(如PULL-REPLY, PUSH)的上下文對(duì)象 -
Packet
: 約定數(shù)據(jù)報(bào)文包含的內(nèi)容元素(注意:它不是協(xié)議格式) -
Protocol
: 數(shù)據(jù)報(bào)文封包解包操作,即通信協(xié)議的實(shí)現(xiàn)接口 -
Codec
: 數(shù)據(jù)包body部分(請(qǐng)求參數(shù)或響應(yīng)結(jié)果)的序列化接口 -
XferPipe
: 數(shù)據(jù)包字節(jié)流的編碼處理管道,如壓縮、加密、校驗(yàn)等
3 重要特性
- 支持自定義通信協(xié)議和包數(shù)據(jù)處理管道
- 使用I/O緩沖區(qū)與多路復(fù)用技術(shù),提升數(shù)據(jù)吞吐量
- 支持設(shè)置讀取包的大小限制(如果超出則斷開連接)
- 支持插件機(jī)制,可以自定義認(rèn)證、心跳、微服務(wù)注冊(cè)中心、統(tǒng)計(jì)信息插件等
- 服務(wù)端和客戶端之間對(duì)等通信,統(tǒng)一為peer端點(diǎn),具有基本一致的用法:
- 推、拉、回復(fù)等通信方法
- 豐富的插件掛載點(diǎn),可以自定義認(rèn)證、心跳、微服務(wù)注冊(cè)中心、統(tǒng)計(jì)信息等等
- 平滑重啟與關(guān)閉
- 日志信息詳盡,支持打印輸入、輸出消息的詳細(xì)信息(狀態(tài)碼、消息頭、消息體)
- 支持設(shè)置慢操作報(bào)警閾值
- 提供Hander的上下文(pull、push的handler)
- 客戶端的Session支持?jǐn)嗑€后自動(dòng)重連
- 支持的網(wǎng)絡(luò)類型:
tcp
、tcp4
、tcp6
、unix
、unixpacket
等
第二部分 TP關(guān)鍵源碼賞析及相關(guān)Go技巧
---------------------------以下為github.com/henrylee2cn/telepot/socket
包內(nèi)容---------------------------
1 Packet統(tǒng)一數(shù)據(jù)包元素
Packet
結(jié)構(gòu)體用于定義統(tǒng)一的數(shù)據(jù)包內(nèi)容元素,為上層架構(gòu)提供穩(wěn)定、統(tǒng)一的操作API。
$ Go技巧分享
1.?在teleport/socket
目錄下執(zhí)行go doc Packet
命令,我們可以獲得以下關(guān)于Packet
的定義、函數(shù)與方法:
type Packet struct {
// Has unexported fields.
}
Packet a socket data packet.
func GetPacket(settings ...PacketSetting) *Packet
func NewPacket(settings ...PacketSetting) *Packet
func (p *Packet) AppendXferPipeFrom(src *Packet)
func (p *Packet) Body() interface{}
func (p *Packet) BodyCodec() byte
func (p *Packet) MarshalBody() ([]byte, error)
func (p *Packet) Meta() *utils.Args
func (p *Packet) Ptype() byte
func (p *Packet) Reset(settings ...PacketSetting)
func (p *Packet) Seq() uint64
func (p *Packet) SetBody(body interface{})
func (p *Packet) SetBodyCodec(bodyCodec byte)
func (p *Packet) SetNewBody(newBodyFunc NewBodyFunc)
func (p *Packet) SetPtype(ptype byte)
func (p *Packet) SetSeq(seq uint64)
func (p *Packet) SetSize(size uint32) error
func (p *Packet) SetUri(uri string)
func (p *Packet) Size() uint32
func (p *Packet) String() string
func (p *Packet) UnmarshalBody(bodyBytes []byte) error
func (p *Packet) UnmarshalNewBody(bodyBytes []byte) error
func (p *Packet) Uri() string
func (p *Packet) XferPipe() *xfer.XferPipe
2.?Packet
全部字段均不可導(dǎo)出,可以增強(qiáng)代碼穩(wěn)定性以及對(duì)其操作的掌控力
3.?下面是由Packet
結(jié)構(gòu)體實(shí)現(xiàn)的兩個(gè)接口Header
和Body
。思考:為什么不直接使用Packet
或者定義兩個(gè)子結(jié)構(gòu)體?
- 使用接口可以達(dá)到限制調(diào)用方法的目的,不同情況下使用不同方法集,開發(fā)者不會(huì)因?yàn)檎{(diào)用了不該調(diào)用的方法而掉坑里
- 在語(yǔ)義上,
Packet
只是用于定義統(tǒng)一的數(shù)據(jù)包內(nèi)容元素,并未給予任何關(guān)于數(shù)據(jù)結(jié)構(gòu)方面(協(xié)議)的暗示、誤導(dǎo)。因此不應(yīng)該使用子結(jié)構(gòu)體
type (
// packet header interface
Header interface {
// Ptype returns the packet sequence
Seq() uint64
// SetSeq sets the packet sequence
SetSeq(uint64)
// Ptype returns the packet type, such as PULL, PUSH, REPLY
Ptype() byte
// Ptype sets the packet type
SetPtype(byte)
// Uri returns the URL string string
Uri() string
// SetUri sets the packet URL string
SetUri(string)
// Meta returns the metadata
Meta() *utils.Args
}
// packet body interface
Body interface {
// BodyCodec returns the body codec type id
BodyCodec() byte
// SetBodyCodec sets the body codec type id
SetBodyCodec(bodyCodec byte)
// Body returns the body object
Body() interface{}
// SetBody sets the body object
SetBody(body interface{})
// SetNewBody resets the function of geting body.
SetNewBody(newBodyFunc NewBodyFunc)
// MarshalBody returns the encoding of body.
MarshalBody() ([]byte, error)
// UnmarshalNewBody unmarshal the encoded data to a new body.
// Note: seq, ptype, uri must be setted already.
UnmarshalNewBody(bodyBytes []byte) error
// UnmarshalBody unmarshal the encoded data to the existed body.
UnmarshalBody(bodyBytes []byte) error
}
// NewBodyFunc creates a new body by header.
NewBodyFunc func(Header) interface{}
)
4.?編譯期校驗(yàn)Packet
是否已實(shí)現(xiàn)Header
與Body
接口的技巧
var (
_ Header = new(Packet)
_ Body = new(Packet)
)
5.?一種常見的自由賦值的函數(shù)用法,用于自由設(shè)置Packet
的字段
// PacketSetting sets Header field.
type PacketSetting func(*Packet)
// WithSeq sets the packet sequence
func WithSeq(seq uint64) PacketSetting {
return func(p *Packet) {
p.seq = seq
}
}
// Ptype sets the packet type
func WithPtype(ptype byte) PacketSetting {
return func(p *Packet) {
p.ptype = ptype
}
}
...
2 Socket接口
Socket
接口是對(duì)net.Conn
的封裝,通過協(xié)議接口Proto
對(duì)數(shù)據(jù)包內(nèi)容元素Packet
進(jìn)行封包、解包與IO傳輸操作。
type (
// Socket is a generic stream-oriented network connection.
//
// Multiple goroutines may invoke methods on a Socket simultaneously.
Socket interface {
net.Conn
// WritePacket writes header and body to the connection.
// Note: must be safe for concurrent use by multiple goroutines.
WritePacket(packet *Packet) error
// ReadPacket reads header and body from the connection.
// Note: must be safe for concurrent use by multiple goroutines.
ReadPacket(packet *Packet) error
// Public returns temporary public data of Socket.
Public() goutil.Map
// PublicLen returns the length of public data of Socket.
PublicLen() int
// Id returns the socket id.
Id() string
// SetId sets the socket id.
SetId(string)
// Reset reset net.Conn and ProtoFunc.
Reset(netConn net.Conn, protoFunc ...ProtoFunc)
}
socket struct {
net.Conn
protocol Proto
id string
idMutex sync.RWMutex
ctxPublic goutil.Map
mu sync.RWMutex
curState int32
fromPool bool
}
)
$ Go技巧分享
1.?為什么要對(duì)外提供接口,而不直接公開結(jié)構(gòu)體?
socket
結(jié)構(gòu)體通過匿名字段net.Conn
的方式“繼承”了底層的連接操作方法,并基于該匿名字段創(chuàng)建了協(xié)議對(duì)象。
所以不能允許外部直接通過socket.Conn=newConn
的方式改變連接句柄。
使用Socket
接口封裝包外不可見的socket
結(jié)構(gòu)體可達(dá)到避免外部直接修改字段的目的。
2.?讀寫鎖遵循最小化鎖定的原則,且defer
絕不是必須的,在確定運(yùn)行安全的情況下盡量避免使用有性能消耗的defer
。
func (s *socket) ReadPacket(packet *Packet) error {
s.mu.RLock()
protocol := s.protocol
s.mu.RUnlock()
return protocol.Unpack(packet)
}
3 Proto協(xié)議接口
Proto
接口按照實(shí)現(xiàn)它的具體規(guī)則,對(duì)Packet
數(shù)據(jù)包內(nèi)容元素進(jìn)行封包、解包、IO等操作。
type (
// Proto pack/unpack protocol scheme of socket packet.
Proto interface {
// Version returns the protocol's id and name.
Version() (byte, string)
// Pack pack socket data packet.
// Note: Make sure to write only once or there will be package contamination!
Pack(*Packet) error
// Unpack unpack socket data packet.
// Note: Concurrent unsafe!
Unpack(*Packet) error
}
// ProtoFunc function used to create a custom Proto interface.
ProtoFunc func(io.ReadWriter) Proto
// FastProto fast socket communication protocol.
FastProto struct {
id byte
name string
r io.Reader
w io.Writer
rMu sync.Mutex
}
)
$ Go技巧分享
1.?將數(shù)據(jù)包的封包、解包操作封裝為Proto
接口,并定義一個(gè)默認(rèn)實(shí)現(xiàn)(FastProto
)。
這是框架設(shè)計(jì)中增強(qiáng)可定制性的一種有效手段。開發(fā)者既可以使用默認(rèn)實(shí)現(xiàn),也可以根據(jù)特殊需求定制自己的個(gè)性實(shí)現(xiàn)。
2.?使用Packet
屏蔽不同協(xié)議的差異性:封包時(shí)以Packet
的字段為內(nèi)容元素進(jìn)行數(shù)據(jù)序列化,解包時(shí)以Packet
為內(nèi)容模板進(jìn)行數(shù)據(jù)的反序列化。
---------------------------以下為github.com/henrylee2cn/telepot/codec
包內(nèi)容---------------------------
4 Codec編解碼
Codec
接口是socket.Packet.body
的編解碼器。TP已默認(rèn)注冊(cè)了JSON、Protobuf、String三種編解碼器。
type (
// Codec makes Encoder and Decoder
Codec interface {
// Id returns codec id.
Id() byte
// Name returns codec name.
Name() string
// Marshal returns the encoding of v.
Marshal(v interface{}) ([]byte, error)
// Unmarshal parses the encoded data and stores the result
// in the value pointed to by v.
Unmarshal(data []byte, v interface{}) error
}
)
$ Go技巧分享
1.?下面codecMap
變量的類型為什么不用關(guān)鍵字type
定義?
var codecMap = struct {
nameMap map[string]Codec
idMap map[byte]Codec
}{
nameMap: make(map[string]Codec),
idMap: make(map[byte]Codec),
}
Go語(yǔ)法允許我們?cè)诼暶髯兞繒r(shí)臨時(shí)定義類型并賦值。因?yàn)?code>codecMap所屬類型只會(huì)有一個(gè)全局唯一的實(shí)例,且不會(huì)用于其他變量類型聲明上,所以直接在聲明變量時(shí)聲明類型可以令代碼更簡(jiǎn)潔。
2.?常用的依賴注入實(shí)現(xiàn)方式,實(shí)現(xiàn)編解碼器的自由定制
const (
NilCodecId byte = 0
NilCodecName string = ""
)
func Reg(codec Codec) {
if codec.Id() == NilCodecId {
panic(fmt.Sprintf("codec id can not be %d", NilCodecId))
}
if _, ok := codecMap.nameMap[codec.Name()]; ok {
panic("multi-register codec name: " + codec.Name())
}
if _, ok := codecMap.idMap[codec.Id()]; ok {
panic(fmt.Sprintf("multi-register codec id: %d", codec.Id()))
}
codecMap.nameMap[codec.Name()] = codec
codecMap.idMap[codec.Id()] = codec
}
func Get(id byte) (Codec, error) {
codec, ok := codecMap.idMap[id]
if !ok {
return nil, fmt.Errorf("unsupported codec id: %d", id)
}
return codec, nil
}
func GetByName(name string) (Codec, error) {
codec, ok := codecMap.nameMap[name]
if !ok {
return nil, fmt.Errorf("unsupported codec name: %s", name)
}
return codec, nil
}
---------------------------以下為github.com/henrylee2cn/telepot/xfer
包內(nèi)容---------------------------
5 XferPipe數(shù)據(jù)編碼管道
XferPipe
接口用于對(duì)數(shù)據(jù)包進(jìn)行一系列自定義處理加工,如gzip壓縮、加密、校驗(yàn)等。
type (
// XferPipe transfer filter pipe, handlers from outer-most to inner-most.
// Note: the length can not be bigger than 255!
XferPipe struct {
filters []XferFilter
}
// XferFilter handles byte stream of packet when transfer.
XferFilter interface {
Id() byte
OnPack([]byte) ([]byte, error)
OnUnpack([]byte) ([]byte, error)
}
)
var xferFilterMap = struct {
idMap map[byte]XferFilter
}{
idMap: make(map[byte]XferFilter),
}
teleport/xfer
包的設(shè)計(jì)與teleport/codec
類似,xferFilterMap
為注冊(cè)中心,提供注冊(cè)、查詢、執(zhí)行等功能。
---------------------------以下為github.com/henrylee2cn/telepot
包內(nèi)容---------------------------
6 Peer通信端點(diǎn)
Peer結(jié)構(gòu)體是TP的一個(gè)通信端點(diǎn),它可以是服務(wù)端也可以是客戶端,甚至可以同時(shí)是服務(wù)端與客戶端。因此,TP是端對(duì)端對(duì)等通信的。
type Peer struct {
PullRouter *Router
PushRouter *Router
// Has unexported fields.
}
func NewPeer(cfg PeerConfig, plugin ...Plugin) *Peer
func (p *Peer) Close() (err error)
func (p *Peer) CountSession() int
func (p *Peer) Dial(addr string, protoFunc ...socket.ProtoFunc) (Session, *Rerror)
func (p *Peer) DialContext(ctx context.Context, addr string, protoFunc ...socket.ProtoFunc) (Session, *Rerror)
func (p *Peer) GetSession(sessionId string) (Session, bool)
func (p *Peer) Listen(protoFunc ...socket.ProtoFunc) error
func (p *Peer) RangeSession(fn func(sess Session) bool)
func (p *Peer) ServeConn(conn net.Conn, protoFunc ...socket.ProtoFunc) (Session, error)
- 通信端點(diǎn)介紹
1.?Peer配置信息
type PeerConfig struct {
TlsCertFile string
TlsKeyFile string
DefaultReadTimeout time.Duration
DefaultWriteTimeout time.Duration
SlowCometDuration time.Duration
DefaultBodyCodec string
PrintBody bool
CountTime bool
DefaultDialTimeout time.Duration
RedialTimes int32
Network string
ListenAddress string
}
2.?Peer的功能列表
- 提供路由功能
- 作為服務(wù)端可同時(shí)支持監(jiān)聽多個(gè)地址端口
- 作為客戶端可與任意服務(wù)端建立連接
- 提供會(huì)話查詢功能
- 支持TLS證書安全加密
- 設(shè)置默認(rèn)的建立連接和讀、寫超時(shí)
- 慢響應(yīng)閥值(超出后運(yùn)行日志由INFO提升為WARN)
- 支持打印body
- 支持在運(yùn)行日志中增加耗時(shí)統(tǒng)計(jì)
$ Go技巧分享
一個(gè)Go協(xié)程的初始堆棧大小為2KB(在運(yùn)行過程中可以動(dòng)態(tài)擴(kuò)展大小)如在高并發(fā)服務(wù)中不加限制地頻繁創(chuàng)建/銷毀協(xié)程,很容易造成內(nèi)存資源耗盡,且對(duì)GC壓力也會(huì)很大。因此,TP內(nèi)部采用協(xié)程資源池來管控協(xié)程,可以大大降低服務(wù)器內(nèi)存與CPU的壓力。(該思路源于fasthttp)
協(xié)程資源池的源碼實(shí)現(xiàn)在本人goutil庫(kù)中的github.com/henrylee2cn/goutil/pool
。下面是TP的二次封裝(保守認(rèn)為一個(gè)goroutine平均占用8KB):
var (
_maxGoroutinesAmount = (1024 * 1024 * 8) / 8 // max memory 8GB (8KB/goroutine)
_maxGoroutineIdleDuration time.Duration
_gopool = pool.NewGoPool(_maxGoroutinesAmount, _maxGoroutineIdleDuration)
)
// SetGopool set or reset go pool config.
// Note: Make sure to call it before calling NewPeer() and Go()
func SetGopool(maxGoroutinesAmount int, maxGoroutineIdleDuration time.Duration) {
_maxGoroutinesAmount, _maxGoroutineIdleDuration := maxGoroutinesAmount, maxGoroutineIdleDuration
if _gopool != nil {
_gopool.Stop()
}
_gopool = pool.NewGoPool(_maxGoroutinesAmount, _maxGoroutineIdleDuration)
}
// Go similar to go func, but return false if insufficient resources.
func Go(fn func()) bool {
if err := _gopool.Go(fn); err != nil {
Warnf("%s", err.Error())
return false
}
return true
}
// AnywayGo similar to go func, but concurrent resources are limited.
func AnywayGo(fn func()) {
TRYGO:
if !Go(fn) {
time.Sleep(time.Second)
goto TRYGO
}
}
每當(dāng)Peer創(chuàng)建一個(gè)session時(shí),都有調(diào)用上述Go
函數(shù)進(jìn)行并發(fā)執(zhí)行:
func (p *Peer) DialContext(ctx context.Context, addr string, protoFunc ...socket.ProtoFunc) (Session, *Rerror) {
...
Go(sess.startReadAndHandle)
...
}
func (p *Peer) Listen(protoFunc ...socket.ProtoFunc) error {
lis, err := listen(p.network, p.listenAddr, p.tlsConfig)
if err != nil {
Fatalf("%v", err)
}
defer lis.Close()
p.listen = lis
network := lis.Addr().Network()
addr := lis.Addr().String()
Printf("listen ok (network:%s, addr:%s)", network, addr)
var (
tempDelay time.Duration // how long to sleep on accept failure
closeCh = p.closeCh
)
for {
conn, e := lis.Accept()
...
AnywayGo(func() {
if c, ok := conn.(*tls.Conn); ok {
...
}
var sess = newSession(p, conn, protoFunc)
if rerr := p.pluginContainer.PostAccept(sess); rerr != nil {
sess.Close()
return
}
Tracef("accept session(network:%s, addr:%s) ok", network, sess.RemoteIp(), sess.Id())
p.sessHub.Set(sess)
sess.startReadAndHandle()
})
}
}
7 Router路由器
TP是對(duì)等通信,路由不再是服務(wù)端的專利,只要是Peer端點(diǎn)就支持注冊(cè)PULL
和PUSH
這兩類消息處理路由。
type Router struct {
handlers map[string]*Handler
unknownApiType **Handler
// only for register router
pathPrefix string
pluginContainer PluginContainer
typ string
maker HandlersMaker
}
func (r *Router) Group(pathPrefix string, plugin ...Plugin) *Router
func (r *Router) Reg(ctrlStruct interface{}, plugin ...Plugin)
func (r *Router) SetUnknown(unknownHandler interface{}, plugin ...Plugin)
$ Go技巧分享
1.?根據(jù)maker HandlersMaker
(Handler的構(gòu)造函數(shù))字段的不同,分別實(shí)現(xiàn)了PullRouter
和PushRouter
兩類路由。
// HandlersMaker makes []*Handler
type HandlersMaker func(
pathPrefix string,
ctrlStruct interface{},
pluginContainer PluginContainer,
) ([]*Handler, error)
2.?簡(jiǎn)潔地路由分組實(shí)現(xiàn):
- 繼承各級(jí)路由的共享字段:
handlers
、unknownApiType
、maker
- 在上級(jí)路由節(jié)點(diǎn)的
pathPrefix
、pluginContainer
字段基礎(chǔ)上追加當(dāng)前節(jié)點(diǎn)信息
// Group add handler group.
func (r *Router) Group(pathPrefix string, plugin ...Plugin) *Router {
pluginContainer, err := r.pluginContainer.cloneAdd(plugin...)
if err != nil {
Fatalf("%v", err)
}
warnInvaildRouterHooks(plugin)
return &Router{
handlers: r.handlers,
unknownApiType: r.unknownApiType,
pathPrefix: path.Join(r.pathPrefix, pathPrefix),
pluginContainer: pluginContainer,
maker: r.maker,
}
}
8 控制器
控制器是指用于提供Handler操作的結(jié)構(gòu)體。
$ Go技巧分享
1.?Go沒有泛型,我們通常使用interface{}
空接口來代替。
但是,空接口不能用于表示結(jié)構(gòu)體的方法。
下面是控制器結(jié)構(gòu)體及其方法的模型定義:
PullController Model:
type Aaa struct {
tp.PullCtx
}
// XxZz register the route: /aaa/xx_zz
func (x *Aaa) XxZz(args *<T>) (<T>, *tp.Rerror) {
...
return r, nil
}
// YyZz register the route: /aaa/yy_zz
func (x *Aaa) YyZz(args *<T>) (<T>, *tp.Rerror) {
...
return r, nil
}
PushController Model:
type Bbb struct {
tp.PushCtx
}
// XxZz register the route: /bbb/yy_zz
func (b *Bbb) XxZz(args *<T>) {
...
return r, nil
}
// YyZz register the route: /bbb/yy_zz
func (b *Bbb) YyZz(args *<T>) {
...
return r, nil
}
以PullController為例,使用reflect
反射包對(duì)未知類型的結(jié)構(gòu)體進(jìn)行模型驗(yàn)證:
func pullHandlersMaker(pathPrefix string, ctrlStruct interface{}, pluginContainer PluginContainer) ([]*Handler, error) {
var (
ctype = reflect.TypeOf(ctrlStruct)
handlers = make([]*Handler, 0, 1)
)
if ctype.Kind() != reflect.Ptr {
return nil, errors.Errorf("register pull handler: the type is not struct point: %s", ctype.String())
}
var ctypeElem = ctype.Elem()
if ctypeElem.Kind() != reflect.Struct {
return nil, errors.Errorf("register pull handler: the type is not struct point: %s", ctype.String())
}
if _, ok := ctrlStruct.(PullCtx); !ok {
return nil, errors.Errorf("register pull handler: the type is not implemented PullCtx interface: %s", ctype.String())
}
iType, ok := ctypeElem.FieldByName("PullCtx")
if !ok || !iType.Anonymous {
return nil, errors.Errorf("register pull handler: the struct do not have anonymous field PullCtx: %s", ctype.String())
}
...
for m := 0; m < ctype.NumMethod(); m++ {
method := ctype.Method(m)
mtype := method.Type
mname := method.Name
// Method must be exported.
if method.PkgPath != "" || isPullCtxType(mname) {
continue
}
// Method needs two ins: receiver, *args.
if mtype.NumIn() != 2 {
return nil, errors.Errorf("register pull handler: %s.%s needs one in argument, but have %d", ctype.String(), mname, mtype.NumIn())
}
// Receiver need be a struct pointer.
structType := mtype.In(0)
if structType.Kind() != reflect.Ptr || structType.Elem().Kind() != reflect.Struct {
return nil, errors.Errorf("register pull handler: %s.%s receiver need be a struct pointer: %s", ctype.String(), mname, structType)
}
// First arg need be exported or builtin, and need be a pointer.
argType := mtype.In(1)
if !goutil.IsExportedOrBuiltinType(argType) {
return nil, errors.Errorf("register pull handler: %s.%s args type not exported: %s", ctype.String(), mname, argType)
}
if argType.Kind() != reflect.Ptr {
return nil, errors.Errorf("register pull handler: %s.%s args type need be a pointer: %s", ctype.String(), mname, argType)
}
// Method needs two outs: reply error.
if mtype.NumOut() != 2 {
return nil, errors.Errorf("register pull handler: %s.%s needs two out arguments, but have %d", ctype.String(), mname, mtype.NumOut())
}
// Reply type must be exported.
replyType := mtype.Out(0)
if !goutil.IsExportedOrBuiltinType(replyType) {
return nil, errors.Errorf("register pull handler: %s.%s first reply type not exported: %s", ctype.String(), mname, replyType)
}
// The return type of the method must be Error.
if returnType := mtype.Out(1); !isRerrorType(returnType.String()) {
return nil, errors.Errorf("register pull handler: %s.%s second reply type %s not *tp.Rerror", ctype.String(), mname, returnType)
}
...
}
...
}
2.?參考HTTP的成熟經(jīng)驗(yàn),TP的路由路徑采用類URL格式,且支持query參數(shù):如/a/b?n=1&m=e
9 Unknown操作函數(shù)
TP可通過func (r *Router) SetUnknown(unknownHandler interface{}, plugin ...Plugin)
方法設(shè)置默認(rèn)Handler,用于處理未找到路由的PULL
或PUSH
消息。
UnknownPullHandler Type:
func(ctx UnknownPullCtx) (interface{}, *Rerror) {
...
return r, nil
}
UnknownPushHandler Type:
func(ctx UnknownPushCtx)
10 Handler的構(gòu)造
// Handler pull or push handler type info
Handler struct {
name string
isUnknown bool
argElem reflect.Type
reply reflect.Type // only for pull handler doc
handleFunc func(*readHandleCtx, reflect.Value)
unknownHandleFunc func(*readHandleCtx)
pluginContainer PluginContainer
}
通過HandlersMaker
對(duì)Controller各個(gè)方法進(jìn)行解析,構(gòu)造出相應(yīng)數(shù)量的Handler。以pullHandlersMaker
函數(shù)為例:
func pullHandlersMaker(pathPrefix string, ctrlStruct interface{}, pluginContainer PluginContainer) ([]*Handler, error) {
var (
ctype = reflect.TypeOf(ctrlStruct)
handlers = make([]*Handler, 0, 1)
)
...
var ctypeElem = ctype.Elem()
...
iType, ok := ctypeElem.FieldByName("PullCtx")
...
var pullCtxOffset = iType.Offset
if pluginContainer == nil {
pluginContainer = newPluginContainer()
}
type PullCtrlValue struct {
ctrl reflect.Value
ctxPtr *PullCtx
}
var pool = &sync.Pool{
New: func() interface{} {
ctrl := reflect.New(ctypeElem)
pullCtxPtr := ctrl.Pointer() + pullCtxOffset
ctxPtr := (*PullCtx)(unsafe.Pointer(pullCtxPtr))
return &PullCtrlValue{
ctrl: ctrl,
ctxPtr: ctxPtr,
}
},
}
for m := 0; m < ctype.NumMethod(); m++ {
method := ctype.Method(m)
mtype := method.Type
mname := method.Name
...
var methodFunc = method.Func
var handleFunc = func(ctx *readHandleCtx, argValue reflect.Value) {
obj := pool.Get().(*PullCtrlValue)
*obj.ctxPtr = ctx
rets := methodFunc.Call([]reflect.Value{obj.ctrl, argValue})
ctx.output.SetBody(rets[0].Interface())
rerr, _ := rets[1].Interface().(*Rerror)
if rerr != nil {
rerr.SetToMeta(ctx.output.Meta())
} else if ctx.output.Body() != nil && ctx.output.BodyCodec() == codec.NilCodecId {
ctx.output.SetBodyCodec(ctx.input.BodyCodec())
}
pool.Put(obj)
}
handlers = append(handlers, &Handler{
name: path.Join(pathPrefix, ctrlStructSnakeName(ctype), goutil.SnakeString(mname)),
handleFunc: handleFunc,
argElem: argType.Elem(),
reply: replyType,
pluginContainer: pluginContainer,
})
}
return handlers, nil
}
$ Go技巧分享
- 對(duì)不可變的部分進(jìn)行預(yù)處理獲得閉包變量,抽離可變部分的邏輯構(gòu)造子函數(shù)。在路由處理過程中直接執(zhí)行這些
handleFunc
子函數(shù)可達(dá)到顯著提升性能的目的 - 使用反射來創(chuàng)建任意類型的實(shí)例并調(diào)用其方法,適用于類型或方法不固定的情況
- 使用對(duì)象池來復(fù)用
PullCtrlValue
,可以降低GC開銷與內(nèi)存占用 - 通過unsafe獲取
ctrlStruct.PullCtx
字段的指針偏移量,進(jìn)而可以快速獲取該字段的值
11 Session會(huì)話
Session是封裝了socket連接的會(huì)話管理實(shí)例。它使用一個(gè)包外不可見的結(jié)構(gòu)體session
來實(shí)現(xiàn)會(huì)話相關(guān)的三個(gè)接口:
PreSession
、Session
、PostSession
。(此處session實(shí)現(xiàn)多接口的做法類似于Packet)
type (
PreSession interface {
...
}
Session interface {
// SetId sets the session id.
SetId(newId string)
// Close closes the session.
Close() error
// Id returns the session id.
Id() string
// Health checks if the session is ok.
Health() bool
// Peer returns the peer.
Peer() *Peer
// AsyncPull sends a packet and receives reply asynchronously.
// If the args is []byte or *[]byte type, it can automatically fill in the body codec name.
AsyncPull(uri string, args interface{}, reply interface{}, done chan *PullCmd, setting ...socket.PacketSetting)
// Pull sends a packet and receives reply.
// Note:
// If the args is []byte or *[]byte type, it can automatically fill in the body codec name;
// If the session is a client role and PeerConfig.RedialTimes>0, it is automatically re-called once after a failure.
Pull(uri string, args interface{}, reply interface{}, setting ...socket.PacketSetting) *PullCmd
// Push sends a packet, but do not receives reply.
// Note:
// If the args is []byte or *[]byte type, it can automatically fill in the body codec name;
// If the session is a client role and PeerConfig.RedialTimes>0, it is automatically re-called once after a failure.
Push(uri string, args interface{}, setting ...socket.PacketSetting) *Rerror
// ReadTimeout returns readdeadline for underlying net.Conn.
ReadTimeout() time.Duration
// RemoteIp returns the remote peer ip.
RemoteIp() string
// LocalIp returns the local peer ip.
LocalIp() string
// ReadTimeout returns readdeadline for underlying net.Conn.
SetReadTimeout(duration time.Duration)
// WriteTimeout returns writedeadline for underlying net.Conn.
SetWriteTimeout(duration time.Duration)
// Socket returns the Socket.
// Socket() socket.Socket
// WriteTimeout returns writedeadline for underlying net.Conn.
WriteTimeout() time.Duration
// Public returns temporary public data of session(socket).
Public() goutil.Map
// PublicLen returns the length of public data of session(socket).
PublicLen() int
}
PostSession interface {
...
}
session struct {
...
}
)
Session采用讀寫異步的方式處理通信消息。在創(chuàng)建Session后,立即啟動(dòng)一個(gè)循環(huán)讀取數(shù)據(jù)包的協(xié)程,并為每個(gè)成功讀取的數(shù)據(jù)包創(chuàng)建一個(gè)處理協(xié)程。
而寫操作則是由session.Pull、session.Push或者Handler三種方式來觸發(fā)執(zhí)行。
$ Go技巧分享
在以客戶端角色執(zhí)行PULL請(qǐng)求時(shí),Session支持同步和異步兩種方式。這是Go的一種經(jīng)典的兼容同步異步調(diào)用的技巧:
func (s *session) AsyncPull(uri string, args interface{}, reply interface{}, done chan *PullCmd, setting ...socket.PacketSetting) {
...
cmd := &PullCmd{
sess: s,
output: output,
reply: reply,
doneChan: done,
start: s.peer.timeNow(),
public: goutil.RwMap(),
}
...
if err := s.write(output); err != nil {
cmd.rerr = rerror_writeFailed.Copy()
cmd.rerr.Detail = err.Error()
cmd.done()
return
}
s.peer.pluginContainer.PostWritePull(cmd)
}
// Pull sends a packet and receives reply.
// If the args is []byte or *[]byte type, it can automatically fill in the body codec name.
func (s *session) Pull(uri string, args interface{}, reply interface{}, setting ...socket.PacketSetting) *PullCmd {
doneChan := make(chan *PullCmd, 1)
s.AsyncPull(uri, args, reply, doneChan, setting...)
pullCmd := <-doneChan
close(doneChan)
return pullCmd
}
實(shí)現(xiàn)步驟:
- 在返回結(jié)果的結(jié)構(gòu)體中綁定一個(gè)chan管道
- 在另一個(gè)協(xié)程中進(jìn)行結(jié)果計(jì)算
- 將該chan做為返回值返回給調(diào)用者
- 將計(jì)算結(jié)果寫入該chan中
- 調(diào)用者從chan中讀出該結(jié)果
- (同步方式是對(duì)異步方式的封裝,等待從chan中讀到結(jié)果后,再將該結(jié)果作為返回值返回)
12 Context上下文
類似常見的Go HTTP框架,TP同樣提供了Context上下文。它攜帶Handler操作相關(guān)的參數(shù),如Peer、Session、Packet、PublicData等。
根據(jù)調(diào)用場(chǎng)景的不同,定義不同接口來限制其方法列表。
此外,TP的平滑關(guān)閉、平滑重啟也是建立在對(duì)Context的使用狀態(tài)監(jiān)控的基礎(chǔ)上。
type (
BackgroundCtx interface {
...
}
PushCtx interface {
...
}
PullCtx interface {
...
}
UnknownPushCtx interface {
...
}
UnknownPullCtx interface {
...
}
WriteCtx interface {
...
}
ReadCtx interface {
...
}
readHandleCtx struct {
sess *session
input *socket.Packet
output *socket.Packet
apiType *Handler
arg reflect.Value
pullCmd *PullCmd
uri *url.URL
query url.Values
public goutil.Map
start time.Time
cost time.Duration
pluginContainer PluginContainer
next *readHandleCtx
}
)
13 Plugin插件
TP提供了插件功能,具有完備的掛載點(diǎn),便于開發(fā)者實(shí)現(xiàn)豐富的功能。例如身份認(rèn)證、心跳、微服務(wù)注冊(cè)中心、信息統(tǒng)計(jì)等等。
type (
Plugin interface {
Name() string
}
PostRegPlugin interface {
Plugin
PostReg(*Handler) *Rerror
}
PostDialPlugin interface {
Plugin
PostDial(PreSession) *Rerror
}
...
PostReadReplyBodyPlugin interface {
Plugin
PostReadReplyBody(ReadCtx) *Rerror
}
...
// PluginContainer plugin container that defines base methods to manage plugins.
PluginContainer interface {
Add(plugins ...Plugin) error
Remove(pluginName string) error
GetByName(pluginName string) Plugin
GetAll() []Plugin
PostReg(*Handler) *Rerror
PostDial(PreSession) *Rerror
...
PostReadReplyBody(ReadCtx) *Rerror
...
cloneAdd(...Plugin) (PluginContainer, error)
}
pluginContainer struct {
plugins []Plugin
}
)
func (p *pluginContainer) PostReg(h *Handler) *Rerror {
var rerr *Rerror
for _, plugin := range p.plugins {
if _plugin, ok := plugin.(PostRegPlugin); ok {
if rerr = _plugin.PostReg(h); rerr != nil {
Fatalf("%s-PostRegPlugin(%s)", plugin.Name(), rerr.String())
return rerr
}
}
}
return nil
}
func (p *pluginContainer) PostDial(sess PreSession) *Rerror {
var rerr *Rerror
for _, plugin := range p.plugins {
if _plugin, ok := plugin.(PostDialPlugin); ok {
if rerr = _plugin.PostDial(sess); rerr != nil {
Debugf("dial fail (addr: %s, id: %s): %s-PostDialPlugin(%s)", sess.RemoteIp(), sess.Id(), plugin.Name(), rerr.String())
return rerr
}
}
}
return nil
}
func (p *pluginContainer) PostReadReplyBody(ctx ReadCtx) *Rerror {
var rerr *Rerror
for _, plugin := range p.plugins {
if _plugin, ok := plugin.(PostReadReplyBodyPlugin); ok {
if rerr = _plugin.PostReadReplyBody(ctx); rerr != nil {
Errorf("%s-PostReadReplyBodyPlugin(%s)", plugin.Name(), rerr.String())
return rerr
}
}
}
return nil
}
$ Go技巧分享
Go接口斷言的靈活運(yùn)用,實(shí)現(xiàn)插件及其管理容器:
- 定義基礎(chǔ)接口并創(chuàng)建統(tǒng)一管理容器
- 在實(shí)現(xiàn)基礎(chǔ)接口的基礎(chǔ)上,增加個(gè)性化接口(具體掛載點(diǎn))的實(shí)現(xiàn),將其注冊(cè)進(jìn)基礎(chǔ)接口管理容器
- 管理容器使用斷言的方法篩選出指定掛載點(diǎn)的插件并執(zhí)行
最新地址:https://github.com/henrylee2cn/tpdoc/blob/master/01/README.md