Go Socket編程之teleport框架是怎樣煉成的

本文通過回顧 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

test code

  • 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

test code

  • 與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

test code

  • rpcx與其他框架的對(duì)比參考(圖片來源于rpcx)
rpc_compare
  • CPU耗時(shí)火焰圖 teleport/socket
tp_socket_profile_torch.png

svg file

  • 堆棧信息火焰圖 teleport/socket
tp_socket_heap_torch.png

svg file


第一部分 TP架構(gòu)設(shè)計(jì)

1 設(shè)計(jì)理念

TP定位于提供socket通信解決方案,遵循以下三點(diǎn)設(shè)計(jì)理念。

  • 通用:不做定向深入,專注長(zhǎng)連接通信
  • 高效:高性能,低消耗
  • 靈活:用法靈活簡(jiǎn)單,易于深入定制
  • 可靠:使用接口(interface)而非約束說明,規(guī)定框架用法

2 架構(gòu)圖

teleport_framework.png
  • 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ò)類型:tcptcp4tcp6、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è)接口HeaderBody。思考:為什么不直接使用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)HeaderBody接口的技巧

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è)PULLPUSH這兩類消息處理路由。

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)了PullRouterPushRouter兩類路由。

// HandlersMaker makes []*Handler
type HandlersMaker func(
    pathPrefix string,
    ctrlStruct interface{},
    pluginContainer PluginContainer,
) ([]*Handler, error)

2.?簡(jiǎn)潔地路由分組實(shí)現(xiàn):

  • 繼承各級(jí)路由的共享字段:handlers、unknownApiTypemaker
  • 在上級(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,用于處理未找到路由的PULLPUSH消息。

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è)接口:
PreSessionSession、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)步驟:

  1. 在返回結(jié)果的結(jié)構(gòu)體中綁定一個(gè)chan管道
  2. 在另一個(gè)協(xié)程中進(jìn)行結(jié)果計(jì)算
  3. 將該chan做為返回值返回給調(diào)用者
  4. 將計(jì)算結(jié)果寫入該chan中
  5. 調(diào)用者從chan中讀出該結(jié)果
  6. (同步方式是對(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)插件及其管理容器:

  1. 定義基礎(chǔ)接口并創(chuàng)建統(tǒng)一管理容器
  2. 在實(shí)現(xiàn)基礎(chǔ)接口的基礎(chǔ)上,增加個(gè)性化接口(具體掛載點(diǎn))的實(shí)現(xiàn),將其注冊(cè)進(jìn)基礎(chǔ)接口管理容器
  3. 管理容器使用斷言的方法篩選出指定掛載點(diǎn)的插件并執(zhí)行

最新地址:https://github.com/henrylee2cn/tpdoc/blob/master/01/README.md

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容