聊聊 Go Socket 框架 Teleport 的設(shè)計(jì)思路

項(xiàng)目源碼

teleport:https://github.com/henrylee2cn/teleport

背景

大家在進(jìn)行業(yè)務(wù)開發(fā)時,是否是否遇到過下列問題,并且無法在Go語言開源生態(tài)中找到一套完整的解決方案?

  • 高性能、可靠地通信?
  • 開發(fā)效率不高?
  • 無法自定義應(yīng)用層協(xié)議?
  • 想要動態(tài)協(xié)商Body編碼類型(如JSON、protobuf等)?
  • 不能以簡潔的RPC方式進(jìn)行業(yè)務(wù)開發(fā)?
  • 沒有靈活的插件擴(kuò)展機(jī)制?
  • 不支持服務(wù)端向客戶端主動推送消息?
  • 特殊場景時需要連接管理,如多種連接類型、會話管理?
  • 使用了非HTTP協(xié)議框架,但不能很好的兼容HTTP協(xié)議,無法方便地與第三方對接?

我對于常見的一些相關(guān)開源項(xiàng)目做了一次粗略調(diào)查,發(fā)現(xiàn)迄今為止,除今天我要分享的這款 teleport 框架外(確切講還包括由teleport擴(kuò)展而來的微服務(wù)框架 tp-micro),貌似并沒有另外一款Go語言的開源框架能夠同時解決上述問題:

框架 描述 高性能 高效開發(fā) DIY應(yīng)用層協(xié)議 Body編碼協(xié)商 RPC范式 插件 推送 連接管理 兼容HTTP協(xié)議
teleport TCP socket 框架 ★★★★ ? ? ? ? ? ? ? ?
net 標(biāo)準(zhǔn)包網(wǎng)絡(luò)工具 ★★★★★ x ? x x x ? ? x
net/rpc 標(biāo)準(zhǔn)包RPC ★★★★☆ x x x ? x x x x
net/http(2) 標(biāo)準(zhǔn)包HTTP2 ★★★☆ x x ? x x ? x ?
gRPC 谷歌出品的RPC框架 ★★★ ? x ? ? x ? x ?
rpcx net/rpc的擴(kuò)展框架 ★★★★ ? x x ? ? ? x ?

概述

teleport 就是在上述需求背景下被創(chuàng)造出來,成為一個通用、高效、靈活的Socket框架。

它可以用于Peer-Peer對等通信、RPC、長連接網(wǎng)關(guān)、微服務(wù)、推送服務(wù),游戲服務(wù)等領(lǐng)域。

其主要特性如下:

* * * * * * * * *
高性能 高效開發(fā) DIY應(yīng)用層協(xié)議 Body編碼協(xié)商 RPC范式 插件 推送 連接管理
(Socket文件描述符/
會話管理/上下文等)
兼容HTTP協(xié)議
平滑關(guān)閉/升級 Log接口 非阻塞異步IO 斷線重連 對等通信 對等API 反向代理 慢響應(yīng)報警 ......

TODO:尚未提供多語言客戶端版本

架構(gòu)

設(shè)計(jì)原則

  • 面向接口設(shè)計(jì),保證代碼穩(wěn)定,提供靈活定制

  • 抽象核心模型,保持最簡化

  • 分層設(shè)計(jì),自下而上逐層封裝,利于穩(wěn)定和維護(hù)

  • 充分利用協(xié)程,且保證可控、可復(fù)用

架構(gòu)示意圖

teleport_module_diagram.png

注:“tp” 是 teleport 的包名,因此它代指 “teleport”。

簡單的性能對比圖

env.png
mean_latency.png
p99_latency.png
throughput.png

為兼容HTTP做準(zhǔn)備

兼容 HTTP 最好的辦法就是在設(shè)計(jì)應(yīng)用層協(xié)議之初就考慮到進(jìn)去。因此,teleport 對應(yīng)用層協(xié)議報文的屬性做了如下抽象:

  • Size 整個報文的長度
  • Transfer-Filter-Pipeline 報文數(shù)據(jù)過濾處理管道
  • Header
    • Seq 消息序號(因?yàn)槭钱惒酵ㄐ牛?/li>
    • Mtype 消息類型(如PULL、REPLY、PUSH)
    • URI 資源標(biāo)識符(對照常見RPC框架中的method,但可以更好地兼容HTTP)
    • Meta 元信息(如錯誤信息、內(nèi)容協(xié)商信息等,對照HTTP Header)
  • Body
    • BodyCodec 消息正文的編碼類型(如JSON、Protobuf)
    • Body 消息正文

從下圖 teleport 報文屬性與 HTTP 報文對比中,不難發(fā)現(xiàn)它們有共通之處。

tp_data_message.png

如何實(shí)現(xiàn)DIY應(yīng)用層協(xié)議?

應(yīng)用層協(xié)議是指建立在 TCP 協(xié)議之上的報文協(xié)議。我們希望開發(fā)者自己定制該協(xié)議,這樣更具備靈活性,比如protobuf、thrift等。

首先要做的第一件事是:

抽象出一個 Message 對象,為應(yīng)用層協(xié)議接口提供字節(jié)流序列化與反序列化模板。

Step1: 抽象 Message 對象

在 teleport/socket 包中抽象出 Message 結(jié)構(gòu)體(上面已經(jīng)介紹過了)

Step2: 抽象 Proto 協(xié)議接口

提供 Proto 協(xié)議接口,對 Message 對象進(jìn)行序列化與反序列化,從而支持開發(fā)者的自定義實(shí)現(xiàn)自己的協(xié)議格式,其接口聲明如下:

type Proto interface {
    Version() (byte, string)
    Pack(*Message) error
    Unpack(*Message) error
}

解釋:

  • Version :實(shí)現(xiàn)該協(xié)議接口的版本號

  • Pack :按照接口實(shí)現(xiàn)的規(guī)則,將 Message 的屬性序列化為字節(jié)流

  • Unpack :按照接口實(shí)現(xiàn)的規(guī)則,將字節(jié)流反序列化進(jìn)一個 Message 對象

目前框架已經(jīng)提供三種協(xié)議:Raw、JSON、Protobuf。

其中以Raw為示例,展示如下:

# raw protocol format(Big Endian):

{4 bytes message length}
{1 byte protocol version}
{1 byte transfer pipe length}
{transfer pipe IDs}
# The following is handled data by transfer pipe
{2 bytes sequence length}
{sequence}
{1 byte message type} # e.g. CALL:1; REPLY:2; PUSH:3
{2 bytes URI length}
{URI}
{2 bytes metadata length}
{metadata(urlencoded)}
{1 byte body codec id}
{body}

如何實(shí)現(xiàn) Body 編碼協(xié)商?

在實(shí)際業(yè)務(wù)場景中,報文的類型是多種多樣的,所以 teleport 使用 Codec 接口對消息正文(Message Body)進(jìn)行編解碼。

type Codec interface {
    Id() byte
    Name() string
    Marshal(v interface{}) ([]byte, error)
    Unmarshal(data []byte, v interface{}) error
}

解釋:

  • Id :編解碼器的唯一識別碼
  • Name :編解碼器的名稱,同樣要求全局唯一,主要是便于開發(fā)者記憶和可視化
  • Marshal :編碼
  • Unmarshal :解碼

開發(fā)者可以將自定義的新編解碼器注入 teleport/codec 包,從而在整個項(xiàng)目中使用。

框架已經(jīng)提供的編解碼器實(shí)現(xiàn):JSON、Protobuf、Form(urlencoded)、Plain(raw text)

在自由支持各種編解碼類型后,我就可以模仿 HTTP 協(xié)議頭的 Content-Type 實(shí)現(xiàn)一下協(xié)商功能了。

在 Request/Response 的通信場景下,按以下步驟進(jìn)行 Body 編碼類型協(xié)商:

  • Step1:請求端將當(dāng)前 Body 的編碼類型設(shè)置到 Message 的 BodyCodec 屬性

  • Step2:在請求端希望收到請求Body不同的編碼類型時(在web開發(fā)中很常見),就可以在 Message 對象的 Meta 元信息中設(shè)置 X-Accept-Body-Codec 來指定響應(yīng)的編碼類型

  • Step3:響應(yīng)端根據(jù)請求的 BodyCodec 屬性解碼 Body,執(zhí)行業(yè)務(wù)邏輯

  • Step4:響應(yīng)端在發(fā)現(xiàn)有 X-Accept-Body-Codec 元信息時,使用該元信息指定類型編碼響應(yīng) Body,否則默認(rèn)使用與請求相同的編碼類型。當(dāng)然,響應(yīng)端的開發(fā)者也可以明確指定編碼類型,這樣就會忽略前面的規(guī)則,強(qiáng)制使用該指定的編碼類型。

在上述 Step2 中,請求端設(shè)置 Message 對象的 X-Accept-Body-Codec Meta 元信息的一段代碼片段:

session.Call("/a/b", arg, result, tp.WithAcceptBodyCodec(codec.ID_PROTOBUF))

其中,tp.WithAcceptBodyCodec 是一種修飾函數(shù)的用法,這類函數(shù)可以實(shí)現(xiàn)靈活地配置策略,一些相關(guān)定義如下。在 teleport/socket 包中:

type MessageSetting func(*Message)

func WithAddMeta(key, value string) MessageSetting {
    return func(m *Message) {
        m.meta.Add(key, value)
    }
}

在 teleport 包中:

const MetaAcceptBodyCodec = "X-Accept-Body-Codec"

func WithAcceptBodyCodec(bodyCodec byte) MessageSetting {
    if bodyCodec == codec.NilCodecId {
        return func(*Message) {}
    }
    return socket.WithAddMeta(MetaAcceptBodyCodec, strconv.FormatUint(uint64(bodyCodec), 10))
}
...
type Session interface {
    Call(uri string, arg interface{}, result interface{}, setting ...socket.MessageSetting) CallCmd
}

說明:Call 其實(shí)類似于 net/http 中的 func (c *Client) Do(req *Request) (*Response, error) 是根據(jù)請求參數(shù) Message 進(jìn)行請求的。

在該場景中為什么選擇使用修飾函數(shù)?為什么不直接傳入 Message 結(jié)構(gòu)體(先將其字段公開)?

  • Message 的字段很多,有的必填,有的選填;例如必填參數(shù) uri、arg 都是它的字段(arg對應(yīng)body字段),meta、context 等為選填;通過上述這種“必填參數(shù)+修飾函數(shù)不定參”的方法聲明,可以從語法層面明確使用規(guī)范(如換成使用結(jié)構(gòu)體,只能使用約定,然后在運(yùn)行時檢查)
  • 修飾函數(shù)的方式可以封裝更加復(fù)雜的配置邏輯,比如設(shè)置兩個關(guān)聯(lián)參數(shù)的情況,某個字段需要寫多行代碼進(jìn)行初始化的情況
  • 修飾函數(shù)的使用更加靈活,具有很強(qiáng)的封裝性,比如可以提供常用的修飾函數(shù)包,可以多個修飾函數(shù)嵌套組合成一個新的修飾函數(shù)等等
  • 特意將 Message 的字段聲明為私有,同時在當(dāng)前包內(nèi)提供一些基礎(chǔ)修飾函數(shù),可以大大提高配置的可控性與安全性,同時也避免了開發(fā)者學(xué)習(xí)一些配置約定的成本

概括一下修飾函數(shù)的使用場景:

  • 配置項(xiàng)很多且一些配置間存在聯(lián)動性
  • 配置項(xiàng)的結(jié)構(gòu)體本身屬于內(nèi)部邏輯的一部分,如果外部傳入后再對其進(jìn)行修改,會對內(nèi)部造成執(zhí)行bug的情況
  • 若僅僅是簡單的配置,建議使用結(jié)構(gòu)體,更加簡單直接,比如 mysql 的配置等

如何管理連接?

一般常見的 Go 語言 RPC 框架都沒有重視對連接的管理,甚至是沒有連接管理功能。那么,是不是就說明連接管理功能不重要?可有可無?其實(shí)不然,這只是與 RPC 框架的定位有關(guān):

實(shí)現(xiàn)遠(yuǎn)程過程調(diào)用,并不強(qiáng)調(diào)連接,甚至是刻意屏蔽掉底層連接

那么,什么場景下,我們需要使用連接管理?

  • 服務(wù)端主動推送消息給指定(一批)連接的客戶端
  • 服務(wù)端主動請求客戶端,并獲得客戶端的響應(yīng)
  • 增加會話管理,將每條連接命名為用戶ID,并綁定用戶信息
  • 獲取文件描述符,對連接性能進(jìn)行調(diào)優(yōu)
  • 異步主動斷開指定(一批)連接
  • 與第三方框架/組件對接

下面我們來了解一下 teleport 是如何實(shí)現(xiàn)連接管理的。

Step1:封裝 Socket 模塊

首先,我們以分層的原則對來自net標(biāo)準(zhǔn)包的 net.Conn 進(jìn)行封裝得到 Socket 接口。它作為整個框架的底層通信接口,向上層提供應(yīng)用層消息通信和連接管理的基礎(chǔ)功能。

該接口涉及五個組件:

  • 來自標(biāo)準(zhǔn)包的 net.Conn 接口
  • 抽象的應(yīng)用層協(xié)議接口 Proto
  • 對字節(jié)流處理的接口管道 XferPipe
  • 抽象出來的 Message 結(jié)構(gòu)體
  • 用于編解碼 MessageBody 數(shù)據(jù)的接口 Codec

常用接口方法如下:

  • WriteMessage(message *Message) error :寫入應(yīng)用層消息
  • ReadMessage(message *Message) error :讀取應(yīng)用層消息
  • SetId(string) 、Id() string :設(shè)置或讀取當(dāng)前連接ID
  • Swap() goutil.Map :存儲與當(dāng)前連接相關(guān)的臨時數(shù)據(jù)
  • ControlFD(f func(fd uintptr)) error :操作當(dāng)前連接的文件描述符

Step2:封裝 Session 模塊

Session 對象封裝了 Socket 接口 ,并負(fù)責(zé)整個會話相關(guān)的事務(wù)(相當(dāng)于引擎)。如:

  • 讀消息協(xié)程
  • 創(chuàng)建消息處理的上下文
  • 執(zhí)行路由與操作
  • 寫入消息
  • 打印運(yùn)行日志
  • 連接的ID命名
  • 綁定連接相關(guān)狀態(tài)信息(用戶資料等)
  • 連接生命周期(連接超時)
  • 一次請求的生命周期(請求超時)
  • 主動斷開連接
  • 撥號端的斷線重連
  • 連接斷開事件通知

Step3:并發(fā) Map 集中管理 Session

Peer 是 teleport 對通信兩端的對等抽象,除了 Listener 與 Dialer 固有的角色差異外,兩種角色擁有完全一致的API。Peer 就包含有一個并發(fā) Map 用于保存全部 Session。因此,開發(fā)者可以通過 Peer 實(shí)現(xiàn):

  • 監(jiān)聽地址端口
  • 撥號建立連接
  • 獲取指定 ID 的 Session 實(shí)例
  • 向所有 Session 廣播消息
  • 查看當(dāng)前連接數(shù)
  • 平滑關(guān)閉全部連接

另外,順便提一下,teleport是采用非阻塞的通信機(jī)制,同時支持同步、異步兩種編程方式。這樣做有什么好處,或者說阻塞通信與非阻塞通信的區(qū)別是什么?

golang 的 socket 是非阻塞式的,也就是說不管是accpet,還是讀寫都是非阻塞的。但是 golang 本身對 socket 做了一定的處理,讓其用起來像阻塞的一樣簡單。

因此,如果我們當(dāng)真把它作為阻塞通信機(jī)制,通過連接池實(shí)現(xiàn)并發(fā)通信,是很浪費(fèi)連接資源的!我們知道,“阻塞通信+連接池”的方式,不僅吞吐量相比較低,而且還有一個無法避免的缺陷:

  • 一類請求的慢響應(yīng),會很快耗盡整個連接池資源,進(jìn)而拖慢整個進(jìn)程的網(wǎng)絡(luò)通信

  • 如果該進(jìn)程是分布式系統(tǒng)中的一個節(jié)點(diǎn),那么這種慢響應(yīng)還會很快蔓延著其他通信節(jié)點(diǎn)

但是,如果使用非阻塞通信機(jī)制,每個請求都不獨(dú)占連接,而是共享連接。這樣:

  • 首先我們可以拋棄復(fù)雜的獨(dú)占式連接池了(文件下載服務(wù)可能還是會用到另外一種連接池)
  • 其次,一類或者一個慢響應(yīng)都不會對其他請求造成影響,同時也就解決了慢響應(yīng)蔓延的問題
  • 第三,可以最大化利用連接資源,提升吞吐量

微服務(wù)系統(tǒng)中,強(qiáng)烈建議使用這種非阻塞通信機(jī)制!

如何設(shè)計(jì)靈活的插件

插件會給框架帶來靈活性和擴(kuò)展性,是一個非常重要的模塊。那么,如何設(shè)計(jì)好它?teleport 從三方面考慮:

  • 合適且豐富的插件位置

  • 按插件位置量身設(shè)計(jì)入?yún)⒑统鰠?/p>

  • 一個插件允許包含一個或多個插件位置

    以下是 teleport 的一些插件位置定義:

插件位置(函數(shù)) 插件位置(函數(shù))
PreNewPeer(*PeerConfig, *PluginContainer) error PostNewPeer(EarlyPeer) error
PostReg(*Handler) error PostListen(net.Addr) error
PostDial(PreSession) *Rerror PostAccept(PreSession) *Rerror
PreWriteCall(WriteCtx) *Rerror PostWriteCall(WriteCtx) *Rerror
PreWriteReply(WriteCtx) *Rerror PostWriteReply(WriteCtx) *Rerror
PreWritePush(WriteCtx) *Rerror PostWritePush(WriteCtx) *Rerror
PreReadHeader(PreCtx) error PostReadCallHeader(ReadCtx) *Rerror
PreReadCallBody(ReadCtx) *Rerror PostReadCallBody(ReadCtx) *Rerror
PostReadPushHeader(ReadCtx) *Rerror PreReadPushBody(ReadCtx) *Rerror
PostReadPushBody(ReadCtx) *Rerror PostReadReplyHeader(ReadCtx) *Rerror
PreReadReplyBody(ReadCtx) *Rerror PostReadReplyBody(ReadCtx) *Rerror
PostDisconnect(BaseSession) *Rerror

上面這些函數(shù)的入?yún)⒅校粠в?* 前綴的都是接口。

其中以 PeerSession、 Ctx 為后綴的入?yún)ⅲń涌陬愋停?,涉及到一種非常有趣、有用的 interface 用法——限制方法集。

Ctx 為例:

ctx.png

實(shí)踐:輕松組裝微服務(wù)

tp-micro 是以 teleport + plugin 的方式擴(kuò)展而來的微服務(wù)。雖然目前還有一些功能未開發(fā),但已有兩家公司使用。它在完整繼承 teleport 特性的同時,增加如下主要模塊:

模塊 模塊 模塊 模塊 模塊 模塊
服務(wù)注冊插件 路由發(fā)現(xiàn)插件(含負(fù)載均衡) 心跳插件 參數(shù)綁定與校驗(yàn)插件 安全加密插件 斷路器
腳手架工具:由模板生成項(xiàng)目、熱編譯 網(wǎng)關(guān) 灰度 Agent
tp-micro_flow_chart.png

聊聊高效開發(fā)的一些事兒

實(shí)現(xiàn) RPC 開發(fā)范式

實(shí)現(xiàn) RPC 范式的好處是代碼書寫簡單、代碼結(jié)構(gòu)清晰明了、對開發(fā)者友好。

在此只貼出一個簡單代碼示例,不展開討論封裝細(xì)節(jié)。

  • server.go
package main

import (
    "fmt"
    "time"

    tp "github.com/henrylee2cn/teleport"
)

func main() {
    // graceful
    go tp.GraceSignal()

    // server peer
    srv := tp.NewPeer(tp.PeerConfig{
        CountTime:   true,
        ListenPort:  9090,
        PrintDetail: true,
    })

    // router
    srv.RouteCall(new(Math))

    // broadcast per 5s
    go func() {
        for {
            time.Sleep(time.Second * 5)
            srv.RangeSession(func(sess tp.Session) bool {
                sess.Push(
                    "/push/status",
                    fmt.Sprintf("this is a broadcast, server time: %v", time.Now()),
                )
                return true
            })
        }
    }()

    // listen and serve
    srv.ListenAndServe()
}

// Math handler
type Math struct {
    tp.CallCtx
}

// Add handles addition request
func (m *Math) Add(arg *[]int) (int, *tp.Rerror) {
    // test query parameter
    tp.Infof("author: %s", m.Query().Get("author"))
    // add
    var r int
    for _, a := range *arg {
        r += a
    }
    // response
    return r, nil
}
  • client.go
package main

import (
    "time"

    tp "github.com/henrylee2cn/teleport"
)

func main() {
    // log level
    tp.SetLoggerLevel("ERROR")

    cli := tp.NewPeer(tp.PeerConfig{})
    defer cli.Close()

    cli.RoutePush(new(Push))

    sess, err := cli.Dial(":9090")
    if err != nil {
        tp.Fatalf("%v", err)
    }

    var result int
    rerr := sess.Call("/math/add?author=henrylee2cn",
        []int{1, 2, 3, 4, 5},
        &result,
    ).Rerror()
    if rerr != nil {
        tp.Fatalf("%v", rerr)
    }
    tp.Printf("result: %d", result)

    tp.Printf("wait for 10s...")
    time.Sleep(time.Second * 10)
}

// Push push handler
type Push struct {
    tp.PushCtx
}

// Push handles '/push/status' message
func (p *Push) Status(arg *string) *tp.Rerror {
    tp.Printf("%s", *arg)
    return nil
}

處理錯誤的姿勢

teleport 對于 Handler 的錯誤返回值,并沒有采用 error 接口類型,而是定義了一個 Rerror 結(jié)構(gòu)體:(用法見上面示例代碼)

type Rerror struct {
    // Code error code
    Code int32
    // Message the error message displayed to the user (optional)
    Message string
    // Reason the cause of the error for debugging (optional)
    Reason string
}

這樣設(shè)計(jì)有幾個好處:

  • Code 字段表示錯誤代號,類似 HTTP 狀態(tài)碼,有利于和 HTTP 協(xié)議完美兼容,同時也方便插件和客戶端對錯誤類型快速判斷與處理

  • Message 字段用于給客戶端的錯誤提示信息,可進(jìn)行字符串格式的定制

  • Reason 字段記錄錯誤發(fā)生的原因甚至上下文,助力Debug

  • 如果開發(fā)者需要與 error 接口交互,Rerror.ToError() error 方法可以實(shí)現(xiàn)

可能有人會問:為什么不直接實(shí)現(xiàn) Rerror.Error() error ?因?yàn)槲沂枪室獾模≡騽t涉及到 interface 的一個經(jīng)典的坑:(*Rerror)(nil) !=(error)(nil)。如果開發(fā)者不小心寫出下面的代碼,就掉坑里了:

var err error
err = sess.Call(...).Rerror()
if err != nil { // 此處掉坑里了,必定會進(jìn)入錯誤處理邏輯,因?yàn)?(*Rerror)(nil) != (error)(nil) 永遠(yuǎn)成立
    ...
}

推薦:服務(wù)端定義一張全局的錯誤碼表,方便于客戶端對接以及錯誤Debug。比如這樣的規(guī)則:

  • 1 ≤ Code ≤ 999:框架錯誤,包括通信錯誤,具體可以與 HTTP 狀態(tài)碼保持一致
  • 1000 ≤ Code ≤ 9999:基礎(chǔ)服務(wù)錯誤
  • 1000000 ≤ 999999:業(yè)務(wù)錯誤,前四位表示模塊或服務(wù),后兩位表示當(dāng)前模塊或服務(wù)中錯誤序號

推薦一種很酷的項(xiàng)目結(jié)構(gòu)

這是 tp-micro 中默認(rèn)的項(xiàng)目組織結(jié)構(gòu),它有 micro gen 命令由模板自動構(gòu)建。

├── README.md
├── __tp-micro__gen__.lock
├── __tp-micro__tpl__.go
├── config
│   └── config.yaml
├── config.go
├── internal
│   ├── handler
│   │   ├── call.tmp.go
│   │   └── push.tmp.go
│   └── model
│       ├── init.go
│       ├── mongo_meta.gen.go
│       ├── mysql_device.gen.go
│       ├── mysql_log.gen.go
│       └── mysql_user.gen.go
├── log
│   └── PID
├── main.go
├── router.gen.go
└── sdk
    ├── rerr.go
    ├── rpc.gen.go
    ├── rpc.gen_test.go
    ├── type.gen.go
    └── val.gen.go

該項(xiàng)目結(jié)構(gòu)整體分為兩部分。

一部分是對外公開的代碼,都位于 sdk 目錄下,比如 client 遠(yuǎn)程調(diào)用的函數(shù)就在這里。

剩余代碼都是不對外公開的,屬于 server 進(jìn)程的部分,其中私有包 internal 下是 server 的主體業(yè)務(wù)邏輯部分。

這樣設(shè)計(jì)的好處是:

  • 外部調(diào)用者(一般是客戶端)只能導(dǎo)入 sdk 包,其余的包要么在 internal 下被私有化,要么就是 main 包,都無法導(dǎo)入;從而起到了從語法級別隔離代碼目的,有效地解決了誤用代碼、復(fù)雜依賴的問題
  • 將 sdk 代碼與 server 代碼放在同一項(xiàng)目中,便于統(tǒng)一管理,減少更新時人為原因造成客戶端與服務(wù)端接口對不上的情況

腳手架提升開發(fā)效率

在 tp-micro 中,提供了一個 micro 工具,介紹兩個最常用的命令:

  • 命令 micro gen 可以通過模板可以快速生成一個項(xiàng)目(上面提到的項(xiàng)目結(jié)構(gòu))
    • 其中包括 mysql、mongo、redis 相關(guān)的 model 層代碼
    • README.md 中會自動寫入接口文檔等,便于交付給客戶端同學(xué)
    • 支持覆蓋更新部分代碼,比如新增接口。
  • 命令 micro run 可以自動編譯運(yùn)行指定項(xiàng)目,并在項(xiàng)目代碼發(fā)生變化時自動進(jìn)行平滑升級
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,171評論 3 119
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評論 25 709
  • 不知什么時候,微信通訊錄里面的人逐漸多了起來,有昔日的好友,有新接觸的朋友。 每天刷刷朋友圈,看到一些動態(tài),有時會...
    想走走閱讀 156評論 0 0
  • 藝趣書苑閱讀 233評論 0 0

友情鏈接更多精彩內(nèi)容