├── .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 | ![rpc_compare](https://raw.githubusercontent.com/henrylee2cn/tpdoc/master/01/src/rpc_compare.png) 77 | 78 | - CPU耗时火焰图 teleport/socket 79 | 80 | ![tp_socket_profile_torch](https://raw.githubusercontent.com/henrylee2cn/teleport/v3/doc/tp_socket_profile_torch.png) 81 | 82 | **[svg file](https://github.com/henrylee2cn/teleport/raw/v3/doc/tp_socket_profile_torch.svg)** 83 | 84 | - 堆栈信息火焰图 teleport/socket 85 | 86 | ![tp_socket_heap_torch](https://raw.githubusercontent.com/henrylee2cn/teleport/v3/doc/tp_socket_heap_torch.png) 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 | ![Teleport-Framework](https://raw.githubusercontent.com/henrylee2cn/teleport/v3/doc/teleport_framework.png) 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 | ![原文二维码](https://github.com/henrylee2cn/tpdoc/raw/master/01/src/qrcode-viewfile.png) 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 | ![Teleport-Framework](https://github.com/henrylee2cn/teleport/raw/v4/doc/teleport_module_diagram.png) 72 | 73 | 注:“tp” 是 teleport 的包名,因此它代指 “teleport”。 74 | 75 | 76 | 77 | ## 简单的性能对比图 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
EnvironmentThroughputsMean LatencyP99 Latency
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 | ![tp_data_message](https://github.com/henrylee2cn/teleport/raw/v4/doc/tp_data_message.png) 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 | ![tp_ctx](https://github.com/henrylee2cn/tpdoc/raw/master/02/src/ctx.png) 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 | ![tp-micro flow chart](https://github.com/xiaoenai/tp-micro/raw/v3/doc/tp-micro_flow_chart.png) 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 | *handlerCtxstructPreCtxinterfaceCallCtxinterfaceUnknownPushCtxinterfaceUnknownCallCtxinterfacePushCtxinterfaceReadCtxinterfaceWriteCtxinterfaceinputCtxinterface -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------