項(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)示意圖

注:“tp” 是 teleport 的包名,因此它代指 “teleport”。
簡單的性能對比圖




為兼容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)它們有共通之處。

如何實(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)體 - 用于編解碼
Message中Body數(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)⒅校粠в?* 前綴的都是接口。
其中以 Peer、Session、 Ctx 為后綴的入?yún)ⅲń涌陬愋停?,涉及到一種非常有趣、有用的 interface 用法——限制方法集。
以 Ctx 為例:

實(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 |

聊聊高效開發(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) ?因?yàn)槲沂枪室獾模≡騽t涉及到 interface 的一個經(jīng)典的坑:(*Rerror)(nil) !=(error)(nil)。如果開發(fā)者不小心寫出下面的代碼,就掉坑里了:Rerror.Error() error
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)行平滑升級