├── .gitattributes
├── .gitignore
├── 01
├── README.md
└── src
│ ├── qrcode-viewfile.png
│ └── rpc_compare.png
├── 02
├── README.md
└── src
│ ├── ctx.png
│ └── ctx.svg
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.* linguist-language=go
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.o
2 | *.a
3 | *.so
4 | _obj
5 | _test
6 | *.[568vq]
7 | [568vq].out
8 | *.cgo1.go
9 | *.cgo2.c
10 | _cgo_defun.c
11 | _cgo_gotypes.go
12 | _cgo_export.*
13 | _testmain.go
14 | *.exe
15 | *.exe~
16 | *.test
17 | *.prof
18 | *.rar
19 | *.zip
20 | *.gz
21 | *.psd
22 | *.bmd
23 | *.cfg
24 | *.pptx
25 | *.log
26 | *nohup.out
27 | *.sublime-project
28 | *.sublime-workspace
29 |
--------------------------------------------------------------------------------
/01/README.md:
--------------------------------------------------------------------------------
1 | # Go Socket编程之teleport框架是怎样炼成的?
2 |
3 | 本文通过回顾 teleport (https://github.com/henrylee2cn/teleport) 框架的开发过程,讲述Go Socket的开发实战经验。
4 |
5 | 本文的内容组织形式:teleport架构源码赏析+相应Go技巧分享
6 |
7 | 期间,我们可以分别从这两条线进行思考与探讨。
8 |
9 | *文中以`TP`作为`teleport`的简称
文中内容针对具有一定Go语言基础的开发者
文中以`Go技巧`是指高于语法常识的一些编程技巧、设计模式
为了压缩篇幅,代码块中删除了一些空行,并使用`...`表示省略行*
10 |
11 | ----------------------------------------
12 |
13 |
14 | |目 录
15 | |--------------------------------
16 | |[TP性能测试](#tp性能测试)
17 | |[第一部分 TP架构设计](#第一部分-tp架构设计)
18 | |[第二部分 TP关键源码赏析及相关Go技巧](#第二部分-tp关键源码赏析及相关go技巧)
19 |
20 |
21 | ## TP性能测试
22 |
23 | TP与其他使用长连接的框架的性能对比:
24 |
25 | **测试用例**
26 |
27 | - 一个服务端与一个客户端进程,在同一台机器上运行
28 | - CPU: Intel Xeon E312xx (Sandy Bridge) 16 cores 2.53GHz
29 | - Memory: 16G
30 | - OS: Linux 2.6.32-696.16.1.el6.centos.plus.x86_64, CentOS 6.4
31 | - Go: 1.9.2
32 | - 信息大小: 581 bytes
33 | - 信息编码:protobuf
34 | - 发送 1000000 条信息
35 |
36 | **测试结果**
37 |
38 | - teleport
39 |
40 | 并发client|平均值(ms)|中位数(ms)|最大值(ms)|最小值(ms)|吞吐率(TPS)
41 | -------------|-------------|-------------|-------------|-------------|-------------
42 | 100|1|0|16|0|75505
43 | 500|9|11|97|0|52192
44 | 1000|19|24|187|0|50040
45 | 2000|39|54|409|0|42551
46 | 5000|96|128|1148|0|46367
47 |
48 | **[test code](https://github.com/henrylee2cn/rpc-benchmark/tree/master/teleport)**
49 |
50 | - teleport/socket
51 |
52 | 并发client|平均值(ms)|中位数(ms)|最大值(ms)|最小值(ms)|吞吐率(TPS)
53 | -------------|-------------|-------------|-------------|-------------|-------------
54 | 100|0|0|14|0|225682
55 | 500|2|1|24|0|212630
56 | 1000|4|3|51|0|180733
57 | 2000|8|6|64|0|183351
58 | 5000|21|18|651|0|133886
59 |
60 | **[test code](https://github.com/henrylee2cn/rpc-benchmark/tree/master/teleport)**
61 |
62 | - 与rpcx的对比
63 |
64 | 并发client|平均值(ms)|中位数(ms)|最大值(ms)|最小值(ms)|吞吐率(TPS)
65 | -------------|-------------|-------------|-------------|-------------|-------------
66 | 100|0|0|50|0|109217
67 | 500|5|4|50|0|88113
68 | 1000|11|10|1040|0|87535
69 | 2000|23|29|3080|0|80886
70 | 5000|59|72|7111|0|78412
71 |
72 | **[test code](https://github.com/henrylee2cn/rpc-benchmark/tree/master/rpcx)**
73 |
74 | - rpcx与其他框架的对比参考(图片来源于rpcx)
75 |
76 | 
77 |
78 | - CPU耗时火焰图 teleport/socket
79 |
80 | 
81 |
82 | **[svg file](https://github.com/henrylee2cn/teleport/raw/v3/doc/tp_socket_profile_torch.svg)**
83 |
84 | - 堆栈信息火焰图 teleport/socket
85 |
86 | 
87 |
88 | **[svg file](https://github.com/henrylee2cn/teleport/raw/v3/doc/tp_socket_heap_torch.svg)**
89 |
90 | ----------------------------------------
91 |
92 | ## 第一部分 TP架构设计
93 |
94 | ### 1 设计理念
95 |
96 | TP定位于提供socket通信解决方案,遵循以下三点设计理念。
97 |
98 | - 通用:不做定向深入,专注长连接通信
99 | - 高效:高性能,低消耗
100 | - 灵活:用法灵活简单,易于深入定制
101 | - 可靠:使用接口(`interface`)而非约束说明,规定框架用法
102 |
103 | ### 2 架构图
104 |
105 | 
106 |
107 | - `Peer`: 通信端点,可以是服务端或客户端
108 | - `Plugin`: 贯穿于通信各个环节的插件
109 | - `Handler`: 用于处理推、拉请求的函数
110 | - `Router`: 通过请求信息(如URI)索引响应函数(Handler)的路由器
111 | - `Socket`: 对net.Conn的封装,增加自定义包协议、传输管道等功能
112 | - `Session`: 基于Socket封装的连接会话,提供的推、拉、回复、关闭等会话操作
113 | - `Context`: 连接会话中一次通信(如PULL-REPLY, PUSH)的上下文对象
114 | - `Packet`: 约定数据报文包含的内容元素(注意:它不是协议格式)
115 | - `Protocol`: 数据报文封包解包操作,即通信协议的实现接口
116 | - `Codec`: 数据包body部分(请求参数或响应结果)的序列化接口
117 | - `XferPipe`: 数据包字节流的编码处理管道,如压缩、加密、校验等
118 |
119 |
120 | ### 3 重要特性
121 |
122 | - 支持自定义通信协议和包数据处理管道
123 | - 使用I/O缓冲区与多路复用技术,提升数据吞吐量
124 | - 支持设置读取包的大小限制(如果超出则断开连接)
125 | - 支持插件机制,可以自定义认证、心跳、微服务注册中心、统计信息插件等
126 | - 服务端和客户端之间对等通信,统一为peer端点,具有基本一致的用法:
127 | - 推、拉、回复等通信方法
128 | - 丰富的插件挂载点,可以自定义认证、心跳、微服务注册中心、统计信息等等
129 | - 平滑重启与关闭
130 | - 日志信息详尽,支持打印输入、输出消息的详细信息(状态码、消息头、消息体)
131 | - 支持设置慢操作报警阈值
132 | - 提供Hander的上下文(pull、push的handler)
133 | - 客户端的Session支持断线后自动重连
134 | - 支持的网络类型:`tcp`、`tcp4`、`tcp6`、`unix`、`unixpacket`等
135 |
136 | ----------------------------------------
137 |
138 | ## 第二部分 TP关键源码赏析及相关Go技巧
139 |
140 |
*---------------------------以下为`github.com/henrylee2cn/telepot/socket`包内容---------------------------*
141 |
142 | ### 1 Packet统一数据包元素
143 |
144 | `Packet`结构体用于定义统一的数据包内容元素,为上层架构提供稳定、统一的操作API。
145 |
146 | #### $ **Go技巧分享**
147 |
148 | 1. 在`teleport/socket`目录下执行`go doc Packet`命令,我们可以获得以下关于`Packet`的定义、函数与方法:
149 |
150 | ```go
151 | type Packet struct {
152 | // Has unexported fields.
153 | }
154 | Packet a socket data packet.
155 |
156 | func GetPacket(settings ...PacketSetting) *Packet
157 | func NewPacket(settings ...PacketSetting) *Packet
158 | func (p *Packet) AppendXferPipeFrom(src *Packet)
159 | func (p *Packet) Body() interface{}
160 | func (p *Packet) BodyCodec() byte
161 | func (p *Packet) MarshalBody() ([]byte, error)
162 | func (p *Packet) Meta() *utils.Args
163 | func (p *Packet) Ptype() byte
164 | func (p *Packet) Reset(settings ...PacketSetting)
165 | func (p *Packet) Seq() uint64
166 | func (p *Packet) SetBody(body interface{})
167 | func (p *Packet) SetBodyCodec(bodyCodec byte)
168 | func (p *Packet) SetNewBody(newBodyFunc NewBodyFunc)
169 | func (p *Packet) SetPtype(ptype byte)
170 | func (p *Packet) SetSeq(seq uint64)
171 | func (p *Packet) SetSize(size uint32) error
172 | func (p *Packet) SetUri(uri string)
173 | func (p *Packet) Size() uint32
174 | func (p *Packet) String() string
175 | func (p *Packet) UnmarshalBody(bodyBytes []byte) error
176 | func (p *Packet) UnmarshalNewBody(bodyBytes []byte) error
177 | func (p *Packet) Uri() string
178 | func (p *Packet) XferPipe() *xfer.XferPipe
179 | ```
180 |
181 | 2. `Packet`全部字段均不可导出,可以增强代码稳定性以及对其操作的掌控力
182 |
183 | 3. 下面是由`Packet`结构体实现的两个接口`Header`和`Body`。思考:为什么不直接使用`Packet`或者定义两个子结构体?
184 |
185 | - 使用接口可以达到限制调用方法的目的,不同情况下使用不同方法集,开发者不会因为调用了不该调用的方法而掉坑里
186 | - 在语义上,`Packet`只是用于定义统一的数据包内容元素,并未给予任何关于数据结构方面(协议)的暗示、误导。因此不应该使用子结构体
187 |
188 | ```go
189 | type (
190 | // packet header interface
191 | Header interface {
192 | // Ptype returns the packet sequence
193 | Seq() uint64
194 | // SetSeq sets the packet sequence
195 | SetSeq(uint64)
196 | // Ptype returns the packet type, such as PULL, PUSH, REPLY
197 | Ptype() byte
198 | // Ptype sets the packet type
199 | SetPtype(byte)
200 | // Uri returns the URL string string
201 | Uri() string
202 | // SetUri sets the packet URL string
203 | SetUri(string)
204 | // Meta returns the metadata
205 | Meta() *utils.Args
206 | }
207 | // packet body interface
208 | Body interface {
209 | // BodyCodec returns the body codec type id
210 | BodyCodec() byte
211 | // SetBodyCodec sets the body codec type id
212 | SetBodyCodec(bodyCodec byte)
213 | // Body returns the body object
214 | Body() interface{}
215 | // SetBody sets the body object
216 | SetBody(body interface{})
217 | // SetNewBody resets the function of geting body.
218 | SetNewBody(newBodyFunc NewBodyFunc)
219 | // MarshalBody returns the encoding of body.
220 | MarshalBody() ([]byte, error)
221 | // UnmarshalNewBody unmarshal the encoded data to a new body.
222 | // Note: seq, ptype, uri must be setted already.
223 | UnmarshalNewBody(bodyBytes []byte) error
224 | // UnmarshalBody unmarshal the encoded data to the existed body.
225 | UnmarshalBody(bodyBytes []byte) error
226 | }
227 | // NewBodyFunc creates a new body by header.
228 | NewBodyFunc func(Header) interface{}
229 | )
230 | ```
231 |
232 | 4. 编译期校验`Packet`是否已实现`Header`与`Body`接口的技巧
233 |
234 | ```go
235 | var (
236 | _ Header = new(Packet)
237 | _ Body = new(Packet)
238 | )
239 | ```
240 |
241 | 5. 一种常见的自由赋值的函数用法,用于自由设置`Packet`的字段
242 |
243 | ```go
244 | // PacketSetting sets Header field.
245 | type PacketSetting func(*Packet)
246 |
247 | // WithSeq sets the packet sequence
248 | func WithSeq(seq uint64) PacketSetting {
249 | return func(p *Packet) {
250 | p.seq = seq
251 | }
252 | }
253 |
254 | // Ptype sets the packet type
255 | func WithPtype(ptype byte) PacketSetting {
256 | return func(p *Packet) {
257 | p.ptype = ptype
258 | }
259 | }
260 | ...
261 | ```
262 |
263 |
264 | ### 2 Socket接口
265 |
266 | `Socket`接口是对`net.Conn`的封装,通过协议接口`Proto`对数据包内容元素`Packet`进行封包、解包与IO传输操作。
267 |
268 | ```go
269 | type (
270 | // Socket is a generic stream-oriented network connection.
271 | //
272 | // Multiple goroutines may invoke methods on a Socket simultaneously.
273 | Socket interface {
274 | net.Conn
275 | // WritePacket writes header and body to the connection.
276 | // Note: must be safe for concurrent use by multiple goroutines.
277 | WritePacket(packet *Packet) error
278 | // ReadPacket reads header and body from the connection.
279 | // Note: must be safe for concurrent use by multiple goroutines.
280 | ReadPacket(packet *Packet) error
281 | // Public returns temporary public data of Socket.
282 | Public() goutil.Map
283 | // PublicLen returns the length of public data of Socket.
284 | PublicLen() int
285 | // Id returns the socket id.
286 | Id() string
287 | // SetId sets the socket id.
288 | SetId(string)
289 | // Reset reset net.Conn and ProtoFunc.
290 | Reset(netConn net.Conn, protoFunc ...ProtoFunc)
291 | }
292 | socket struct {
293 | net.Conn
294 | protocol Proto
295 | id string
296 | idMutex sync.RWMutex
297 | ctxPublic goutil.Map
298 | mu sync.RWMutex
299 | curState int32
300 | fromPool bool
301 | }
302 | )
303 | ```
304 |
305 | #### $ **Go技巧分享**
306 |
307 | 1. 为什么要对外提供接口,而不直接公开结构体?
308 |
309 | `socket`结构体通过匿名字段`net.Conn`的方式“继承”了底层的连接操作方法,并基于该匿名字段创建了协议对象。
310 |
311 | 所以不能允许外部直接通过`socket.Conn=newConn`的方式改变连接句柄。
312 |
313 | 使用`Socket`接口封装包外不可见的`socket`结构体可达到避免外部直接修改字段的目的。
314 |
315 | 2. 读写锁遵循最小化锁定的原则,且`defer`绝不是必须的,在确定运行安全的情况下尽量避免使用有性能消耗的`defer`。
316 |
317 | ```go
318 | func (s *socket) ReadPacket(packet *Packet) error {
319 | s.mu.RLock()
320 | protocol := s.protocol
321 | s.mu.RUnlock()
322 | return protocol.Unpack(packet)
323 | }
324 | ```
325 |
326 | ### 3 Proto协议接口
327 |
328 | `Proto`接口按照实现它的具体规则,对`Packet`数据包内容元素进行封包、解包、IO等操作。
329 |
330 | ```go
331 | type (
332 | // Proto pack/unpack protocol scheme of socket packet.
333 | Proto interface {
334 | // Version returns the protocol's id and name.
335 | Version() (byte, string)
336 | // Pack pack socket data packet.
337 | // Note: Make sure to write only once or there will be package contamination!
338 | Pack(*Packet) error
339 | // Unpack unpack socket data packet.
340 | // Note: Concurrent unsafe!
341 | Unpack(*Packet) error
342 | }
343 | // ProtoFunc function used to create a custom Proto interface.
344 | ProtoFunc func(io.ReadWriter) Proto
345 |
346 | // FastProto fast socket communication protocol.
347 | FastProto struct {
348 | id byte
349 | name string
350 | r io.Reader
351 | w io.Writer
352 | rMu sync.Mutex
353 | }
354 | )
355 | ```
356 |
357 | #### $ **Go技巧分享**
358 |
359 | 1. 将数据包的封包、解包操作封装为`Proto`接口,并定义一个默认实现(`FastProto`)。
360 | 这是框架设计中增强可定制性的一种有效手段。开发者既可以使用默认实现,也可以根据特殊需求定制自己的个性实现。
361 |
362 | 2. 使用`Packet`屏蔽不同协议的差异性:封包时以`Packet`的字段为内容元素进行数据序列化,解包时以`Packet`为内容模板进行数据的反序列化。
363 |
364 |
*---------------------------以下为`github.com/henrylee2cn/telepot/codec`包内容---------------------------*
365 |
366 | ### 4 Codec编解码
367 |
368 | `Codec`接口是`socket.Packet.body`的编解码器。TP已默认注册了JSON、Protobuf、String三种编解码器。
369 |
370 | ```go
371 | type (
372 | // Codec makes Encoder and Decoder
373 | Codec interface {
374 | // Id returns codec id.
375 | Id() byte
376 | // Name returns codec name.
377 | Name() string
378 | // Marshal returns the encoding of v.
379 | Marshal(v interface{}) ([]byte, error)
380 | // Unmarshal parses the encoded data and stores the result
381 | // in the value pointed to by v.
382 | Unmarshal(data []byte, v interface{}) error
383 | }
384 | )
385 | ```
386 |
387 | #### $ **Go技巧分享**
388 |
389 |
390 | 1. 下面`codecMap`变量的类型为什么不用关键字`type`定义?
391 |
392 | ```go
393 | var codecMap = struct {
394 | nameMap map[string]Codec
395 | idMap map[byte]Codec
396 | }{
397 | nameMap: make(map[string]Codec),
398 | idMap: make(map[byte]Codec),
399 | }
400 | ```
401 |
402 | Go语法允许我们在声明变量时临时定义类型并赋值。因为`codecMap`所属类型只会有一个全局唯一的实例,且不会用于其他变量类型声明上,所以直接在声明变量时声明类型可以令代码更简洁。
403 |
404 | 2. 常用的依赖注入实现方式,实现编解码器的自由定制
405 |
406 | ```go
407 | const (
408 | NilCodecId byte = 0
409 | NilCodecName string = ""
410 | )
411 |
412 | func Reg(codec Codec) {
413 | if codec.Id() == NilCodecId {
414 | panic(fmt.Sprintf("codec id can not be %d", NilCodecId))
415 | }
416 | if _, ok := codecMap.nameMap[codec.Name()]; ok {
417 | panic("multi-register codec name: " + codec.Name())
418 | }
419 | if _, ok := codecMap.idMap[codec.Id()]; ok {
420 | panic(fmt.Sprintf("multi-register codec id: %d", codec.Id()))
421 | }
422 | codecMap.nameMap[codec.Name()] = codec
423 | codecMap.idMap[codec.Id()] = codec
424 | }
425 |
426 | func Get(id byte) (Codec, error) {
427 | codec, ok := codecMap.idMap[id]
428 | if !ok {
429 | return nil, fmt.Errorf("unsupported codec id: %d", id)
430 | }
431 | return codec, nil
432 | }
433 |
434 | func GetByName(name string) (Codec, error) {
435 | codec, ok := codecMap.nameMap[name]
436 | if !ok {
437 | return nil, fmt.Errorf("unsupported codec name: %s", name)
438 | }
439 | return codec, nil
440 | }
441 | ```
442 |
443 |
*---------------------------以下为`github.com/henrylee2cn/telepot/xfer`包内容---------------------------*
444 |
445 | ### 5 XferPipe数据编码管道
446 |
447 | `XferPipe`接口用于对数据包进行一系列自定义处理加工,如gzip压缩、加密、校验等。
448 |
449 | ```go
450 | type (
451 | // XferPipe transfer filter pipe, handlers from outer-most to inner-most.
452 | // Note: the length can not be bigger than 255!
453 | XferPipe struct {
454 | filters []XferFilter
455 | }
456 | // XferFilter handles byte stream of packet when transfer.
457 | XferFilter interface {
458 | Id() byte
459 | OnPack([]byte) ([]byte, error)
460 | OnUnpack([]byte) ([]byte, error)
461 | }
462 | )
463 |
464 | var xferFilterMap = struct {
465 | idMap map[byte]XferFilter
466 | }{
467 | idMap: make(map[byte]XferFilter),
468 | }
469 | ```
470 |
471 | `teleport/xfer`包的设计与`teleport/codec`类似,`xferFilterMap`为注册中心,提供注册、查询、执行等功能。
472 |
473 |
*---------------------------以下为`github.com/henrylee2cn/telepot`包内容---------------------------*
474 |
475 | ### 6 Peer通信端点
476 |
477 | Peer结构体是TP的一个通信端点,它可以是服务端也可以是客户端,甚至可以同时是服务端与客户端。因此,TP是端对端对等通信的。
478 |
479 | ```go
480 | type Peer struct {
481 | PullRouter *Router
482 | PushRouter *Router
483 | // Has unexported fields.
484 | }
485 | func NewPeer(cfg PeerConfig, plugin ...Plugin) *Peer
486 | func (p *Peer) Close() (err error)
487 | func (p *Peer) CountSession() int
488 | func (p *Peer) Dial(addr string, protoFunc ...socket.ProtoFunc) (Session, *Rerror)
489 | func (p *Peer) DialContext(ctx context.Context, addr string, protoFunc ...socket.ProtoFunc) (Session, *Rerror)
490 | func (p *Peer) GetSession(sessionId string) (Session, bool)
491 | func (p *Peer) Listen(protoFunc ...socket.ProtoFunc) error
492 | func (p *Peer) RangeSession(fn func(sess Session) bool)
493 | func (p *Peer) ServeConn(conn net.Conn, protoFunc ...socket.ProtoFunc) (Session, error)
494 | ```
495 |
496 | - 通信端点介绍
497 |
498 | 1. Peer配置信息
499 |
500 | ```go
501 | type PeerConfig struct {
502 | TlsCertFile string
503 | TlsKeyFile string
504 | DefaultReadTimeout time.Duration
505 | DefaultWriteTimeout time.Duration
506 | SlowCometDuration time.Duration
507 | DefaultBodyCodec string
508 | PrintDetail bool
509 | CountTime bool
510 | DefaultDialTimeout time.Duration
511 | RedialTimes int32
512 | Network string
513 | ListenAddress string
514 | }
515 | ```
516 |
517 | 2. Peer的功能列表
518 |
519 | - 提供路由功能
520 | - 作为服务端可同时支持监听多个地址端口
521 | - 作为客户端可与任意服务端建立连接
522 | - 提供会话查询功能
523 | - 支持TLS证书安全加密
524 | - 设置默认的建立连接和读、写超时
525 | - 慢响应阀值(超出后运行日志由INFO提升为WARN)
526 | - 支持打印body
527 | - 支持在运行日志中增加耗时统计
528 |
529 | #### $ **Go技巧分享**
530 |
531 | 一个Go协程的初始堆栈大小为2KB(在运行过程中可以动态扩展大小)如在高并发服务中不加限制地频繁创建/销毁协程,很容易造成内存资源耗尽,且对GC压力也会很大。因此,TP内部采用协程资源池来管控协程,可以大大降低服务器内存与CPU的压力。(该思路源于fasthttp)
532 |
533 | 协程资源池的源码实现在本人[goutil](https://github.com/henrylee2cn/goutil)库中的`github.com/henrylee2cn/goutil/pool`。下面是TP的二次封装(保守认为一个goroutine平均占用8KB):
534 |
535 | ```go
536 | var (
537 | _maxGoroutinesAmount = (1024 * 1024 * 8) / 8 // max memory 8GB (8KB/goroutine)
538 | _maxGoroutineIdleDuration time.Duration
539 | _gopool = pool.NewGoPool(_maxGoroutinesAmount, _maxGoroutineIdleDuration)
540 | )
541 | // SetGopool set or reset go pool config.
542 | // Note: Make sure to call it before calling NewPeer() and Go()
543 | func SetGopool(maxGoroutinesAmount int, maxGoroutineIdleDuration time.Duration) {
544 | _maxGoroutinesAmount, _maxGoroutineIdleDuration := maxGoroutinesAmount, maxGoroutineIdleDuration
545 | if _gopool != nil {
546 | _gopool.Stop()
547 | }
548 | _gopool = pool.NewGoPool(_maxGoroutinesAmount, _maxGoroutineIdleDuration)
549 | }
550 | // Go similar to go func, but return false if insufficient resources.
551 | func Go(fn func()) bool {
552 | if err := _gopool.Go(fn); err != nil {
553 | Warnf("%s", err.Error())
554 | return false
555 | }
556 | return true
557 | }
558 | // AnywayGo similar to go func, but concurrent resources are limited.
559 | func AnywayGo(fn func()) {
560 | TRYGO:
561 | if !Go(fn) {
562 | time.Sleep(time.Second)
563 | goto TRYGO
564 | }
565 | }
566 | ```
567 |
568 | 每当Peer创建一个session时,都有调用上述`Go`函数进行并发执行:
569 |
570 | ```go
571 | func (p *Peer) DialContext(ctx context.Context, addr string, protoFunc ...socket.ProtoFunc) (Session, *Rerror) {
572 | ...
573 | Go(sess.startReadAndHandle)
574 | ...
575 | }
576 | func (p *Peer) Listen(protoFunc ...socket.ProtoFunc) error {
577 | lis, err := listen(p.network, p.listenAddr, p.tlsConfig)
578 | if err != nil {
579 | Fatalf("%v", err)
580 | }
581 | defer lis.Close()
582 | p.listen = lis
583 |
584 | network := lis.Addr().Network()
585 | addr := lis.Addr().String()
586 | Printf("listen ok (network:%s, addr:%s)", network, addr)
587 |
588 | var (
589 | tempDelay time.Duration // how long to sleep on accept failure
590 | closeCh = p.closeCh
591 | )
592 | for {
593 | conn, e := lis.Accept()
594 | ...
595 | AnywayGo(func() {
596 | if c, ok := conn.(*tls.Conn); ok {
597 | ...
598 | }
599 | var sess = newSession(p, conn, protoFunc)
600 | if rerr := p.pluginContainer.PostAccept(sess); rerr != nil {
601 | sess.Close()
602 | return
603 | }
604 | Tracef("accept session(network:%s, addr:%s) ok", network, sess.RemoteIp(), sess.Id())
605 | p.sessHub.Set(sess)
606 | sess.startReadAndHandle()
607 | })
608 | }
609 | }
610 | ```
611 |
612 | ### 7 Router路由器
613 |
614 | TP是对等通信,路由不再是服务端的专利,只要是Peer端点就支持注册`PULL`和`PUSH`这两类消息处理路由。
615 |
616 | ```go
617 | type Router struct {
618 | handlers map[string]*Handler
619 | unknownApiType **Handler
620 | // only for register router
621 | pathPrefix string
622 | pluginContainer PluginContainer
623 | typ string
624 | maker HandlersMaker
625 | }
626 |
627 | func (r *Router) Group(pathPrefix string, plugin ...Plugin) *Router
628 | func (r *Router) Reg(ctrlStruct interface{}, plugin ...Plugin)
629 | func (r *Router) SetUnknown(unknownHandler interface{}, plugin ...Plugin)
630 | ```
631 |
632 | #### $ **Go技巧分享**
633 |
634 | 1. 根据`maker HandlersMaker`(Handler的构造函数)字段的不同,分别实现了`PullRouter`和`PushRouter`两类路由。
635 |
636 | ```go
637 | // HandlersMaker makes []*Handler
638 | type HandlersMaker func(
639 | pathPrefix string,
640 | ctrlStruct interface{},
641 | pluginContainer PluginContainer,
642 | ) ([]*Handler, error)
643 | ```
644 |
645 | 2. 简洁地路由分组实现:
646 |
647 | * 继承各级路由的共享字段:`handlers`、`unknownApiType`、`maker`
648 | * 在上级路由节点的`pathPrefix`、`pluginContainer`字段基础上追加当前节点信息
649 |
650 | ```go
651 | // Group add handler group.
652 | func (r *Router) Group(pathPrefix string, plugin ...Plugin) *Router {
653 | pluginContainer, err := r.pluginContainer.cloneAdd(plugin...)
654 | if err != nil {
655 | Fatalf("%v", err)
656 | }
657 | warnInvaildRouterHooks(plugin)
658 | return &Router{
659 | handlers: r.handlers,
660 | unknownApiType: r.unknownApiType,
661 | pathPrefix: path.Join(r.pathPrefix, pathPrefix),
662 | pluginContainer: pluginContainer,
663 | maker: r.maker,
664 | }
665 | }
666 | ```
667 |
668 | ### 8 控制器
669 |
670 | 控制器是指用于提供Handler操作的结构体。
671 |
672 | #### $ **Go技巧分享**
673 |
674 | 1. Go没有泛型,我们通常使用`interface{}`空接口来代替。
675 | 但是,空接口不能用于表示结构体的方法。
676 |
677 | 下面是控制器结构体及其方法的模型定义:
678 |
679 | PullController Model:
680 |
681 | ```go
682 | type Aaa struct {
683 | tp.PullCtx
684 | }
685 | // XxZz register the route: /aaa/xx_zz
686 | func (x *Aaa) XxZz(args *) (, *tp.Rerror) {
687 | ...
688 | return r, nil
689 | }
690 | // YyZz register the route: /aaa/yy_zz
691 | func (x *Aaa) YyZz(args *) (, *tp.Rerror) {
692 | ...
693 | return r, nil
694 | }
695 | ```
696 |
697 | PushController Model:
698 |
699 | ```go
700 | type Bbb struct {
701 | tp.PushCtx
702 | }
703 | // XxZz register the route: /bbb/yy_zz
704 | func (b *Bbb) XxZz(args *) {
705 | ...
706 | return r, nil
707 | }
708 | // YyZz register the route: /bbb/yy_zz
709 | func (b *Bbb) YyZz(args *) {
710 | ...
711 | return r, nil
712 | }
713 | ```
714 |
715 | 以PullController为例,使用`reflect`反射包对未知类型的结构体进行模型验证:
716 |
717 | ```go
718 | func pullHandlersMaker(pathPrefix string, ctrlStruct interface{}, pluginContainer PluginContainer) ([]*Handler, error) {
719 | var (
720 | ctype = reflect.TypeOf(ctrlStruct)
721 | handlers = make([]*Handler, 0, 1)
722 | )
723 |
724 | if ctype.Kind() != reflect.Ptr {
725 | return nil, errors.Errorf("register pull handler: the type is not struct point: %s", ctype.String())
726 | }
727 |
728 | var ctypeElem = ctype.Elem()
729 | if ctypeElem.Kind() != reflect.Struct {
730 | return nil, errors.Errorf("register pull handler: the type is not struct point: %s", ctype.String())
731 | }
732 |
733 | if _, ok := ctrlStruct.(PullCtx); !ok {
734 | return nil, errors.Errorf("register pull handler: the type is not implemented PullCtx interface: %s", ctype.String())
735 | }
736 |
737 | iType, ok := ctypeElem.FieldByName("PullCtx")
738 | if !ok || !iType.Anonymous {
739 | return nil, errors.Errorf("register pull handler: the struct do not have anonymous field PullCtx: %s", ctype.String())
740 | }
741 | ...
742 | for m := 0; m < ctype.NumMethod(); m++ {
743 | method := ctype.Method(m)
744 | mtype := method.Type
745 | mname := method.Name
746 | // Method must be exported.
747 | if method.PkgPath != "" || isPullCtxType(mname) {
748 | continue
749 | }
750 | // Method needs two ins: receiver, *args.
751 | if mtype.NumIn() != 2 {
752 | return nil, errors.Errorf("register pull handler: %s.%s needs one in argument, but have %d", ctype.String(), mname, mtype.NumIn())
753 | }
754 | // Receiver need be a struct pointer.
755 | structType := mtype.In(0)
756 | if structType.Kind() != reflect.Ptr || structType.Elem().Kind() != reflect.Struct {
757 | return nil, errors.Errorf("register pull handler: %s.%s receiver need be a struct pointer: %s", ctype.String(), mname, structType)
758 | }
759 | // First arg need be exported or builtin, and need be a pointer.
760 | argType := mtype.In(1)
761 | if !goutil.IsExportedOrBuiltinType(argType) {
762 | return nil, errors.Errorf("register pull handler: %s.%s args type not exported: %s", ctype.String(), mname, argType)
763 | }
764 | if argType.Kind() != reflect.Ptr {
765 | return nil, errors.Errorf("register pull handler: %s.%s args type need be a pointer: %s", ctype.String(), mname, argType)
766 | }
767 | // Method needs two outs: reply error.
768 | if mtype.NumOut() != 2 {
769 | return nil, errors.Errorf("register pull handler: %s.%s needs two out arguments, but have %d", ctype.String(), mname, mtype.NumOut())
770 | }
771 | // Reply type must be exported.
772 | replyType := mtype.Out(0)
773 | if !goutil.IsExportedOrBuiltinType(replyType) {
774 | return nil, errors.Errorf("register pull handler: %s.%s first reply type not exported: %s", ctype.String(), mname, replyType)
775 | }
776 |
777 | // The return type of the method must be Error.
778 | if returnType := mtype.Out(1); !isRerrorType(returnType.String()) {
779 | return nil, errors.Errorf("register pull handler: %s.%s second reply type %s not *tp.Rerror", ctype.String(), mname, returnType)
780 | }
781 | ...
782 | }
783 | ...
784 | }
785 | ```
786 |
787 | 2. 参考HTTP的成熟经验,TP的路由路径采用类URL格式,且支持query参数:如`/a/b?n=1&m=e`
788 |
789 | ### 9 Unknown操作函数
790 |
791 | TP可通过`func (r *Router) SetUnknown(unknownHandler interface{}, plugin ...Plugin)`方法设置默认Handler,用于处理未找到路由的`PULL`或`PUSH`消息。
792 |
793 | UnknownPullHandler Type:
794 |
795 | ```go
796 | func(ctx UnknownPullCtx) (interface{}, *Rerror) {
797 | ...
798 | return r, nil
799 | }
800 | ```
801 |
802 | UnknownPushHandler Type:
803 |
804 | ```go
805 | func(ctx UnknownPushCtx)
806 | ```
807 |
808 | ### 10 Handler的构造
809 |
810 | ```go
811 | // Handler pull or push handler type info
812 | Handler struct {
813 | name string
814 | isUnknown bool
815 | argElem reflect.Type
816 | reply reflect.Type // only for pull handler doc
817 | handleFunc func(*readHandleCtx, reflect.Value)
818 | unknownHandleFunc func(*readHandleCtx)
819 | pluginContainer PluginContainer
820 | }
821 | ```
822 |
823 | 通过`HandlersMaker`对Controller各个方法进行解析,构造出相应数量的Handler。以`pullHandlersMaker`函数为例:
824 |
825 | ```go
826 | func pullHandlersMaker(pathPrefix string, ctrlStruct interface{}, pluginContainer PluginContainer) ([]*Handler, error) {
827 | var (
828 | ctype = reflect.TypeOf(ctrlStruct)
829 | handlers = make([]*Handler, 0, 1)
830 | )
831 | ...
832 | var ctypeElem = ctype.Elem()
833 | ...
834 | iType, ok := ctypeElem.FieldByName("PullCtx")
835 | ...
836 | var pullCtxOffset = iType.Offset
837 |
838 | if pluginContainer == nil {
839 | pluginContainer = newPluginContainer()
840 | }
841 |
842 | type PullCtrlValue struct {
843 | ctrl reflect.Value
844 | ctxPtr *PullCtx
845 | }
846 | var pool = &sync.Pool{
847 | New: func() interface{} {
848 | ctrl := reflect.New(ctypeElem)
849 | pullCtxPtr := ctrl.Pointer() + pullCtxOffset
850 | ctxPtr := (*PullCtx)(unsafe.Pointer(pullCtxPtr))
851 | return &PullCtrlValue{
852 | ctrl: ctrl,
853 | ctxPtr: ctxPtr,
854 | }
855 | },
856 | }
857 |
858 | for m := 0; m < ctype.NumMethod(); m++ {
859 | method := ctype.Method(m)
860 | mtype := method.Type
861 | mname := method.Name
862 | ...
863 | var methodFunc = method.Func
864 | var handleFunc = func(ctx *readHandleCtx, argValue reflect.Value) {
865 | obj := pool.Get().(*PullCtrlValue)
866 | *obj.ctxPtr = ctx
867 | rets := methodFunc.Call([]reflect.Value{obj.ctrl, argValue})
868 | ctx.output.SetBody(rets[0].Interface())
869 | rerr, _ := rets[1].Interface().(*Rerror)
870 | if rerr != nil {
871 | rerr.SetToMeta(ctx.output.Meta())
872 |
873 | } else if ctx.output.Body() != nil && ctx.output.BodyCodec() == codec.NilCodecId {
874 | ctx.output.SetBodyCodec(ctx.input.BodyCodec())
875 | }
876 | pool.Put(obj)
877 | }
878 |
879 | handlers = append(handlers, &Handler{
880 | name: path.Join(pathPrefix, ctrlStructSnakeName(ctype), goutil.SnakeString(mname)),
881 | handleFunc: handleFunc,
882 | argElem: argType.Elem(),
883 | reply: replyType,
884 | pluginContainer: pluginContainer,
885 | })
886 | }
887 | return handlers, nil
888 | }
889 | ```
890 |
891 | #### $ **Go技巧分享**
892 |
893 | - 对不可变的部分进行预处理获得闭包变量,抽离可变部分的逻辑构造子函数。在路由处理过程中直接执行这些`handleFunc`子函数可达到显著提升性能的目的
894 | - 使用反射来创建任意类型的实例并调用其方法,适用于类型或方法不固定的情况
895 | - 使用对象池来复用`PullCtrlValue`,可以降低GC开销与内存占用
896 | - 通过unsafe获取`ctrlStruct.PullCtx`字段的指针偏移量,进而可以快速获取该字段的值
897 |
898 | ### 11 Session会话
899 |
900 | Session是封装了socket连接的会话管理实例。它使用一个包外不可见的结构体`session`来实现会话相关的三个接口:
901 | `PreSession`、`Session`、`PostSession`。(此处session实现多接口的做法类似于Packet)
902 |
903 |
904 | ```go
905 | type (
906 | PreSession interface {
907 | ...
908 | }
909 | Session interface {
910 | // SetId sets the session id.
911 | SetId(newId string)
912 | // Close closes the session.
913 | Close() error
914 | // Id returns the session id.
915 | Id() string
916 | // Health checks if the session is ok.
917 | Health() bool
918 | // Peer returns the peer.
919 | Peer() *Peer
920 | // AsyncPull sends a packet and receives reply asynchronously.
921 | // If the args is []byte or *[]byte type, it can automatically fill in the body codec name.
922 | AsyncPull(uri string, args interface{}, reply interface{}, done chan *PullCmd, setting ...socket.PacketSetting)
923 | // Pull sends a packet and receives reply.
924 | // Note:
925 | // If the args is []byte or *[]byte type, it can automatically fill in the body codec name;
926 | // If the session is a client role and PeerConfig.RedialTimes>0, it is automatically re-called once after a failure.
927 | Pull(uri string, args interface{}, reply interface{}, setting ...socket.PacketSetting) *PullCmd
928 | // Push sends a packet, but do not receives reply.
929 | // Note:
930 | // If the args is []byte or *[]byte type, it can automatically fill in the body codec name;
931 | // If the session is a client role and PeerConfig.RedialTimes>0, it is automatically re-called once after a failure.
932 | Push(uri string, args interface{}, setting ...socket.PacketSetting) *Rerror
933 | // ReadTimeout returns readdeadline for underlying net.Conn.
934 | ReadTimeout() time.Duration
935 | // RemoteIp returns the remote peer ip.
936 | RemoteIp() string
937 | // LocalIp returns the local peer ip.
938 | LocalIp() string
939 | // ReadTimeout returns readdeadline for underlying net.Conn.
940 | SetReadTimeout(duration time.Duration)
941 | // WriteTimeout returns writedeadline for underlying net.Conn.
942 | SetWriteTimeout(duration time.Duration)
943 | // Socket returns the Socket.
944 | // Socket() socket.Socket
945 | // WriteTimeout returns writedeadline for underlying net.Conn.
946 | WriteTimeout() time.Duration
947 | // Public returns temporary public data of session(socket).
948 | Public() goutil.Map
949 | // PublicLen returns the length of public data of session(socket).
950 | PublicLen() int
951 | }
952 | PostSession interface {
953 | ...
954 | }
955 | session struct {
956 | ...
957 | }
958 | )
959 | ```
960 |
961 | Session采用读写异步的方式处理通信消息。在创建Session后,立即启动一个循环读取数据包的协程,并为每个成功读取的数据包创建一个处理协程。
962 |
963 | 而写操作则是由session.Pull、session.Push或者Handler三种方式来触发执行。
964 |
965 | #### $ **Go技巧分享**
966 |
967 | 在以客户端角色执行PULL请求时,Session支持同步和异步两种方式。这是Go的一种经典的兼容同步异步调用的技巧:
968 |
969 | ```go
970 | func (s *session) AsyncPull(uri string, args interface{}, reply interface{}, done chan *PullCmd, setting ...socket.PacketSetting) {
971 | ...
972 | cmd := &PullCmd{
973 | sess: s,
974 | output: output,
975 | reply: reply,
976 | doneChan: done,
977 | start: s.peer.timeNow(),
978 | public: goutil.RwMap(),
979 | }
980 | ...
981 | if err := s.write(output); err != nil {
982 | cmd.rerr = rerror_writeFailed.Copy()
983 | cmd.rerr.Detail = err.Error()
984 | cmd.done()
985 | return
986 | }
987 | s.peer.pluginContainer.PostWritePull(cmd)
988 | }
989 |
990 | // Pull sends a packet and receives reply.
991 | // If the args is []byte or *[]byte type, it can automatically fill in the body codec name.
992 | func (s *session) Pull(uri string, args interface{}, reply interface{}, setting ...socket.PacketSetting) *PullCmd {
993 | doneChan := make(chan *PullCmd, 1)
994 | s.AsyncPull(uri, args, reply, doneChan, setting...)
995 | pullCmd := <-doneChan
996 | close(doneChan)
997 | return pullCmd
998 | }
999 | ```
1000 |
1001 | 实现步骤:
1002 |
1003 | 1. 在返回结果的结构体中绑定一个chan管道
1004 | 2. 在另一个协程中进行结果计算
1005 | 3. 将该chan做为返回值返回给调用者
1006 | 4. 将计算结果写入该chan中
1007 | 5. 调用者从chan中读出该结果
1008 | 6. (同步方式是对异步方式的封装,等待从chan中读到结果后,再将该结果作为返回值返回)
1009 |
1010 |
1011 | ### 12 Context上下文
1012 |
1013 | 类似常见的Go HTTP框架,TP同样提供了Context上下文。它携带Handler操作相关的参数,如Peer、Session、Packet、PublicData等。
1014 |
1015 | 根据调用场景的不同,定义不同接口来限制其方法列表。
1016 |
1017 | 此外,TP的平滑关闭、平滑重启也是建立在对Context的使用状态监控的基础上。
1018 |
1019 | ```go
1020 | type (
1021 | BackgroundCtx interface {
1022 | ...
1023 | }
1024 | PushCtx interface {
1025 | ...
1026 | }
1027 | PullCtx interface {
1028 | ...
1029 | }
1030 | UnknownPushCtx interface {
1031 | ...
1032 | }
1033 | UnknownPullCtx interface {
1034 | ...
1035 | }
1036 | WriteCtx interface {
1037 | ...
1038 | }
1039 | ReadCtx interface {
1040 | ...
1041 | }
1042 | readHandleCtx struct {
1043 | sess *session
1044 | input *socket.Packet
1045 | output *socket.Packet
1046 | apiType *Handler
1047 | arg reflect.Value
1048 | pullCmd *PullCmd
1049 | uri *url.URL
1050 | query url.Values
1051 | public goutil.Map
1052 | start time.Time
1053 | cost time.Duration
1054 | pluginContainer PluginContainer
1055 | next *readHandleCtx
1056 | }
1057 | )
1058 | ```
1059 |
1060 | ### 13 Plugin插件
1061 |
1062 | TP提供了插件功能,具有完备的挂载点,便于开发者实现丰富的功能。例如身份认证、心跳、微服务注册中心、信息统计等等。
1063 |
1064 | ```go
1065 | type (
1066 | Plugin interface {
1067 | Name() string
1068 | }
1069 | PostRegPlugin interface {
1070 | Plugin
1071 | PostReg(*Handler) *Rerror
1072 | }
1073 | PostDialPlugin interface {
1074 | Plugin
1075 | PostDial(PreSession) *Rerror
1076 | }
1077 | ...
1078 | PostReadReplyBodyPlugin interface {
1079 | Plugin
1080 | PostReadReplyBody(ReadCtx) *Rerror
1081 | }
1082 | ...
1083 | // PluginContainer plugin container that defines base methods to manage plugins.
1084 | PluginContainer interface {
1085 | Add(plugins ...Plugin) error
1086 | Remove(pluginName string) error
1087 | GetByName(pluginName string) Plugin
1088 | GetAll() []Plugin
1089 | PostReg(*Handler) *Rerror
1090 | PostDial(PreSession) *Rerror
1091 | ...
1092 | PostReadReplyBody(ReadCtx) *Rerror
1093 | ...
1094 | cloneAdd(...Plugin) (PluginContainer, error)
1095 | }
1096 | pluginContainer struct {
1097 | plugins []Plugin
1098 | }
1099 | )
1100 |
1101 | func (p *pluginContainer) PostReg(h *Handler) *Rerror {
1102 | var rerr *Rerror
1103 | for _, plugin := range p.plugins {
1104 | if _plugin, ok := plugin.(PostRegPlugin); ok {
1105 | if rerr = _plugin.PostReg(h); rerr != nil {
1106 | Fatalf("%s-PostRegPlugin(%s)", plugin.Name(), rerr.String())
1107 | return rerr
1108 | }
1109 | }
1110 | }
1111 | return nil
1112 | }
1113 | func (p *pluginContainer) PostDial(sess PreSession) *Rerror {
1114 | var rerr *Rerror
1115 | for _, plugin := range p.plugins {
1116 | if _plugin, ok := plugin.(PostDialPlugin); ok {
1117 | if rerr = _plugin.PostDial(sess); rerr != nil {
1118 | Debugf("dial fail (addr: %s, id: %s): %s-PostDialPlugin(%s)", sess.RemoteIp(), sess.Id(), plugin.Name(), rerr.String())
1119 | return rerr
1120 | }
1121 | }
1122 | }
1123 | return nil
1124 | }
1125 | func (p *pluginContainer) PostReadReplyBody(ctx ReadCtx) *Rerror {
1126 | var rerr *Rerror
1127 | for _, plugin := range p.plugins {
1128 | if _plugin, ok := plugin.(PostReadReplyBodyPlugin); ok {
1129 | if rerr = _plugin.PostReadReplyBody(ctx); rerr != nil {
1130 | Errorf("%s-PostReadReplyBodyPlugin(%s)", plugin.Name(), rerr.String())
1131 | return rerr
1132 | }
1133 | }
1134 | }
1135 | return nil
1136 | }
1137 | ```
1138 |
1139 | #### $ **Go技巧分享**
1140 |
1141 | Go接口断言的灵活运用,实现插件及其管理容器:
1142 |
1143 | 1. 定义基础接口并创建统一管理容器
1144 | 2. 在实现基础接口的基础上,增加个性化接口(具体挂载点)的实现,将其注册进基础接口管理容器
1145 | 3. 管理容器使用断言的方法筛选出指定挂载点的插件并执行
1146 |
1147 | -------------------------
1148 |
1149 | 原文二维码
1150 |
1151 | 
1152 |
--------------------------------------------------------------------------------
/01/src/qrcode-viewfile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andeya/erpc-doc/ec414b819b4a15f2fda66454b31d20bbf01cd1ce/01/src/qrcode-viewfile.png
--------------------------------------------------------------------------------
/01/src/rpc_compare.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andeya/erpc-doc/ec414b819b4a15f2fda66454b31d20bbf01cd1ce/01/src/rpc_compare.png
--------------------------------------------------------------------------------
/02/README.md:
--------------------------------------------------------------------------------
1 | # 聊聊 Go Socket 框架 Teleport 的设计思路
2 |
3 |
4 |
5 | **项目源码**
6 |
7 | teleport:https://github.com/henrylee2cn/teleport
8 |
9 | ## 背景
10 |
11 | 大家在进行业务开发时,是否是否遇到过下列问题,并且无法在Go语言开源生态中找到一套完整的解决方案?
12 |
13 | - 高性能、可靠地通信?
14 | - 开发效率不高?
15 | - 无法自定义应用层协议?
16 | - 想要动态协商Body编码类型(如JSON、protobuf等)?
17 | - 不能以简洁的RPC方式进行业务开发?
18 | - 没有灵活的插件扩展机制?
19 | - 不支持服务端向客户端主动推送消息?
20 | - 特殊场景时需要连接管理,如多种连接类型、会话管理?
21 | - 使用了非HTTP协议框架,但不能很好的兼容HTTP协议,无法方便地与第三方对接?
22 |
23 |
24 |
25 | 我对于常见的一些相关开源项目做了一次粗略调查,发现迄今为止,除今天我要分享的这款 [teleport](https://github.com/henrylee2cn/teleport) 框架外(确切讲还包括由teleport扩展而来的微服务框架 [tp-micro](https://github.com/xiaoenai/tp-micro)),貌似并没有另外一款Go语言的开源框架能够同时解决上述问题:
26 |
27 | | 框架 | 描述 | 高性能 |高效开发|DIY应用层协议 | Body编码协商 | RPC范式 | 插件 |推送|连接管理|兼容HTTP协议|
28 | | ------ | ------ | ---------------- | ---------------- | ------------- | -------- | -------- | -------- | -------- | -------- | -------- |
29 | | **teleport** | **TCP socket 框架** | ★★★★ | ✓ | ✓ | ✓ |✓|✓|✓|✓|✓|
30 | | net | 标准包网络工具 | ★★★★★ | x | ✓ | x |x|x|✓|✓|x|
31 | | net/rpc | 标准包RPC | ★★★★☆ | x | x | x |✓|x|x|x|x|
32 | | net/http(2) | 标准包HTTP2 | ★★★☆ | x | x | ✓ |x|x|✓|x|✓|
33 | | gRPC | 谷歌出品的RPC框架 | ★★★ | ✓ | x | ✓ |✓|x|✓|x|✓|
34 | | rpcx | net/rpc的扩展框架 | ★★★★ | ✓ | x | x |✓|✓|✓|x|✓|
35 |
36 |
37 |
38 | ## 概述
39 |
40 | [teleport](https://github.com/henrylee2cn/teleport) 就是在上述需求背景下被创造出来,成为一个通用、高效、灵活的Socket框架。
41 |
42 | 它可以用于Peer-Peer对等通信、RPC、长连接网关、微服务、推送服务,游戏服务等领域。
43 |
44 | 其主要特性如下:
45 |
46 | | * | * | * | * | * | * | * | * | * |
47 | | :-----------: | :------: | :-----------: | :----------: | :------: | :-----: | :------: | :----------------------------------------------------: | :----------: |
48 | | 高性能 | 高效开发 | DIY应用层协议 | Body编码协商 | RPC范式 | 插件 | 推送 | 连接管理
(Socket文件描述符/
会话管理/上下文等) | 兼容HTTP协议 |
49 | | 平滑关闭/升级 | Log接口 | 非阻塞异步IO | 断线重连 | 对等通信 | 对等API | 反向代理 | 慢响应报警 | ...... |
50 |
51 | TODO:尚未提供多语言客户端版本
52 |
53 |
54 |
55 | ## 架构
56 |
57 |
58 |
59 | ### 设计原则
60 |
61 | - 面向接口设计,保证代码稳定,提供灵活定制
62 | - 抽象核心模型,保持最简化
63 | - 分层设计,自下而上逐层封装,利于稳定和维护
64 |
65 | - 充分利用协程,且保证可控、可复用
66 |
67 |
68 |
69 | ### 架构示意图
70 |
71 | 
72 |
73 | 注:“tp” 是 teleport 的包名,因此它代指 “teleport”。
74 |
75 |
76 |
77 | ## 简单的性能对比图
78 |
79 |
80 | Environment | Throughputs | Mean Latency | P99 Latency |
81 |
82 |  |
83 |  |
84 |  |
85 |  |
86 |
87 |
88 |
89 |
90 | ## 为兼容HTTP做准备
91 |
92 | 兼容 HTTP 最好的办法就是在设计应用层协议之初就考虑到进去。因此,teleport 对应用层协议报文的属性做了如下抽象:
93 |
94 | - Size 整个报文的长度
95 | - Transfer-Filter-Pipeline 报文数据过滤处理管道
96 | - Header
97 | - Seq 消息序号(因为是异步通信)
98 | - Mtype 消息类型(如PULL、REPLY、PUSH)
99 | - URI 资源标识符(对照常见RPC框架中的method,但可以更好地兼容HTTP)
100 | - Meta 元信息(如错误信息、内容协商信息等,对照HTTP Header)
101 | - Body
102 | - BodyCodec 消息正文的编码类型(如JSON、Protobuf)
103 | - Body 消息正文
104 |
105 |
106 |
107 | 从下图 teleport 报文属性与 HTTP 报文对比中,不难发现它们有共通之处。
108 |
109 | 
110 |
111 | ## 如何实现DIY应用层协议?
112 |
113 | 应用层协议是指建立在 TCP 协议之上的报文协议。我们希望开发者自己定制该协议,这样更具备灵活性,比如protobuf、thrift等。
114 |
115 |
116 |
117 | 首先要做的第一件事是:
118 |
119 | 抽象出一个 Message 对象,为应用层协议接口提供字节流序列化与反序列化模板。
120 |
121 |
122 |
123 | ### Step1: 抽象 Message 对象
124 |
125 | 在 teleport/socket 包中抽象出 Message 结构体(上面已经介绍过了)
126 |
127 |
128 |
129 | ### Step2: 抽象 Proto 协议接口
130 |
131 | 提供 Proto 协议接口,对 Message 对象进行序列化与反序列化,从而支持开发者的自定义实现自己的协议格式,其接口声明如下:
132 |
133 | ```go
134 | type Proto interface {
135 | Version() (byte, string)
136 | Pack(*Message) error
137 | Unpack(*Message) error
138 | }
139 | ```
140 |
141 | 解释:
142 |
143 | - `Version` :实现该协议接口的版本号
144 |
145 | - `Pack` :按照接口实现的规则,将 Message 的属性序列化为字节流
146 |
147 | - `Unpack` :按照接口实现的规则,将字节流反序列化进一个 Message 对象
148 |
149 |
150 | 目前框架已经提供三种协议:Raw、JSON、Protobuf。
151 |
152 | 其中以Raw为示例,展示如下:
153 |
154 | ```
155 | # raw protocol format(Big Endian):
156 |
157 | {4 bytes message length}
158 | {1 byte protocol version}
159 | {1 byte transfer pipe length}
160 | {transfer pipe IDs}
161 | # The following is handled data by transfer pipe
162 | {2 bytes sequence length}
163 | {sequence}
164 | {1 byte message type} # e.g. CALL:1; REPLY:2; PUSH:3
165 | {2 bytes URI length}
166 | {URI}
167 | {2 bytes metadata length}
168 | {metadata(urlencoded)}
169 | {1 byte body codec id}
170 | {body}
171 | ```
172 |
173 |
174 |
175 | ## 如何实现 Body 编码协商?
176 |
177 | 在实际业务场景中,报文的类型是多种多样的,所以 teleport 使用 `Codec` 接口对消息正文(Message Body)进行编解码。
178 |
179 | ```go
180 | type Codec interface {
181 | Id() byte
182 | Name() string
183 | Marshal(v interface{}) ([]byte, error)
184 | Unmarshal(data []byte, v interface{}) error
185 | }
186 | ```
187 |
188 | 解释:
189 |
190 | - `Id` :编解码器的唯一识别码
191 | - `Name` :编解码器的名称,同样要求全局唯一,主要是便于开发者记忆和可视化
192 | - `Marshal` :编码
193 | - `Unmarshal` :解码
194 |
195 | 开发者可以将自定义的新编解码器注入 `teleport/codec` 包,从而在整个项目中使用。
196 |
197 | 框架已经提供的编解码器实现:JSON、Protobuf、Form(urlencoded)、Plain(raw text)
198 |
199 |
200 |
201 | 在自由支持各种编解码类型后,我就可以模仿 HTTP 协议头的 Content-Type 实现一下协商功能了。
202 |
203 |
204 |
205 | 在 Request/Response 的通信场景下,按以下步骤进行 Body 编码类型协商:
206 |
207 | - Step1:请求端将当前 Body 的编码类型设置到 Message 的 `BodyCodec` 属性
208 |
209 | - Step2:在请求端希望收到请求Body不同的编码类型时(在web开发中很常见),就可以在 Message 对象的 Meta 元信息中设置 `X-Accept-Body-Codec` 来指定响应的编码类型
210 |
211 | - Step3:响应端根据请求的 `BodyCodec` 属性解码 Body,执行业务逻辑
212 | - Step4:响应端在发现有 `X-Accept-Body-Codec` 元信息时,使用该元信息指定类型编码响应 Body,否则默认使用与请求相同的编码类型。当然,响应端的开发者也可以明确指定编码类型,这样就会忽略前面的规则,强制使用该指定的编码类型。
213 |
214 |
215 |
216 | 在上述 Step2 中,请求端设置 Message 对象的 `X-Accept-Body-Codec` Meta 元信息的一段代码片段:
217 |
218 | ```go
219 | session.Call("/a/b", arg, result, tp.WithAcceptBodyCodec(codec.ID_PROTOBUF))
220 | ```
221 |
222 |
223 | 其中,`tp.WithAcceptBodyCodec` 是一种修饰函数的用法,这类函数可以实现灵活地配置策略,一些相关定义如下。在 teleport/socket 包中:
224 |
225 | ```go
226 | type MessageSetting func(*Message)
227 |
228 | func WithAddMeta(key, value string) MessageSetting {
229 | return func(m *Message) {
230 | m.meta.Add(key, value)
231 | }
232 | }
233 | ```
234 |
235 | 在 teleport 包中:
236 |
237 | ```go
238 | const MetaAcceptBodyCodec = "X-Accept-Body-Codec"
239 |
240 | func WithAcceptBodyCodec(bodyCodec byte) MessageSetting {
241 | if bodyCodec == codec.NilCodecId {
242 | return func(*Message) {}
243 | }
244 | return socket.WithAddMeta(MetaAcceptBodyCodec, strconv.FormatUint(uint64(bodyCodec), 10))
245 | }
246 | ...
247 | type Session interface {
248 | Call(uri string, arg interface{}, result interface{}, setting ...socket.MessageSetting) CallCmd
249 | }
250 | ```
251 |
252 | 说明:Call 其实类似于 net/http 中的 `func (c *Client) Do(req *Request) (*Response, error)` 是根据请求参数 Message 进行请求的。
253 |
254 | 在该场景中为什么选择使用修饰函数?为什么不直接传入 Message 结构体(先将其字段公开)?
255 |
256 | - Message 的字段很多,有的必填,有的选填;例如必填参数 uri、arg 都是它的字段(arg对应body字段),meta、context 等为选填;通过上述这种“必填参数+修饰函数不定参”的方法声明,可以从语法层面明确使用规范(如换成使用结构体,只能使用约定,然后在运行时检查)
257 | - 修饰函数的方式可以封装更加复杂的配置逻辑,比如设置两个关联参数的情况,某个字段需要写多行代码进行初始化的情况
258 | - 修饰函数的使用更加灵活,具有很强的封装性,比如可以提供常用的修饰函数包,可以多个修饰函数嵌套组合成一个新的修饰函数等等
259 | - 特意将 Message 的字段声明为私有,同时在当前包内提供一些基础修饰函数,可以大大提高配置的可控性与安全性,同时也避免了开发者学习一些配置约定的成本
260 |
261 | 概括一下修饰函数的使用场景:
262 |
263 | - 配置项很多且一些配置间存在联动性
264 | - 配置项的结构体本身属于内部逻辑的一部分,如果外部传入后再对其进行修改,会对内部造成执行bug的情况
265 | - 若仅仅是简单的配置,建议使用结构体,更加简单直接,比如 mysql 的配置等
266 |
267 |
268 |
269 | ## 如何管理连接?
270 |
271 |
272 |
273 | 一般常见的 Go 语言 RPC 框架都没有重视对连接的管理,甚至是没有连接管理功能。那么,是不是就说明连接管理功能不重要?可有可无?其实不然,这只是与 RPC 框架的定位有关:
274 |
275 | *实现远程过程调用,并不强调连接,甚至是刻意屏蔽掉底层连接*!
276 |
277 |
278 |
279 | 那么,什么场景下,我们需要使用连接管理?
280 |
281 | - 服务端主动推送消息给**指定(一批)连接**的客户端
282 | - 服务端主动请求客户端,并获得客户端的响应
283 | - 增加会话管理,将每条连接命名为用户ID,并绑定用户信息
284 | - 获取文件描述符,对连接性能进行调优
285 | - 异步主动断开**指定(一批)连接**
286 | - 与第三方框架/组件对接
287 |
288 |
289 |
290 | 下面我们来了解一下 teleport 是如何实现连接管理的。
291 |
292 |
293 |
294 | ### Step1:封装 Socket 模块
295 |
296 | 首先,我们以分层的原则对来自net标准包的 `net.Conn` 进行封装得到 Socket 接口。它作为整个框架的底层通信接口,向上层提供应用层消息通信和连接管理的基础功能。
297 |
298 | 该接口涉及五个组件:
299 |
300 | - 来自标准包的 `net.Conn` 接口
301 | - 抽象的应用层协议接口 `Proto`
302 | - 对字节流处理的接口管道 `XferPipe`
303 | - 抽象出来的 `Message` 结构体
304 | - 用于编解码 `Message` 中 `Body` 数据的接口 `Codec`
305 |
306 |
307 |
308 | 常用接口方法如下:
309 |
310 | - `WriteMessage(message *Message) error` :写入应用层消息
311 | - `ReadMessage(message *Message) error` :读取应用层消息
312 | - `SetId(string)` 、`Id() string` :设置或读取当前连接ID
313 | - `Swap() goutil.Map` :存储与当前连接相关的临时数据
314 | - `ControlFD(f func(fd uintptr)) error` :操作当前连接的文件描述符
315 |
316 |
317 |
318 | ### Step2:封装 Session 模块
319 |
320 |
321 |
322 | Session 对象封装了 Socket 接口 ,并负责整个会话相关的事务(相当于引擎)。如:
323 |
324 | - 读消息协程
325 | - 创建消息处理的上下文
326 | - 执行路由与操作
327 | - 写入消息
328 | - 打印运行日志
329 | - 连接的ID命名
330 | - 绑定连接相关状态信息(用户资料等)
331 | - 连接生命周期(连接超时)
332 | - 一次请求的生命周期(请求超时)
333 | - 主动断开连接
334 | - 拨号端的断线重连
335 | - 连接断开事件通知
336 |
337 |
338 |
339 | ### Step3:并发 Map 集中管理 Session
340 |
341 | Peer 是 teleport 对通信两端的对等抽象,除了 Listener 与 Dialer 固有的角色差异外,两种角色拥有完全一致的API。Peer 就包含有一个并发 Map 用于保存全部 Session。因此,开发者可以通过 Peer 实现:
342 |
343 | - 监听地址端口
344 | - 拨号建立连接
345 | - 获取指定 ID 的 Session 实例
346 | - 向所有 Session 广播消息
347 | - 查看当前连接数
348 | - 平滑关闭全部连接
349 |
350 |
351 |
352 | 另外,顺便提一下,teleport是采用非阻塞的通信机制,同时支持同步、异步两种编程方式。这样做有什么好处,或者说阻塞通信与非阻塞通信的区别是什么?
353 |
354 | golang 的 socket 是非阻塞式的,也就是说不管是accpet,还是读写都是非阻塞的。但是 golang 本身对 socket 做了一定的处理,让其用起来像阻塞的一样简单。
355 |
356 | 因此,如果我们当真把它作为阻塞通信机制,通过连接池实现并发通信,是很浪费连接资源的!我们知道,“阻塞通信+连接池”的方式,不仅吞吐量相比较低,而且还有一个无法避免的缺陷:
357 |
358 | - 一类请求的慢响应,会很快耗尽整个连接池资源,进而拖慢整个进程的网络通信
359 |
360 | - 如果该进程是分布式系统中的一个节点,那么这种慢响应还会很快蔓延着其他通信节点
361 |
362 | 但是,如果使用非阻塞通信机制,每个请求都不独占连接,而是共享连接。这样:
363 |
364 | - 首先我们可以抛弃复杂的独占式连接池了(文件下载服务可能还是会用到另外一种连接池)
365 | - 其次,一类或者一个慢响应都不会对其他请求造成影响,同时也就解决了慢响应蔓延的问题
366 | - 第三,可以最大化利用连接资源,提升吞吐量
367 |
368 | 微服务系统中,强烈建议使用这种非阻塞通信机制!
369 |
370 |
371 |
372 | ## 如何设计灵活的插件
373 |
374 |
375 |
376 | 插件会给框架带来灵活性和扩展性,是一个非常重要的模块。那么,如何设计好它?teleport 从三方面考虑:
377 |
378 | - 合适且丰富的插件位置
379 |
380 | - 按插件位置量身设计入参和出参
381 |
382 | - 一个插件允许包含一个或多个插件位置
383 |
384 | 以下是 teleport 的一些插件位置定义:
385 |
386 | | 插件位置(函数) | 插件位置(函数) |
387 | | ----------------------------------------------- | ------------------------------------ |
388 | | PreNewPeer(*PeerConfig, *PluginContainer) error | PostNewPeer(EarlyPeer) error |
389 | | PostReg(*Handler) error | PostListen(net.Addr) error |
390 | | PostDial(PreSession) *Rerror | PostAccept(PreSession) *Rerror |
391 | | PreWriteCall(WriteCtx) *Rerror | PostWriteCall(WriteCtx) *Rerror |
392 | | PreWriteReply(WriteCtx) *Rerror | PostWriteReply(WriteCtx) *Rerror |
393 | | PreWritePush(WriteCtx) *Rerror | PostWritePush(WriteCtx) *Rerror |
394 | | PreReadHeader(PreCtx) error | PostReadCallHeader(ReadCtx) *Rerror |
395 | | PreReadCallBody(ReadCtx) *Rerror | PostReadCallBody(ReadCtx) *Rerror |
396 | | PostReadPushHeader(ReadCtx) *Rerror | PreReadPushBody(ReadCtx) *Rerror |
397 | | PostReadPushBody(ReadCtx) *Rerror | PostReadReplyHeader(ReadCtx) *Rerror |
398 | | PreReadReplyBody(ReadCtx) *Rerror | PostReadReplyBody(ReadCtx) *Rerror |
399 | | PostDisconnect(BaseSession) *Rerror | |
400 |
401 |
402 |
403 | 上面这些函数的入参中,不带有 `*` 前缀的都是接口。
404 |
405 | 其中以 `Peer`、`Session`、 `Ctx` 为后缀的入参(接口类型),涉及到一种非常有趣、有用的 interface 用法——限制方法集。
406 |
407 | 以 `Ctx` 为例:
408 |
409 | 
410 |
411 | ## 实践:轻松组装微服务
412 |
413 | [tp-micro](https://github.com/xiaoenai/tp-micro) 是以 teleport + plugin 的方式扩展而来的微服务。虽然目前还有一些功能未开发,但已有两家公司使用。它在完整继承 teleport 特性的同时,增加如下主要模块:
414 |
415 | | 模块 | 模块 | 模块 | 模块 | 模块 | 模块 |
416 | | :--------------------------------: | :------------------------: | :------: | :----------------: | :----------: | :----: |
417 | | 服务注册插件 | 路由发现插件(含负载均衡) | 心跳插件 | 参数绑定与校验插件 | 安全加密插件 | 断路器 |
418 | | 脚手架工具:由模板生成项目、热编译 | 网关 | 灰度 | Agent | | |
419 |
420 |
421 |
422 | 
423 |
424 | ## 聊聊高效开发的一些事儿
425 |
426 | ### 实现 RPC 开发范式
427 |
428 | 实现 RPC 范式的好处是代码书写简单、代码结构清晰明了、对开发者友好。
429 |
430 | 在此只贴出一个简单代码示例,不展开讨论封装细节。
431 |
432 | - server.go
433 |
434 | ```go
435 | package main
436 |
437 | import (
438 | "fmt"
439 | "time"
440 |
441 | tp "github.com/henrylee2cn/teleport"
442 | )
443 |
444 | func main() {
445 | // graceful
446 | go tp.GraceSignal()
447 |
448 | // server peer
449 | srv := tp.NewPeer(tp.PeerConfig{
450 | CountTime: true,
451 | ListenPort: 9090,
452 | PrintDetail: true,
453 | })
454 |
455 | // router
456 | srv.RouteCall(new(Math))
457 |
458 | // broadcast per 5s
459 | go func() {
460 | for {
461 | time.Sleep(time.Second * 5)
462 | srv.RangeSession(func(sess tp.Session) bool {
463 | sess.Push(
464 | "/push/status",
465 | fmt.Sprintf("this is a broadcast, server time: %v", time.Now()),
466 | )
467 | return true
468 | })
469 | }
470 | }()
471 |
472 | // listen and serve
473 | srv.ListenAndServe()
474 | }
475 |
476 | // Math handler
477 | type Math struct {
478 | tp.CallCtx
479 | }
480 |
481 | // Add handles addition request
482 | func (m *Math) Add(arg *[]int) (int, *tp.Rerror) {
483 | // test query parameter
484 | tp.Infof("author: %s", m.Query().Get("author"))
485 | // add
486 | var r int
487 | for _, a := range *arg {
488 | r += a
489 | }
490 | // response
491 | return r, nil
492 | }
493 | ```
494 |
495 |
496 |
497 | - client.go
498 |
499 | ```go
500 | package main
501 |
502 | import (
503 | "time"
504 |
505 | tp "github.com/henrylee2cn/teleport"
506 | )
507 |
508 | func main() {
509 | // log level
510 | tp.SetLoggerLevel("ERROR")
511 |
512 | cli := tp.NewPeer(tp.PeerConfig{})
513 | defer cli.Close()
514 |
515 | cli.RoutePush(new(Push))
516 |
517 | sess, err := cli.Dial(":9090")
518 | if err != nil {
519 | tp.Fatalf("%v", err)
520 | }
521 |
522 | var result int
523 | rerr := sess.Call("/math/add?author=henrylee2cn",
524 | []int{1, 2, 3, 4, 5},
525 | &result,
526 | ).Rerror()
527 | if rerr != nil {
528 | tp.Fatalf("%v", rerr)
529 | }
530 | tp.Printf("result: %d", result)
531 |
532 | tp.Printf("wait for 10s...")
533 | time.Sleep(time.Second * 10)
534 | }
535 |
536 | // Push push handler
537 | type Push struct {
538 | tp.PushCtx
539 | }
540 |
541 | // Push handles '/push/status' message
542 | func (p *Push) Status(arg *string) *tp.Rerror {
543 | tp.Printf("%s", *arg)
544 | return nil
545 | }
546 | ```
547 |
548 |
549 |
550 | ### 处理错误的姿势
551 |
552 | teleport 对于 Handler 的错误返回值,并没有采用 error 接口类型,而是定义了一个 Rerror 结构体:(用法见上面示例代码)
553 |
554 | ```go
555 | type Rerror struct {
556 | // Code error code
557 | Code int32
558 | // Message the error message displayed to the user (optional)
559 | Message string
560 | // Reason the cause of the error for debugging (optional)
561 | Reason string
562 | }
563 | ```
564 |
565 | 这样设计有几个好处:
566 |
567 | - Code 字段表示错误代号,类似 HTTP 状态码,有利于和 HTTP 协议完美兼容,同时也方便插件和客户端对错误类型快速判断与处理
568 |
569 | - Message 字段用于给客户端的错误提示信息,可进行字符串格式的定制
570 |
571 | - Reason 字段记录错误发生的原因甚至上下文,助力Debug
572 |
573 | - 如果开发者需要与 error 接口交互,`Rerror.ToError() error` 方法可以实现
574 |
575 |
576 |
577 | 可能有人会问:为什么不直接实现 ~~`Rerror.Error() error`~~ ?因为我是故意的!原因则涉及到 interface 的一个经典的坑:(*Rerror)(nil) !=(error)(nil)。如果开发者不小心写出下面的代码,就掉坑里了:
578 |
579 | ```
580 | var err error
581 | err = sess.Call(...).Rerror()
582 | if err != nil { // 此处掉坑里了,必定会进入错误处理逻辑,因为 (*Rerror)(nil) != (error)(nil) 永远成立
583 | ...
584 | }
585 | ```
586 |
587 |
588 |
589 | 推荐:服务端定义一张全局的错误码表,方便于客户端对接以及错误Debug。比如这样的规则:
590 |
591 | - 1 ≤ Code ≤ 999:框架错误,包括通信错误,具体可以与 HTTP 状态码保持一致
592 | - 1000 ≤ Code ≤ 9999:基础服务错误
593 | - 1000000 ≤ 999999:业务错误,前四位表示模块或服务,后两位表示当前模块或服务中错误序号
594 |
595 |
596 |
597 | ### 推荐一种很酷的项目结构
598 |
599 | 这是 tp-micro 中默认的项目组织结构,它有 `micro gen` 命令由模板自动构建。
600 |
601 | ```
602 | ├── README.md
603 | ├── __tp-micro__gen__.lock
604 | ├── __tp-micro__tpl__.go
605 | ├── config
606 | │ └── config.yaml
607 | ├── config.go
608 | ├── internal
609 | │ ├── handler
610 | │ │ ├── call.tmp.go
611 | │ │ └── push.tmp.go
612 | │ └── model
613 | │ ├── init.go
614 | │ ├── mongo_meta.gen.go
615 | │ ├── mysql_device.gen.go
616 | │ ├── mysql_log.gen.go
617 | │ └── mysql_user.gen.go
618 | ├── log
619 | │ └── PID
620 | ├── main.go
621 | ├── router.gen.go
622 | └── sdk
623 | ├── rerr.go
624 | ├── rpc.gen.go
625 | ├── rpc.gen_test.go
626 | ├── type.gen.go
627 | └── val.gen.go
628 | ```
629 |
630 | 该项目结构整体分为两部分。
631 |
632 | 一部分是对外公开的代码,都位于 sdk 目录下,比如 client 远程调用的函数就在这里。
633 |
634 | 剩余代码都是不对外公开的,属于 server 进程的部分,其中私有包 internal 下是 server 的主体业务逻辑部分。
635 |
636 | 这样设计的好处是:
637 |
638 | - 外部调用者(一般是客户端)只能导入 sdk 包,其余的包要么在 internal 下被私有化,要么就是 main 包,都无法导入;从而起到了从语法级别隔离代码目的,有效地解决了误用代码、复杂依赖的问题
639 | - 将 sdk 代码与 server 代码放在同一项目中,便于统一管理,减少更新时人为原因造成客户端与服务端接口对不上的情况
640 |
641 |
642 |
643 | ### 脚手架提升开发效率
644 |
645 | 在 tp-micro 中,提供了一个 `micro` 工具,介绍两个最常用的命令:
646 |
647 | - 命令 `micro gen` 可以通过模板可以快速生成一个项目(上面提到的项目结构)
648 | - 其中包括 mysql、mongo、redis 相关的 model 层代码
649 | - README.md 中会自动写入接口文档等,便于交付给客户端同学
650 | - 支持覆盖更新部分代码,比如新增接口。
651 | - 命令 `micro run` 可以自动编译运行指定项目,并在项目代码发生变化时自动进行平滑升级
652 |
--------------------------------------------------------------------------------
/02/src/ctx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andeya/erpc-doc/ec414b819b4a15f2fda66454b31d20bbf01cd1ce/02/src/ctx.png
--------------------------------------------------------------------------------
/02/src/ctx.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # erpc-doc
2 |
3 | eRPC Documents
4 |
5 | 1. [《Go Socket编程之teleport框架是怎样炼成的?》](https://github.com/henrylee2cn/tpdoc/blob/master/01/README.md)
6 | 2. [《聊聊 Go Socket 框架 Teleport 的设计思路》](https://github.com/henrylee2cn/tpdoc/blob/master/02/README.md)
7 |
--------------------------------------------------------------------------------