Repository: smallnest/rpcx Branch: master Commit: d57f1f75012c Files: 128 Total size: 428.0 KB Directory structure: gitextract_r3mw_qpx/ ├── .github/ │ └── workflows/ │ └── go.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── _testutils/ │ ├── GoUnusedProtection__.go │ ├── arith_service.go │ ├── arith_service.proto │ ├── thrift_arith_service-consts.go │ ├── thrift_arith_service.go │ └── thrift_arith_service.thrift ├── client/ │ ├── cache_client_builder.go │ ├── circuit_breaker.go │ ├── circuit_breaker_test.go │ ├── client.go │ ├── client_test.go │ ├── connection.go │ ├── connection_iouring.go │ ├── connection_iouring_test.go │ ├── connection_kcp.go │ ├── connection_memu.go │ ├── connection_nonkcp.go │ ├── connection_nonquic.go │ ├── connection_quic.go │ ├── connection_rdma.go │ ├── discovery.go │ ├── dns_discovery.go │ ├── failmode_enumer.go │ ├── geo_utils.go │ ├── hash_utils.go │ ├── mdns_discovery.go │ ├── mode.go │ ├── multiple_servers_discovery.go │ ├── oneclient.go │ ├── oneclient_pool.go │ ├── oneclient_pool_test.go │ ├── peer2peer_discovery.go │ ├── ping_utils.go │ ├── plugin.go │ ├── selectmode_enumer.go │ ├── selector.go │ ├── selector_test.go │ ├── smooth_weighted_round_robin.go │ ├── xclient.go │ ├── xclient_pool.go │ ├── xclient_pool_test.go │ └── xclient_test.go ├── clientplugin/ │ └── req_rate_limiting_redis.go ├── codec/ │ ├── codec.go │ ├── codec_test.go │ └── testdata/ │ ├── GoUnusedProtection__.go │ ├── gen.sh │ ├── protobuf.pb.go │ ├── protobuf.proto │ ├── thrift_colorgroup-consts.go │ ├── thrift_colorgroup.go │ └── thrift_colorgroup.thrift ├── errors/ │ ├── error.go │ └── error_test.go ├── go.mod ├── go.sum ├── log/ │ ├── default_logger.go │ ├── dummy_logger.go │ └── logger.go ├── protocol/ │ ├── compressor.go │ ├── compressor_test.go │ ├── message.go │ ├── message_chan_test.go │ ├── message_test.go │ └── testdata/ │ ├── benchmark.pb.go │ ├── benchmark.proto │ └── gen.sh ├── reflection/ │ ├── server_reflection.go │ └── server_reflection_test.go ├── server/ │ ├── context.go │ ├── converter.go │ ├── converter_test.go │ ├── file_transfer.go │ ├── gateway.go │ ├── jsonrpc2.go │ ├── jsonrpc2_wire.go │ ├── kcp.go │ ├── listener.go │ ├── listener_linux.go │ ├── listener_rdma.go │ ├── listener_unix.go │ ├── memconn.go │ ├── option.go │ ├── option_test.go │ ├── options.go │ ├── plugin.go │ ├── plugin_test.go │ ├── pool.go │ ├── pool_test.go │ ├── quic.go │ ├── router.go │ ├── server.go │ ├── server_test.go │ ├── service.go │ ├── service_test.go │ └── stream.go ├── serverplugin/ │ ├── alias.go │ ├── blacklist.go │ ├── mdns.go │ ├── metrics.go │ ├── rate_limiting.go │ ├── redis.go │ ├── registry_test.go │ ├── req_rate_limiting.go │ ├── req_rate_limiting_redis.go │ ├── tee.go │ └── whitelist.go ├── share/ │ ├── context.go │ ├── context_test.go │ ├── share.go │ └── share_test.go ├── tool/ │ └── xgen/ │ ├── README.md │ ├── parser/ │ │ └── parser.go │ └── xgen.go └── util/ ├── buffer_pool.go ├── buffer_pool_test.go ├── compress.go ├── compress_test.go ├── converter.go ├── net.go └── net_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.x uses: actions/setup-go@v2 with: go-version: ^1.13 - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Get dependencies run: | go get -v -t -d ./... - name: Build run: go build -v ./... - name: Test run: go test -v ./... ================================================ FILE: .gitignore ================================================ # vendor/ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ .idea cover.html .history ================================================ FILE: .travis.yml ================================================ language: go go: - 1.17.x env: - GO111MODULE=on before_script: - rm -f go.sum - go get -tags "quic kcp" -v github.com/smallnest/rpcx@HEAD - go get github.com/mattn/goveralls script: - go test -v ./... - goveralls -service=travis-ci notifications: email: recipients: smallnest@gmail.com on_success: change on_failure: always ================================================ FILE: CHANGELOG.md ================================================ # [rpcx](http://rpcx.io) ## 1.9.0 - unregister all services on close automatically - add PostHTTPRequestPlugin - support io_uring - add CacheDiscovery - add Oneshot method for XClient - support RDMA ## 1.8.0 - supports distributed rate limiter based on go-redis/redis-rate - move zookeeper plugin to https://github.com/smallnest/rpcx-zookeepr - move consul plugin to https://github.com/smallnest/rpcx-consul - move redis plugin to https://github.com/smallnest/rpcx-redis - move influxd/opentelemetry plugin to https://github.com/smallnest/rpcx-plugins - you can write customized error, for example `{"code": 500, err: "internal error"}` - server support the work pool by `WithPool` - support to write services like `go std http router` style without reflect - simplify async write for service - improve performance ## 1.7.0 - move etcd support to github.com/rpcxio/rpcx-etcd - Broken API: NewXXXDiscovery returns error instead of panic - support AdvertiseAddr in FileTransfer - support Auth for OneClientPool - support Auth for XClientPool - Broken API: add meta parameter for SendFile/DownloadFile - support streaming between server side and client side - support DNS as service discovery - support rpcx flow tracing - support websocket as the transport like tcp,kcp and quic - add CMuxPlugin to allow developing customzied services by using the same single port - re-tag rpcx to make sure the version is less than 2 (for go module) - support visit grpc services by rpcx clients: https://github.com/rpcxio/rpcxplus/tree/master/grpcx - support configing grpc servicves in rpcx server side - improve rpcx performance - add Inform method in XClient - add memory connection for unit tests - supports opentelemetry ## 1.6.0 - support reflection - add kubernetes config example - improve nacos support - improve message.Encode performance - re-register services in etcd v3 - avoid duplicated client creation - add SelectNodePlugin that can interrupt the Select method - support TcpCopy by TeePlugin - support reuseport for http invoke - return reply even in case of server errors - Change two methods' name of client plugin! - Broken API: add error parameter in `PreWriteResponse`(#486) - Broken API: change ReadTimeout/WriteTimeout to IdleTimeout - Support passing Deadline of client contexts to server side - remove InprocessClient plugin - use heartbeat/tcp_keepalive to avoid client hanging ## 1.5.0 - support jsonrpc 2.0 - support CORS for jsonrpc 2.0 - support opentracing and opencensus - upload/download files by streaming - add Pool for XClient and OneClient - remove rudp support - add ConnCreated plugin. Yu can use it to set KCP UDPSession - update client plugins. All plugin returns error instead of bool - support ETCD 3.0 API - support redis as registry - support redis DB selection - fix RegisterFunction issues - add Filter for clients - remove most of build tags such as etcd, zookeeper,consul,reuseport - add Nacos as registry http://nacos.io - support blacklist and whitlist ## 1.4.0 - Support utp and rudp - Add OneClient to support invoke multile servicesby one client - Finish compress feature - Add more plugins for monitoring connection - Support dynamic port allocation - Use go module to manage dependencies - Support shutdown graceful - Add [rpcx-java](https://github.com/smallnest/rpcx-java) to support develop raw java services and clients - Support thrift codec - Setup rpcx offcial site: http://rpcx.io - Add Chinese document: http://cn.doc.rpcx.io or https://smallnest.gitbooks.io/go-rpc-programming-guide ## 1.3.1 - Add http gateway: https://github.com/rpcxio/rpcx-gateway - Add direct http invoke - Add bidirectional communication - Add xgen tool to generate codes for services automatically fix bugs. ## 1.3.0 - Rewrite rpcx. It implements its protocol and won't implemented based on wrapper of go standard rpc lib - Add go tags for pluggable plugins - Add English document: https://github.com/smallnest/rpcx-programming - Add rpcx 3.0 examples: https://github.com/rpcxio/rpcx-examples rpcx 3.0 is not compatible with rpcx 2.0 and below ================================================ FILE: LICENSE ================================================ Copyright 2017 Chao yuepan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ WORKDIR=`pwd` default: build vet: go vet ./... tools: go get github.com/golangci/golangci-lint/cmd/golangci-lint go get github.com/golang/lint/golint go get github.com/axw/gocov/gocov go get github.com/matm/gocov-html golangci-lint: golangci-lint run -D errcheck --build-tags 'quic kcp' lint: golint ./... doc: godoc -http=:6060 deps: go list -f '{{ join .Deps "\n"}}' ./... |grep "/" | grep -v "github.com/smallnest/rpcx"| grep "\." | sort |uniq fmt: go fmt ./... build: go build ./... build-all: go build -tags "kcp quic" ./... test: go test -race -tags "kcp quic" ./... cover: gocov test -tags "kcp quic" ./... | gocov-html > cover.html open cover.html check-libs: GIT_TERMINAL_PROMPT=1 GO111MODULE=on go list -m -u all | column -t update-libs: GIT_TERMINAL_PROMPT=1 GO111MODULE=on go get -u -v ./... mod-tidy: GIT_TERMINAL_PROMPT=1 GO111MODULE=on go mod tidy ================================================ FILE: README.md ================================================ - **stable branch**: v1.7.x - **development branch**: master Official site: [http://rpcx.io](http://rpcx.io/) [![License](https://img.shields.io/:license-apache%202-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GoDoc](https://godoc.org/github.com/smallnest/rpcx?status.png)](http://godoc.org/github.com/smallnest/rpcx) [![github actions](https://github.com/smallnest/rpcx/actions)](https://github.com/smallnest/rpcx/actions/workflows/go.yml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/smallnest/rpcx)](https://goreportcard.com/report/github.com/smallnest/rpcx) [![coveralls](https://coveralls.io/repos/smallnest/rpcx/badge.svg?branch=master&service=github)](https://coveralls.io/github/smallnest/rpcx?branch=master) [![QQ3群](https://img.shields.io/:QQ3群-953962236-blue.svg)](_documents/rpcx_dev_qq3.jpg) **Notice: etcd** since rpcx 1.7.6, some plugins have been moved to the independent project: - `etcd` plugin has been moved to [rpcx-etcd](https://github.com/rpcxio/rpcx-etcd) - `zookeeper` plugin has been moved to [rpcx-zookeeper](https://github.com/rpcxio/rpcx-zookeeper) - `consul` plugin has been moved to [rpcx-consul](https://github.com/rpcxio/rpcx-consul) - `redis` plugin has been moved to [rpcx-redis](https://github.com/rpcxio/rpcx-redis) - `influxdb` plugin has been moved to [rpcx-plugins](https://github.com/rpcxio/rpcx-plugins) - `opentelemetry` plugin has been moved to [rpcx-plugins](https://github.com/rpcxio/rpcx-plugins) ## Announce A tcpdump-like tool added: [rpcxdump](https://github.com/smallnest/rpcxdump)。 You can use it to debug communications between rpcx services and clients. ![](https://github.com/smallnest/rpcxdump/blob/master/snapshoot.png) ## Cross-Languages you can use other programming languages besides Go to access rpcx services. - **rpcx-gateway**: You can write clients in any programming languages to call rpcx services via [rpcx-gateway](https://github.com/rpcxio/rpcx-gateway) - **http invoke**: you can use the same http requests to access rpcx gateway - **Java Services/Clients**: You can use [rpcx-java](https://github.com/smallnest/rpcx-java) to implement/access rpcx services via raw protocol. - **rust rpcx**: You can write rpcx services in rust by [rpcx-rs](https://github.com/smallnest/rpcx-rs) > If you can write Go methods, you can also write rpc services. It is so easy to write rpc applications with rpcx. ## Installation install the basic features: `go get -v github.com/smallnest/rpcx/...` If you want to use `quic`、`kcp` registry, use those tags to `go get` 、 `go build` or `go run`. For example, if you want to use all features, you can: ```sh go get -v -tags "quic kcp" github.com/smallnest/rpcx/... ``` **_tags_**: - **quic**: support quic transport - **kcp**: support kcp transport ## Which companies are using rpcx?

## Features rpcx is a RPC framework like [Alibaba Dubbo](http://dubbo.io/) and [Weibo Motan](https://github.com/weibocom/motan). **rpcx** is created for targets: 1. **Simple**: easy to learn, easy to develop, easy to integrate and easy to deploy 2. **Performance**: high performance (>= grpc-go) 3. **Cross-platform**: support _raw slice of bytes_, _JSON_, _Protobuf_ and _MessagePack_. Theoretically it can be used with java, php, python, c/c++, node.js, c# and other platforms 4. **Service discovery and service governance**: support zookeeper, etcd and consul. It contains below features - Support raw Go functions. There's no need to define proto files. - Pluggable. Features can be extended such as service discovery, tracing. - Support TCP, HTTP, [QUIC](https://en.wikipedia.org/wiki/QUIC) and [KCP](https://github.com/skywind3000/kcp) - Support multiple codecs such as JSON, Protobuf, [MessagePack](https://msgpack.org/index.html) and raw bytes. - Service discovery. Support peer2peer, configured peers, [zookeeper](https://zookeeper.apache.org), [etcd](https://github.com/coreos/etcd), [consul](https://www.consul.io) and [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS). - Fault tolerance:Failover, Failfast, Failtry. - Load banlancing:support Random, RoundRobin, Consistent hashing, Weighted, network quality and Geography. - Support Compression. - Support passing metadata. - Support Authorization. - Support heartbeat and one-way request. - Other features: metrics, log, timeout, alias, circuit breaker. - Support bidirectional communication. - Support access via HTTP so you can write clients in any programming languages. - Support API gateway. - Support backup request, forking and broadcast. rpcx uses a binary protocol and platform-independent, which means you can develop services in other languages such as Java, python, nodejs, and you can use other prorgramming languages to invoke services developed in Go. There is a UI manager: [rpcx-ui](https://github.com/smallnest/rpcx-ui). ## Performance Test results show rpcx has better performance than other rpc framework except standard rpc lib. The benchmark code is at [rpcx-benchmark](https://github.com/rpcx-ecosystem/rpcx-benchmark). **Listen to others, but test by yourself**. **_Test Environment_** - **CPU**: Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz, 32 cores - **Memory**: 32G - **Go**: 1.9.0 - **OS**: CentOS 7 / 3.10.0-229.el7.x86_64 **_Use_** - protobuf - the client and the server on the same server - 581 bytes payload - 500/2000/5000 concurrent clients - mock processing time: 0ms, 10ms and 30ms **_Test Result_** ### mock 0ms process time
ThroughputsMean LatencyP99 Latency
### mock 10ms process time
ThroughputsMean LatencyP99 Latency
### mock 30ms process time
ThroughputsMean LatencyP99 Latency
## Examples You can find all examples at [rpcxio/rpcx-examples](https://github.com/rpcxio/rpcx-examples). The below is a simple example. **Server** ```go // define example.Arith …… s := server.NewServer() s.RegisterName("Arith", new(example.Arith), "") s.Serve("tcp", addr) ``` **Client** ```go // prepare requests …… d, err := client.NewPeer2PeerDiscovery("tcp@"+addr, "") xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) defer xclient.Close() err = xclient.Call(context.Background(), "Mul", args, reply, nil) ``` ## Contributors ## Contribute see [contributors](https://github.com/smallnest/rpcx/graphs/contributors). Welcome to contribute: - submit issues or requirements - send PRs - write projects to use rpcx - write tutorials or articles to introduce rpcx ## License Apache License, Version 2.0 ================================================ FILE: _testutils/GoUnusedProtection__.go ================================================ // Code generated by Thrift Compiler (0.14.0). DO NOT EDIT. package testutils var GoUnusedProtection__ int ================================================ FILE: _testutils/arith_service.go ================================================ // Code generated by protoc-gen-gogo. // source: arith_service.proto // DO NOT EDIT! /* Package client is a generated protocol buffer package. It is generated from these files: arith_service.proto It has these top-level messages: ProtoArgs ProtoReply */ package testutils import proto "github.com/gogo/protobuf/proto" import fmt "fmt" import math "math" import io "io" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package type ProtoArgs struct { A int32 `protobuf:"varint,1,opt,name=A,proto3" json:"A,omitempty"` B int32 `protobuf:"varint,2,opt,name=B,proto3" json:"B,omitempty"` } func (m *ProtoArgs) Reset() { *m = ProtoArgs{} } func (m *ProtoArgs) String() string { return proto.CompactTextString(m) } func (*ProtoArgs) ProtoMessage() {} func (*ProtoArgs) Descriptor() ([]byte, []int) { return fileDescriptorArithService, []int{0} } func (m *ProtoArgs) GetA() int32 { if m != nil { return m.A } return 0 } func (m *ProtoArgs) GetB() int32 { if m != nil { return m.B } return 0 } type ProtoReply struct { C int32 `protobuf:"varint,1,opt,name=C,proto3" json:"C,omitempty"` } func (m *ProtoReply) Reset() { *m = ProtoReply{} } func (m *ProtoReply) String() string { return proto.CompactTextString(m) } func (*ProtoReply) ProtoMessage() {} func (*ProtoReply) Descriptor() ([]byte, []int) { return fileDescriptorArithService, []int{1} } func (m *ProtoReply) GetC() int32 { if m != nil { return m.C } return 0 } func init() { proto.RegisterType((*ProtoArgs)(nil), "client.ProtoArgs") proto.RegisterType((*ProtoReply)(nil), "client.ProtoReply") } func (m *ProtoArgs) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *ProtoArgs) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.A != 0 { dAtA[i] = 0x8 i++ i = encodeVarintArithService(dAtA, i, uint64(m.A)) } if m.B != 0 { dAtA[i] = 0x10 i++ i = encodeVarintArithService(dAtA, i, uint64(m.B)) } return i, nil } func (m *ProtoReply) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *ProtoReply) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.C != 0 { dAtA[i] = 0x8 i++ i = encodeVarintArithService(dAtA, i, uint64(m.C)) } return i, nil } func encodeFixed64ArithService(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) dAtA[offset+1] = uint8(v >> 8) dAtA[offset+2] = uint8(v >> 16) dAtA[offset+3] = uint8(v >> 24) dAtA[offset+4] = uint8(v >> 32) dAtA[offset+5] = uint8(v >> 40) dAtA[offset+6] = uint8(v >> 48) dAtA[offset+7] = uint8(v >> 56) return offset + 8 } func encodeFixed32ArithService(dAtA []byte, offset int, v uint32) int { dAtA[offset] = uint8(v) dAtA[offset+1] = uint8(v >> 8) dAtA[offset+2] = uint8(v >> 16) dAtA[offset+3] = uint8(v >> 24) return offset + 4 } func encodeVarintArithService(dAtA []byte, offset int, v uint64) int { for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return offset + 1 } func (m *ProtoArgs) Size() (n int) { var l int _ = l if m.A != 0 { n += 1 + sovArithService(uint64(m.A)) } if m.B != 0 { n += 1 + sovArithService(uint64(m.B)) } return n } func (m *ProtoReply) Size() (n int) { var l int _ = l if m.C != 0 { n += 1 + sovArithService(uint64(m.C)) } return n } func sovArithService(x uint64) (n int) { for { n++ x >>= 7 if x == 0 { break } } return n } func sozArithService(x uint64) (n int) { return sovArithService(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *ProtoArgs) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowArithService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: ProtoArgs: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: ProtoArgs: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field A", wireType) } m.A = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowArithService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.A |= (int32(b) & 0x7F) << shift if b < 0x80 { break } } case 2: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field B", wireType) } m.B = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowArithService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.B |= (int32(b) & 0x7F) << shift if b < 0x80 { break } } default: iNdEx = preIndex skippy, err := skipArithService(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthArithService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *ProtoReply) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowArithService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: ProtoReply: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: ProtoReply: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field C", wireType) } m.C = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowArithService } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.C |= (int32(b) & 0x7F) << shift if b < 0x80 { break } } default: iNdEx = preIndex skippy, err := skipArithService(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthArithService } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipArithService(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowArithService } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowArithService } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } return iNdEx, nil case 1: iNdEx += 8 return iNdEx, nil case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowArithService } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } iNdEx += length if length < 0 { return 0, ErrInvalidLengthArithService } return iNdEx, nil case 3: for { var innerWire uint64 var start int = iNdEx for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowArithService } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ innerWire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } innerWireType := int(innerWire & 0x7) if innerWireType == 4 { break } next, err := skipArithService(dAtA[start:]) if err != nil { return 0, err } iNdEx = start + next } return iNdEx, nil case 4: return iNdEx, nil case 5: iNdEx += 4 return iNdEx, nil default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } } panic("unreachable") } var ( ErrInvalidLengthArithService = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowArithService = fmt.Errorf("proto: integer overflow") ) func init() { proto.RegisterFile("arith_service.proto", fileDescriptorArithService) } var fileDescriptorArithService = []byte{ // 126 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4e, 0x2c, 0xca, 0x2c, 0xc9, 0x88, 0x2f, 0x4e, 0x2d, 0x2a, 0xcb, 0x4c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x4b, 0xce, 0xc9, 0x4c, 0xcd, 0x2b, 0x51, 0x52, 0xe7, 0xe2, 0x0c, 0x00, 0x09, 0x38, 0x16, 0xa5, 0x17, 0x0b, 0xf1, 0x70, 0x31, 0x3a, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xb0, 0x06, 0x31, 0x3a, 0x82, 0x78, 0x4e, 0x12, 0x4c, 0x10, 0x9e, 0x93, 0x92, 0x14, 0x17, 0x17, 0x58, 0x61, 0x50, 0x6a, 0x41, 0x4e, 0x25, 0x48, 0xce, 0x19, 0xa6, 0xd2, 0xd9, 0x49, 0xe0, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0x21, 0x89, 0x0d, 0x6c, 0x8b, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xfa, 0x7e, 0x25, 0x8d, 0x7c, 0x00, 0x00, 0x00, } ================================================ FILE: _testutils/arith_service.proto ================================================ // protoc --gogofaster_out=. arith_service.proto // mv arith_service.pb.go arith_service_test.go syntax = "proto3"; package client; message ProtoArgs { int32 A = 1; int32 B = 2; } message ProtoReply { int32 C = 1; } ================================================ FILE: _testutils/thrift_arith_service-consts.go ================================================ // Code generated by Thrift Compiler (0.14.0). DO NOT EDIT. package testutils import ( "bytes" "context" "fmt" "github.com/apache/thrift/lib/go/thrift" "time" ) // (needed to ensure safety because of naive import list construction.) var _ = thrift.ZERO var _ = fmt.Printf var _ = context.Background var _ = time.Now var _ = bytes.Equal func init() { } ================================================ FILE: _testutils/thrift_arith_service.go ================================================ // Code generated by Thrift Compiler (0.14.0). DO NOT EDIT. package testutils import ( "bytes" "context" "fmt" "github.com/apache/thrift/lib/go/thrift" "time" ) // (needed to ensure safety because of naive import list construction.) var _ = thrift.ZERO var _ = fmt.Printf var _ = context.Background var _ = time.Now var _ = bytes.Equal // Attributes: // - A // - B type ThriftArgs_ struct { A int32 `thrift:"a,1" db:"a" json:"a"` B int32 `thrift:"b,2" db:"b" json:"b"` } func NewThriftArgs_() *ThriftArgs_ { return &ThriftArgs_{} } func (p *ThriftArgs_) GetA() int32 { return p.A } func (p *ThriftArgs_) GetB() int32 { return p.B } func (p *ThriftArgs_) Read(ctx context.Context, iprot thrift.TProtocol) error { if _, err := iprot.ReadStructBegin(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) } for { _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx) if err != nil { return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) } if fieldTypeId == thrift.STOP { break } switch fieldId { case 1: if fieldTypeId == thrift.I32 { if err := p.ReadField1(ctx, iprot); err != nil { return err } } else { if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } case 2: if fieldTypeId == thrift.I32 { if err := p.ReadField2(ctx, iprot); err != nil { return err } } else { if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } default: if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } if err := iprot.ReadFieldEnd(ctx); err != nil { return err } } if err := iprot.ReadStructEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) } return nil } func (p *ThriftArgs_) ReadField1(ctx context.Context, iprot thrift.TProtocol) error { if v, err := iprot.ReadI32(ctx); err != nil { return thrift.PrependError("error reading field 1: ", err) } else { p.A = v } return nil } func (p *ThriftArgs_) ReadField2(ctx context.Context, iprot thrift.TProtocol) error { if v, err := iprot.ReadI32(ctx); err != nil { return thrift.PrependError("error reading field 2: ", err) } else { p.B = v } return nil } func (p *ThriftArgs_) Write(ctx context.Context, oprot thrift.TProtocol) error { if err := oprot.WriteStructBegin(ctx, "ThriftArgs"); err != nil { return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) } if p != nil { if err := p.writeField1(ctx, oprot); err != nil { return err } if err := p.writeField2(ctx, oprot); err != nil { return err } } if err := oprot.WriteFieldStop(ctx); err != nil { return thrift.PrependError("write field stop error: ", err) } if err := oprot.WriteStructEnd(ctx); err != nil { return thrift.PrependError("write struct stop error: ", err) } return nil } func (p *ThriftArgs_) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) { if err := oprot.WriteFieldBegin(ctx, "a", thrift.I32, 1); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:a: ", p), err) } if err := oprot.WriteI32(ctx, int32(p.A)); err != nil { return thrift.PrependError(fmt.Sprintf("%T.a (1) field write error: ", p), err) } if err := oprot.WriteFieldEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field end error 1:a: ", p), err) } return err } func (p *ThriftArgs_) writeField2(ctx context.Context, oprot thrift.TProtocol) (err error) { if err := oprot.WriteFieldBegin(ctx, "b", thrift.I32, 2); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:b: ", p), err) } if err := oprot.WriteI32(ctx, int32(p.B)); err != nil { return thrift.PrependError(fmt.Sprintf("%T.b (2) field write error: ", p), err) } if err := oprot.WriteFieldEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field end error 2:b: ", p), err) } return err } func (p *ThriftArgs_) Equals(other *ThriftArgs_) bool { if p == other { return true } else if p == nil || other == nil { return false } if p.A != other.A { return false } if p.B != other.B { return false } return true } func (p *ThriftArgs_) String() string { if p == nil { return "" } return fmt.Sprintf("ThriftArgs_(%+v)", *p) } // Attributes: // - C type ThriftReply struct { C int32 `thrift:"c,1" db:"c" json:"c"` } func NewThriftReply() *ThriftReply { return &ThriftReply{} } func (p *ThriftReply) GetC() int32 { return p.C } func (p *ThriftReply) Read(ctx context.Context, iprot thrift.TProtocol) error { if _, err := iprot.ReadStructBegin(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) } for { _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx) if err != nil { return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) } if fieldTypeId == thrift.STOP { break } switch fieldId { case 1: if fieldTypeId == thrift.I32 { if err := p.ReadField1(ctx, iprot); err != nil { return err } } else { if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } default: if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } if err := iprot.ReadFieldEnd(ctx); err != nil { return err } } if err := iprot.ReadStructEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) } return nil } func (p *ThriftReply) ReadField1(ctx context.Context, iprot thrift.TProtocol) error { if v, err := iprot.ReadI32(ctx); err != nil { return thrift.PrependError("error reading field 1: ", err) } else { p.C = v } return nil } func (p *ThriftReply) Write(ctx context.Context, oprot thrift.TProtocol) error { if err := oprot.WriteStructBegin(ctx, "ThriftReply"); err != nil { return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) } if p != nil { if err := p.writeField1(ctx, oprot); err != nil { return err } } if err := oprot.WriteFieldStop(ctx); err != nil { return thrift.PrependError("write field stop error: ", err) } if err := oprot.WriteStructEnd(ctx); err != nil { return thrift.PrependError("write struct stop error: ", err) } return nil } func (p *ThriftReply) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) { if err := oprot.WriteFieldBegin(ctx, "c", thrift.I32, 1); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:c: ", p), err) } if err := oprot.WriteI32(ctx, int32(p.C)); err != nil { return thrift.PrependError(fmt.Sprintf("%T.c (1) field write error: ", p), err) } if err := oprot.WriteFieldEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field end error 1:c: ", p), err) } return err } func (p *ThriftReply) Equals(other *ThriftReply) bool { if p == other { return true } else if p == nil || other == nil { return false } if p.C != other.C { return false } return true } func (p *ThriftReply) String() string { if p == nil { return "" } return fmt.Sprintf("ThriftReply(%+v)", *p) } ================================================ FILE: _testutils/thrift_arith_service.thrift ================================================ struct ThriftArgs { 1: i32 a, 2: i32 b, } struct ThriftReply { 1: i32 c, } ================================================ FILE: client/cache_client_builder.go ================================================ package client import "sync" // CacheClientBuilder defines builder interface to generate RPCCient. type CacheClientBuilder interface { SetCachedClient(client RPCClient, k, servicePath, serviceMethod string) FindCachedClient(k, servicePath, serviceMethod string) RPCClient DeleteCachedClient(client RPCClient, k, servicePath, serviceMethod string) GenerateClient(k, servicePath, serviceMethod string) (client RPCClient, err error) } // RegisterCacheClientBuilder(network string, builder CacheClientBuilder) var cacheClientBuildersMutex sync.RWMutex var cacheClientBuilders = make(map[string]CacheClientBuilder) func RegisterCacheClientBuilder(network string, builder CacheClientBuilder) { cacheClientBuildersMutex.Lock() defer cacheClientBuildersMutex.Unlock() cacheClientBuilders[network] = builder } func getCacheClientBuilder(network string) (CacheClientBuilder, bool) { cacheClientBuildersMutex.RLock() defer cacheClientBuildersMutex.RUnlock() builder, ok := cacheClientBuilders[network] return builder, ok } ================================================ FILE: client/circuit_breaker.go ================================================ package client import ( "errors" "sync/atomic" "time" ) var ( ErrBreakerOpen = errors.New("breaker open") ErrBreakerTimeout = errors.New("breaker time out") ) // ConsecCircuitBreaker is window sliding CircuitBreaker with failure threshold. type ConsecCircuitBreaker struct { lastFailureTime int64 failures uint64 failureThreshold uint64 window time.Duration } // NewConsecCircuitBreaker returns a new ConsecCircuitBreaker. func NewConsecCircuitBreaker(failureThreshold uint64, window time.Duration) *ConsecCircuitBreaker { return &ConsecCircuitBreaker{ failureThreshold: failureThreshold, window: window, } } // Call Circuit function func (cb *ConsecCircuitBreaker) Call(fn func() error, d time.Duration) error { var err error if !cb.ready() { return ErrBreakerOpen } if d == 0 { err = fn() } else { c := make(chan error, 1) go func() { c <- fn() close(c) }() t := time.NewTimer(d) select { case e := <-c: err = e case <-t.C: err = ErrBreakerTimeout } t.Stop() } if err == nil { cb.success() } else { cb.fail() } return err } func (cb *ConsecCircuitBreaker) ready() bool { lastFailureTime := time.Unix(0, atomic.LoadInt64(&cb.lastFailureTime)) if time.Since(lastFailureTime) > cb.window { cb.reset() return true } failures := atomic.LoadUint64(&cb.failures) return failures < cb.failureThreshold } func (cb *ConsecCircuitBreaker) success() { cb.reset() } func (cb *ConsecCircuitBreaker) fail() { atomic.AddUint64(&cb.failures, 1) atomic.StoreInt64(&cb.lastFailureTime, time.Now().UnixNano()) } func (cb *ConsecCircuitBreaker) Success() { cb.success() } func (cb *ConsecCircuitBreaker) Fail() { cb.fail() } func (cb *ConsecCircuitBreaker) Ready() bool { return cb.ready() } func (cb *ConsecCircuitBreaker) reset() { atomic.StoreUint64(&cb.failures, 0) atomic.StoreInt64(&cb.lastFailureTime, time.Now().UnixNano()) } ================================================ FILE: client/circuit_breaker_test.go ================================================ package client import ( "errors" "math/rand" "testing" "time" ) func TestConsecCircuitBreaker(t *testing.T) { count := -1 fn := func() error { count++ if count >= 5 && count < 10 { return nil } return errors.New("test error") } cb := NewConsecCircuitBreaker(5, 100*time.Millisecond) for i := 0; i < 25; i++ { err := cb.Call(fn, 200*time.Millisecond) switch { case i < 5: if err.Error() != "test error" { t.Fatalf("expect %v, got %v", "test error", err) } case i >= 5 && i < 10: if err != ErrBreakerOpen { t.Fatalf("expect %v, got %v", ErrBreakerOpen, err) } case i >= 10 && i < 15: if err != nil { t.Fatalf("expect success, got %v", err) } case i >= 15 && i < 20: if err.Error() != "test error" { t.Fatalf("expect %v, got %v", "test error", err) } case i >= 20 && i < 25: if err != ErrBreakerOpen { t.Fatalf("expect %v, got %v", ErrBreakerOpen, err) } } if i == 9 { // expired time.Sleep(150 * time.Millisecond) } } } func TestCircuitBreakerRace(t *testing.T) { cb := NewConsecCircuitBreaker(2, 50*time.Millisecond) routines := 100 loop := 100000 fn := func() error { if rand.Intn(2) == 1 { return nil } return errors.New("test error") } for r := 0; r < routines; r++ { go func() { for i := 0; i < loop; i++ { cb.Call(fn, 100*time.Millisecond) } }() } } ================================================ FILE: client/client.go ================================================ package client import ( "bufio" "context" "crypto/tls" "errors" "fmt" "io" "net" "net/url" "strconv" "strings" "sync" "time" circuit "github.com/rubyist/circuitbreaker" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" ) const ( XVersion = "X-RPCX-Version" XMessageType = "X-RPCX-MesssageType" XHeartbeat = "X-RPCX-Heartbeat" XOneway = "X-RPCX-Oneway" XMessageStatusType = "X-RPCX-MessageStatusType" XSerializeType = "X-RPCX-SerializeType" XMessageID = "X-RPCX-MessageID" XServicePath = "X-RPCX-ServicePath" XServiceMethod = "X-RPCX-ServiceMethod" XMeta = "X-RPCX-Meta" XErrorMessage = "X-RPCX-ErrorMessage" ) // ServiceError is an error from server. type ServiceError interface { Error() string IsServiceError() bool } // NewServiceError creates a ServiceError with the error message. func NewServiceError(s string) ServiceError { return strErr(s) } // ClientErrorFunc is a function to create a customized error. var ClientErrorFunc func(res *protocol.Message, e string) ServiceError type strErr string func (s strErr) Error() string { return string(s) } func (s strErr) IsServiceError() bool { return true } // DefaultOption is a common option configuration for client. var DefaultOption = Option{ Retries: 3, RPCPath: share.DefaultRPCPath, ConnectTimeout: time.Second, SerializeType: protocol.MsgPack, CompressType: protocol.None, BackupLatency: 10 * time.Millisecond, MaxWaitForHeartbeat: 30 * time.Second, TCPKeepAlivePeriod: time.Minute, BidirectionalBlock: false, TimeToDisallow: time.Minute, } // Breaker is a CircuitBreaker interface. type Breaker interface { Call(func() error, time.Duration) error Fail() Success() Ready() bool } // CircuitBreaker is a default circuit breaker (RateBreaker(0.95, 100)). var CircuitBreaker Breaker = circuit.NewRateBreaker(0.95, 100) // ErrShutdown connection is closed. var ( ErrShutdown = errors.New("connection is shut down") ErrUnsupportedCodec = errors.New("unsupported codec") ) const ( // ReaderBuffsize is used for bufio reader. ReaderBuffsize = 16 * 1024 // WriterBuffsize is used for bufio writer. WriterBuffsize = 16 * 1024 ) type seqKey struct{} // RPCClient is interface that defines one client to call one server. type RPCClient interface { Connect(network, address string) error Go(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call Call(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}) error SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) Close() error RemoteAddr() string RegisterServerMessageChan(ch chan<- *protocol.Message) UnregisterServerMessageChan() IsClosing() bool IsShutdown() bool GetConn() net.Conn } // Client represents a RPC client. type Client struct { option Option Conn net.Conn r *bufio.Reader // w *bufio.Writer mutex sync.Mutex // protects following seq uint64 pending map[uint64]*Call closing bool // user has called Close shutdown bool // server has told us to stop pluginClosed bool // the plugin has been called Plugins PluginContainer ServerMessageChan chan<- *protocol.Message } // NewClient returns a new Client with the option. func NewClient(option Option) *Client { return &Client{ option: option, } } // RemoteAddr returns the remote address. func (client *Client) RemoteAddr() string { return client.Conn.RemoteAddr().String() } // GetConn returns the underlying conn. func (client *Client) GetConn() net.Conn { return client.Conn } // Option contains all options for creating clients. type Option struct { // Group is used to select the services in the same group. Services set group info in their meta. // If it is empty, clients will ignore group. Group string // Retries retries to send Retries int // RetryInterval is the interval between retries RetryInterval time.Duration // Time to disallow the bad server not to be selected TimeToDisallow time.Duration // TLSConfig for tcp and quic TLSConfig *tls.Config // kcp.BlockCrypt Block interface{} // RPCPath for http connection RPCPath string // ConnectTimeout sets timeout for dialing ConnectTimeout time.Duration // IdleTimeout sets max idle time for underlying net.Conns IdleTimeout time.Duration // BackupLatency is used for Failbackup mode. rpcx will sends another request if the first response doesn't return in BackupLatency time. BackupLatency time.Duration // Breaker is used to config CircuitBreaker GenBreaker func() Breaker SerializeType protocol.SerializeType CompressType protocol.CompressType // send heartbeat message to service and check responses Heartbeat bool // interval for heartbeat HeartbeatInterval time.Duration MaxWaitForHeartbeat time.Duration // TCPKeepAlive, if it is zero we don't set keepalive TCPKeepAlivePeriod time.Duration // bidirectional mode, if true serverMessageChan will block to wait message for consume. default false. BidirectionalBlock bool // alaways use the selected server until it is bad Sticky bool // not call server message handler NilCallServerMessageHandler func(msg *protocol.Message) } // Call represents an active RPC. type Call struct { ServicePath string // The name of the service and method to call. ServiceMethod string // The name of the service and method to call. Metadata map[string]string // metadata ResMetadata map[string]string Args interface{} // The argument to the function (*struct). Reply interface{} // The reply from the function (*struct). Error error // After completion, the error status. Done chan *Call // Strobes when call is complete. Raw bool // raw message or not } func (call *Call) done() { if call.Done == nil { // Oneshot return } select { case call.Done <- call: // ok default: log.Debug("rpc: discarding Call reply due to insufficient Done chan capacity") } } // RegisterServerMessageChan registers the channel that receives server requests. func (client *Client) RegisterServerMessageChan(ch chan<- *protocol.Message) { client.ServerMessageChan = ch } // UnregisterServerMessageChan removes ServerMessageChan. func (client *Client) UnregisterServerMessageChan() { client.ServerMessageChan = nil } // IsClosing client is closing or not. func (client *Client) IsClosing() bool { client.mutex.Lock() defer client.mutex.Unlock() return client.closing } // IsShutdown client is shutdown or not. func (client *Client) IsShutdown() bool { client.mutex.Lock() defer client.mutex.Unlock() return client.shutdown } // Go invokes the function asynchronously. It returns the Call structure representing // the invocation. The done channel will signal when the call is complete by returning // the same Call object. If done is nil, Go will allocate a new channel. // If non-nil, done must be buffered or Go will deliberately crash. func (client *Client) Go(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call { call := new(Call) call.ServicePath = servicePath call.ServiceMethod = serviceMethod meta := ctx.Value(share.ReqMetaDataKey) if meta != nil { // copy meta in context to meta in requests call.Metadata = meta.(map[string]string) } if !share.IsShareContext(ctx) { ctx = share.NewContext(ctx) } call.Args = args call.Reply = reply // // we allow done is nil // if done == nil { // done = make(chan *Call, 10) // buffered. // } else { // // If caller passes done != nil, it must arrange that // // done has enough buffer for the number of simultaneous // // RPCs that will be using that channel. If the channel // // is totally unbuffered, it's best not to run at all. // if cap(done) == 0 { // log.Panic("rpc: done channel is unbuffered") // } // } call.Done = done if share.Trace { log.Debugf("client.Go send request for %s.%s, args: %+v in case of client call", servicePath, serviceMethod, args) } go client.send(ctx, call) return call } // Call invokes the named function, waits for it to complete, and returns its error status. func (client *Client) Call(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}) error { return client.call(ctx, servicePath, serviceMethod, args, reply) } func (client *Client) call(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}) error { seq := new(uint64) if sharedCtx, ok := ctx.(*share.Context); ok { sharedCtx.SetValue(seqKey{}, seq) } else { ctx = context.WithValue(ctx, seqKey{}, seq) } if share.Trace { log.Debugf("client.call for %s.%s, args: %+v in case of client call", servicePath, serviceMethod, args) defer func() { log.Debugf("client.call done for %s.%s, args: %+v in case of client call", servicePath, serviceMethod, args) }() } Done := client.Go(ctx, servicePath, serviceMethod, args, reply, make(chan *Call, 10)).Done var err error select { case <-ctx.Done(): // cancel by context client.mutex.Lock() call := client.pending[*seq] delete(client.pending, *seq) client.mutex.Unlock() if call != nil { call.Error = ctx.Err() call.done() } return ctx.Err() case call := <-Done: err = call.Error meta := ctx.Value(share.ResMetaDataKey) if meta != nil && len(call.ResMetadata) > 0 { resMeta := meta.(map[string]string) locker, ok := ctx.Value(share.ContextTagsLock).(*sync.Mutex) if ok { locker.Lock() for k, v := range call.ResMetadata { resMeta[k] = v } resMeta[share.ServerAddress] = client.Conn.RemoteAddr().String() locker.Unlock() } else { for k, v := range call.ResMetadata { resMeta[k] = v } resMeta[share.ServerAddress] = client.Conn.RemoteAddr().String() } } } return err } // SendRaw sends raw messages. You don't care args and replies. func (client *Client) SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) { if sharedCtx, ok := ctx.(*share.Context); ok { sharedCtx.SetValue(seqKey{}, r.Seq()) } else { ctx = context.WithValue(ctx, seqKey{}, r.Seq()) } call := new(Call) call.Raw = true call.ServicePath = r.ServicePath call.ServiceMethod = r.ServiceMethod meta := ctx.Value(share.ReqMetaDataKey) rmeta := make(map[string]string) // copy meta to rmeta if meta != nil { for k, v := range meta.(map[string]string) { rmeta[k] = v } } // copy r.Metadata to rmeta if r.Metadata != nil { for k, v := range r.Metadata { rmeta[k] = v } } if meta != nil { // copy meta in context to meta in requests call.Metadata = rmeta } r.Metadata = rmeta if !share.IsShareContext(ctx) { ctx = share.NewContext(ctx) } done := make(chan *Call, 10) call.Done = done seq := r.Seq() client.mutex.Lock() if client.pending == nil { client.pending = make(map[uint64]*Call) } client.pending[seq] = call client.mutex.Unlock() data := r.EncodeSlicePointer() _, err := client.Conn.Write(*data) protocol.PutData(data) if err != nil { client.mutex.Lock() call = client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() if call != nil { call.Error = err call.done() } return nil, nil, err } if r.IsOneway() { client.mutex.Lock() call = client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() if call != nil { call.done() } return nil, nil, nil } var m map[string]string var payload []byte select { case <-ctx.Done(): // cancel by context client.mutex.Lock() call := client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() if call != nil { call.Error = ctx.Err() call.done() } return nil, nil, ctx.Err() case call := <-done: err = call.Error m = call.ResMetadata if call.Reply != nil { payload = call.Reply.([]byte) } } return m, payload, err } func convertRes2Raw(res *protocol.Message) (map[string]string, []byte, error) { m := make(map[string]string) m[XVersion] = strconv.Itoa(int(res.Version())) if res.IsHeartbeat() { m[XHeartbeat] = "true" } if res.IsOneway() { m[XOneway] = "true" } if res.MessageStatusType() == protocol.Error { m[XMessageStatusType] = "Error" } else { m[XMessageStatusType] = "Normal" } // if res.CompressType() == protocol.Gzip { // m["Content-Encoding"] = "gzip" // } m[XMeta] = urlencode(res.Metadata) m[XSerializeType] = strconv.Itoa(int(res.SerializeType())) m[XMessageID] = strconv.FormatUint(res.Seq(), 10) m[XServicePath] = res.ServicePath m[XServiceMethod] = res.ServiceMethod return m, res.Payload, nil } func urlencode(data map[string]string) string { if len(data) == 0 { return "" } var buf strings.Builder first := true for k, v := range data { if !first { buf.WriteByte('&') } buf.WriteString(url.QueryEscape(k)) buf.WriteByte('=') buf.WriteString(url.QueryEscape(v)) first = false } return buf.String() } func (client *Client) send(ctx context.Context, call *Call) { // Register this call. client.mutex.Lock() if client.shutdown || client.closing { call.Error = ErrShutdown client.mutex.Unlock() call.done() return } isHeartbeat := call.ServicePath == "" && call.ServiceMethod == "" serializeType := client.option.SerializeType if isHeartbeat { serializeType = protocol.MsgPack } codec := share.Codecs[serializeType] if codec == nil { call.Error = ErrUnsupportedCodec client.mutex.Unlock() call.done() return } if client.pending == nil { client.pending = make(map[uint64]*Call) } seq := client.seq client.seq++ client.pending[seq] = call client.mutex.Unlock() if cseq, ok := ctx.Value(seqKey{}).(*uint64); ok { *cseq = seq } // req := protocol.NewMessage() req := protocol.NewMessage() req.SetMessageType(protocol.Request) req.SetSeq(seq) if call.Reply == nil { req.SetOneway(true) } // heartbeat, and use default SerializeType (msgpack) if isHeartbeat { req.SetHeartbeat(true) req.SetSerializeType(protocol.MsgPack) } else { req.SetSerializeType(client.option.SerializeType) } if call.Metadata != nil { req.Metadata = call.Metadata } req.ServicePath = call.ServicePath req.ServiceMethod = call.ServiceMethod data, err := codec.Encode(call.Args) if err != nil { client.mutex.Lock() delete(client.pending, seq) client.mutex.Unlock() call.Error = err call.done() return } if len(data) > 1024 && client.option.CompressType != protocol.None { req.SetCompressType(client.option.CompressType) } req.Payload = data if client.Plugins != nil { _ = client.Plugins.DoClientBeforeEncode(req) } if share.Trace { log.Debugf("client.send for %s.%s, args: %+v in case of client call", call.ServicePath, call.ServiceMethod, call.Args) } allData := req.EncodeSlicePointer() _, err = client.Conn.Write(*allData) protocol.PutData(allData) if share.Trace { log.Debugf("client.sent for %s.%s, args: %+v in case of client call", call.ServicePath, call.ServiceMethod, call.Args) } if err != nil { if e, ok := err.(*net.OpError); ok { if e.Err != nil { err = fmt.Errorf("net.OpError: %s", e.Err.Error()) } else { err = errors.New("net.OpError") } } client.mutex.Lock() call = client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() if call != nil { call.Error = err call.done() } return } isOneway := req.IsOneway() if isOneway { client.mutex.Lock() call = client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() if call != nil { call.done() } } if client.option.IdleTimeout != 0 { _ = client.Conn.SetDeadline(time.Now().Add(client.option.IdleTimeout)) } } func (client *Client) input() { var err error for err == nil { res := protocol.NewMessage() if client.option.IdleTimeout != 0 { _ = client.Conn.SetDeadline(time.Now().Add(client.option.IdleTimeout)) } err = res.Decode(client.r) if err != nil { break } if client.Plugins != nil { _ = client.Plugins.DoClientAfterDecode(res) } seq := res.Seq() var call *Call isServerMessage := (res.MessageType() == protocol.Request && !res.IsHeartbeat() && res.IsOneway()) if !isServerMessage { client.mutex.Lock() call = client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() } if share.Trace { log.Debugf("client.input received %v", res) } switch { case call == nil: if isServerMessage { if client.ServerMessageChan != nil { client.handleServerRequest(res) } else if client.option.NilCallServerMessageHandler != nil { client.option.NilCallServerMessageHandler(res) } continue } case res.MessageStatusType() == protocol.Error: // We've got an error response. Give this to the request if len(res.Metadata) > 0 { call.ResMetadata = res.Metadata // convert server error to a customized error, which implements ServerError interface if ClientErrorFunc != nil { call.Error = ClientErrorFunc(res, res.Metadata[protocol.ServiceError]) } else { call.Error = strErr(res.Metadata[protocol.ServiceError]) } } if call.Raw { call.Metadata, call.Reply, _ = convertRes2Raw(res) call.Metadata[XErrorMessage] = call.Error.Error() call.ResMetadata = res.Metadata } else if len(res.Payload) > 0 { data := res.Payload codec := share.Codecs[res.SerializeType()] if codec != nil { _ = codec.Decode(data, call.Reply) } } call.done() default: if call.Raw { call.Metadata, call.Reply, _ = convertRes2Raw(res) call.ResMetadata = res.Metadata } else { data := res.Payload if len(data) > 0 { codec := share.Codecs[res.SerializeType()] if codec == nil { call.Error = strErr(ErrUnsupportedCodec.Error()) } else { err = codec.Decode(data, call.Reply) if err != nil { call.Error = strErr(err.Error()) } } } if len(res.Metadata) > 0 { call.ResMetadata = res.Metadata } } call.done() } } // Terminate pending calls. if client.ServerMessageChan != nil { req := protocol.NewMessage() req.SetMessageType(protocol.Request) req.SetMessageStatusType(protocol.Error) if req.Metadata == nil { req.Metadata = make(map[string]string) if err != nil { req.Metadata[protocol.ServiceError] = err.Error() } } req.Metadata["server"] = client.Conn.RemoteAddr().String() client.handleServerRequest(req) } client.mutex.Lock() if !client.pluginClosed { if client.Plugins != nil { client.Plugins.DoClientConnectionClose(client.Conn) } client.pluginClosed = true } client.Conn.Close() client.shutdown = true closing := client.closing if e, ok := err.(*net.OpError); ok { if e.Addr != nil || e.Err != nil { err = fmt.Errorf("net.OpError: %s", e.Err.Error()) } else { err = errors.New("net.OpError") } } if err == io.EOF { if closing { err = ErrShutdown } else { err = io.ErrUnexpectedEOF } } for _, call := range client.pending { call.Error = err call.done() } client.mutex.Unlock() if err != nil && !closing { log.Errorf("rpcx: client protocol error: %v", err) } } func (client *Client) handleServerRequest(msg *protocol.Message) { defer func() { if r := recover(); r != nil { log.Errorf("ServerMessageChan may be closed so client remove it. Please add it again if you want to handle server requests. error is %v", r) client.ServerMessageChan = nil } }() serverMessageChan := client.ServerMessageChan if serverMessageChan != nil { if client.option.BidirectionalBlock { serverMessageChan <- msg } else { select { case serverMessageChan <- msg: default: log.Warnf("ServerMessageChan may be full so the server request %d has been dropped", msg.Seq()) } } } } func (client *Client) heartbeat() { t := time.NewTicker(client.option.HeartbeatInterval) if client.option.MaxWaitForHeartbeat == 0 { client.option.MaxWaitForHeartbeat = 30 * time.Second } for range t.C { if client.IsShutdown() || client.IsClosing() { t.Stop() return } request := time.Now().UnixNano() reply := int64(0) ctx, cancel := context.WithTimeout(context.Background(), client.option.MaxWaitForHeartbeat) err := client.Call(ctx, "", "", &request, &reply) abnormal := false if ctx.Err() != nil { log.Warnf("failed to heartbeat to %s, context err: %v", client.Conn.RemoteAddr().String(), ctx.Err()) abnormal = true } cancel() if err != nil { log.Warnf("failed to heartbeat to %s: %v", client.Conn.RemoteAddr().String(), err) abnormal = true } if reply != request { log.Warnf("reply %d in heartbeat to %s is different from request %d", reply, client.Conn.RemoteAddr().String(), request) } if abnormal { client.Close() } } } // Close calls the underlying connection's Close method. If the connection is already // shutting down, ErrShutdown is returned. func (client *Client) Close() error { client.mutex.Lock() for seq, call := range client.pending { delete(client.pending, seq) if call != nil { call.Error = ErrShutdown call.done() } } var err error if !client.pluginClosed { if client.Plugins != nil { client.Plugins.DoClientConnectionClose(client.Conn) } client.pluginClosed = true err = client.Conn.Close() } if client.closing || client.shutdown { client.mutex.Unlock() return ErrShutdown } client.closing = true client.mutex.Unlock() return err } ================================================ FILE: client/client_test.go ================================================ package client import ( "context" "fmt" "math/rand" "net" "sync" "testing" "time" testutils "github.com/smallnest/rpcx/_testutils" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/server" ) type Args struct { A int B int } type Reply struct { C int } type Arith int func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error { reply.C = args.A * args.B return nil } type PBArith int func (t *PBArith) Mul(ctx context.Context, args *testutils.ProtoArgs, reply *testutils.ProtoReply) error { reply.C = args.A * args.B return nil } func (t *Arith) ThriftMul(ctx context.Context, args *testutils.ThriftArgs_, reply *testutils.ThriftReply) error { reply.C = args.A * args.B return nil } type Bidirectional struct { *server.Server } func (t *Bidirectional) Mul(ctx context.Context, args *Args, reply *Reply) error { conn := ctx.Value(server.RemoteConnContextKey).(net.Conn) reply.C = args.A * args.B t.SendMessage(conn, "test_service_path", "test_service_method", nil, []byte("abcde")) return nil } func TestClient_IT(t *testing.T) { s := server.NewServer() _ = s.RegisterName("Arith", new(Arith), "") _ = s.RegisterName("PBArith", new(PBArith), "") go func() { _ = s.Serve("tcp", "127.0.0.1:0") }() defer s.Close() time.Sleep(500 * time.Millisecond) addr := s.Address().String() client := &Client{ option: DefaultOption, } err := client.Connect("tcp", addr) if err != nil { t.Fatalf("failed to connect: %v", err) } defer client.Close() args := &Args{ A: 10, B: 20, } reply := &Reply{} err = client.Call(context.Background(), "Arith", "Mul", args, reply) if err != nil { t.Fatalf("failed to call: %v", err) } if reply.C != 200 { t.Fatalf("expect 200 but got %d", reply.C) } err = client.Call(context.Background(), "Arith", "Add", args, reply) if err == nil { t.Fatal("expect an error but got nil") } client.option.SerializeType = protocol.MsgPack reply = &Reply{} err = client.Call(context.Background(), "Arith", "Mul", args, reply) if err != nil { t.Fatalf("failed to call: %v", err) } if reply.C != 200 { t.Fatalf("expect 200 but got %d", reply.C) } client.option.SerializeType = protocol.ProtoBuffer pbArgs := &testutils.ProtoArgs{ A: 10, B: 20, } pbReply := &testutils.ProtoReply{} err = client.Call(context.Background(), "PBArith", "Mul", pbArgs, pbReply) if err != nil { t.Fatalf("failed to call: %v", err) } if pbReply.C != 200 { t.Fatalf("expect 200 but got %d", pbReply.C) } } func TestClient_IT_Concurrency(t *testing.T) { s := server.NewServer() _ = s.RegisterName("PBArith", new(PBArith), "") go func() { _ = s.Serve("tcp", "127.0.0.1:0") }() defer s.Close() time.Sleep(500 * time.Millisecond) addr := s.Address().String() client := &Client{ option: DefaultOption, } err := client.Connect("tcp", addr) if err != nil { t.Fatalf("failed to connect: %v", err) } defer client.Close() var wg sync.WaitGroup wg.Add(100) for i := 0; i < 100; i++ { i := i go testSendRaw(t, client, uint64(i), rand.Int31(), rand.Int31(), &wg) } wg.Wait() } func testSendRaw(t *testing.T, client *Client, seq uint64, x, y int32, wg *sync.WaitGroup) { defer wg.Done() rpcxReq := protocol.NewMessage() rpcxReq.SetMessageType(protocol.Request) rpcxReq.SetSeq(seq) rpcxReq.ServicePath = "PBArith" rpcxReq.ServiceMethod = "Mul" rpcxReq.SetSerializeType(protocol.ProtoBuffer) rpcxReq.SetOneway(false) pbArgs := &testutils.ProtoArgs{ A: x, B: y, } data, _ := pbArgs.Marshal() rpcxReq.Payload = data _, reply, err := client.SendRaw(context.Background(), rpcxReq) if err != nil { t.Errorf("failed to call SendRaw: %v", err) return } pbReply := &testutils.ProtoReply{} err = pbReply.Unmarshal(reply) if err != nil { t.Errorf("failed to unmarshal reply: %v", err) return } if pbReply.C != x*y { t.Errorf("expect %d but got %d", x*y, pbReply.C) return } } func TestClient_Res_Reset(t *testing.T) { var res = protocol.NewMessage() res.Payload = []byte{1, 2, 3, 4, 5, 6, 7, 8} data := res.Payload res.Reset() if len(data) == 0 { t.Fatalf("data has been set to empty after response has been reset: %v", data) } } func TestClient_Bidirectional(t *testing.T) { s := server.NewServer() _ = s.RegisterName("Bidirectional", &Bidirectional{Server: s}, "") go func() { _ = s.Serve("tcp", "127.0.0.1:0") }() defer s.Close() time.Sleep(500 * time.Millisecond) addr := s.Address().String() opt := DefaultOption var receive string opt.NilCallServerMessageHandler = func(msg *protocol.Message) { fmt.Printf("receive msg from server: %s\n", msg.Payload) receive = string(msg.Payload) } client := &Client{ option: opt, } err := client.Connect("tcp", addr) if err != nil { t.Fatalf("failed to connect: %v", err) } defer client.Close() args := &Args{ A: 10, B: 20, } reply := &Reply{} err = client.Call(context.Background(), "Bidirectional", "Mul", args, reply) if err != nil { t.Fatalf("failed to call: %v", err) } if receive != "abcde" { t.Fatalf("expect abcde but got %s", receive) } if reply.C != 200 { t.Fatalf("expect 200 but got %d", reply.C) } } ================================================ FILE: client/connection.go ================================================ package client import ( "bufio" "crypto/tls" "errors" "fmt" "io" "net" "net/http" "time" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/share" "golang.org/x/net/websocket" ) type ConnFactoryFn func(c *Client, network, address string) (net.Conn, error) var ConnFactories = make(map[string]ConnFactoryFn) func init() { ConnFactories["http"] = newDirectHTTPConn ConnFactories["kcp"] = newDirectKCPConn ConnFactories["quic"] = newDirectQuicConn ConnFactories["unix"] = newDirectConn ConnFactories["memu"] = newMemuConn ConnFactories["iouring"] = newIOUringConn } // Connect connects the server via specified network. func (client *Client) Connect(network, address string) error { var conn net.Conn var err error switch network { case "http": conn, err = newDirectHTTPConn(client, network, address) case "ws", "wss": conn, err = newDirectWSConn(client, network, address) default: fn := ConnFactories[network] if fn != nil { conn, err = fn(client, network, address) } else { conn, err = newDirectConn(client, network, address) } } if err == nil && conn != nil { if tc, ok := conn.(*net.TCPConn); ok && client.option.TCPKeepAlivePeriod > 0 { _ = tc.SetKeepAlive(true) _ = tc.SetKeepAlivePeriod(client.option.TCPKeepAlivePeriod) } if client.option.IdleTimeout != 0 { _ = conn.SetDeadline(time.Now().Add(client.option.IdleTimeout)) } if client.Plugins != nil { conn, err = client.Plugins.DoConnCreated(conn) if err != nil { return err } } client.Conn = conn client.r = bufio.NewReaderSize(conn, ReaderBuffsize) // c.w = bufio.NewWriterSize(conn, WriterBuffsize) // start reading and writing since connected go client.input() if client.option.Heartbeat && client.option.HeartbeatInterval > 0 { go client.heartbeat() } } if err != nil && client.Plugins != nil { client.Plugins.DoConnCreateFailed(network, address) } return err } func newDirectConn(c *Client, network, address string) (net.Conn, error) { var conn net.Conn var tlsConn *tls.Conn var err error if c == nil { err = fmt.Errorf("nil client") return nil, err } if c.option.TLSConfig != nil { dialer := &net.Dialer{ Timeout: c.option.ConnectTimeout, } tlsConn, err = tls.DialWithDialer(dialer, network, address, c.option.TLSConfig) // or conn:= tls.Client(netConn, &config) conn = net.Conn(tlsConn) } else { conn, err = net.DialTimeout(network, address, c.option.ConnectTimeout) } if err != nil { log.Warnf("failed to dial server: %v", err) return nil, err } return conn, nil } var connected = "200 Connected to rpcx" func newDirectHTTPConn(c *Client, network, address string) (net.Conn, error) { if c == nil { return nil, errors.New("empty client") } path := c.option.RPCPath if path == "" { path = share.DefaultRPCPath } var conn net.Conn var tlsConn *tls.Conn var err error if c.option.TLSConfig != nil { dialer := &net.Dialer{ Timeout: c.option.ConnectTimeout, } tlsConn, err = tls.DialWithDialer(dialer, "tcp", address, c.option.TLSConfig) // or conn:= tls.Client(netConn, &config) conn = net.Conn(tlsConn) } else { conn, err = net.DialTimeout("tcp", address, c.option.ConnectTimeout) } if err != nil { log.Errorf("failed to dial server: %v", err) return nil, err } _, err = io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n") if err != nil { // Dial() success but Write() failed here, close the successfully // created conn before return. conn.Close() log.Errorf("failed to make CONNECT: %v", err) return nil, err } // Require successful HTTP response // before switching to RPC protocol. resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"}) if err == nil && resp.Status == connected { return conn, nil } if err == nil { log.Errorf("unexpected HTTP response: %v", err) err = errors.New("unexpected HTTP response: " + resp.Status) } conn.Close() return nil, &net.OpError{ Op: "dial-http", Net: network + " " + address, Addr: nil, Err: err, } } func newDirectWSConn(c *Client, network, address string) (net.Conn, error) { if c == nil { return nil, errors.New("empty client") } path := c.option.RPCPath if path == "" { path = share.DefaultRPCPath } var conn net.Conn var err error // url := "ws://localhost:12345/ws" var url, origin string if network == "ws" { url = fmt.Sprintf("ws://%s%s", address, path) origin = fmt.Sprintf("http://%s", address) } else { url = fmt.Sprintf("wss://%s%s", address, path) origin = fmt.Sprintf("https://%s", address) } if c.option.TLSConfig != nil { config, erri := websocket.NewConfig(url, origin) if erri != nil { return nil, erri } config.TlsConfig = c.option.TLSConfig conn, err = websocket.DialConfig(config) } else { conn, err = websocket.Dial(url, "", origin) } return conn, err } ================================================ FILE: client/connection_iouring.go ================================================ package client import ( "net" ) // experimental func newIOUringConn(c *Client, network, address string) (net.Conn, error) { return newDirectConn(c, "tcp", address) } ================================================ FILE: client/connection_iouring_test.go ================================================ //go:build linux // +build linux package client // func TestXClient_IOUring(t *testing.T) { // s := server.NewServer() // s.RegisterName("Arith", new(Arith), "") // go s.Serve("iouring", "127.0.0.1:8972") // defer s.Close() // time.Sleep(500 * time.Millisecond) // addr := s.Address().String() // d, err := NewPeer2PeerDiscovery("iouring@"+addr, "desc=a test service") // if err != nil { // t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err) // } // xclient := NewXClient("Arith", Failtry, RandomSelect, d, DefaultOption) // defer xclient.Close() // args := &Args{ // A: 10, // B: 20, // } // reply := &Reply{} // err = xclient.Call(context.Background(), "Mul", args, reply) // if err != nil { // t.Fatalf("failed to call: %v", err) // } // if reply.C != 200 { // t.Fatalf("expect 200 but got %d", reply.C) // } // } ================================================ FILE: client/connection_kcp.go ================================================ // +build kcp package client import ( "net" kcp "github.com/xtaci/kcp-go" ) func newDirectKCPConn(c *Client, network, address string) (net.Conn, error) { return kcp.DialWithOptions(address, c.option.Block.(kcp.BlockCrypt), 10, 3) } ================================================ FILE: client/connection_memu.go ================================================ package client import ( "net" "github.com/akutz/memconn" ) func newMemuConn(c *Client, network, address string) (net.Conn, error) { return memconn.Dial(network, address) } ================================================ FILE: client/connection_nonkcp.go ================================================ // +build !kcp package client import ( "errors" "net" ) func newDirectKCPConn(c *Client, network, address string) (net.Conn, error) { return nil, errors.New("kcp unsupported") } ================================================ FILE: client/connection_nonquic.go ================================================ //go:build !quic // +build !quic package client import ( "errors" "net" ) func newDirectQuicConn(c *Client, network, address string) (net.Conn, error) { return nil, errors.New("quic unsupported") } ================================================ FILE: client/connection_quic.go ================================================ //go:build quic // +build quic package client import ( "crypto/tls" "net" "github.com/quic-go/quic-go" "github.com/smallnest/quick" ) func newDirectQuicConn(c *Client, network, address string) (net.Conn, error) { tlsConf := c.option.TLSConfig if tlsConf == nil { tlsConf = &tls.Config{InsecureSkipVerify: true} } if len(tlsConf.NextProtos) == 0 { tlsConf.NextProtos = []string{"rpcx"} } quicConfig := &quic.Config{} return quick.Dial(address, tlsConf, quicConfig) } ================================================ FILE: client/connection_rdma.go ================================================ //go:build rdma // +build rdma package client import ( "errors" "net" "github.com/smallnest/rsocket" ) func init() { ConnFactories["rdma"] = newRDMAConn } func newRDMAConn(c *Client, network, address string) (net.Conn, error) { if network != "rdma" { return nil, errors.New("network is not rdma") } return rsocket.DialTCP(address) } ================================================ FILE: client/discovery.go ================================================ package client import ( "encoding/json" "os" "path/filepath" "sync" ) // ServiceDiscoveryFilter can be used to filter services with customized logics. // Servers can register its services but clients can use the customized filter to select some services. // It returns true if ServiceDiscovery wants to use this service, otherwise it returns false. type ServiceDiscoveryFilter func(kvp *KVPair) bool // ServiceDiscovery defines ServiceDiscovery of zookeeper, etcd and consul type ServiceDiscovery interface { GetServices() []*KVPair // return all services in the registry WatchService() chan []*KVPair // watch the change of services, it's a golang channel RemoveWatcher(ch chan []*KVPair) Clone(servicePath string) (ServiceDiscovery, error) SetFilter(ServiceDiscoveryFilter) // set customized filter to filter services Close() } type cachedServiceDiscovery struct { threshold int cachedFile string cached []*KVPair chansLock sync.RWMutex chans map[chan []*KVPair]chan []*KVPair ServiceDiscovery } // CacheDiscovery caches the services in a file, it will return the cached services if the number of services is greater than threshold. // It is very useful when the register center is lost. func CacheDiscovery(threshold int, cachedFile string, discovery ServiceDiscovery) ServiceDiscovery { if cachedFile == "" { cachedFile = ".cache/discovery.json" } cachedFileDir := filepath.Dir(cachedFile) if _, err := os.Stat(cachedFileDir); os.IsNotExist(err) { // 目录不存在,创建目录 if err := os.MkdirAll(cachedFileDir, os.ModePerm); err != nil { panic(err) } } return &cachedServiceDiscovery{ threshold: threshold, cachedFile: cachedFile, ServiceDiscovery: discovery, chans: make(map[chan []*KVPair]chan []*KVPair), } } func (cd *cachedServiceDiscovery) GetServices() []*KVPair { kvPairs := cd.ServiceDiscovery.GetServices() n := len(kvPairs) if n > cd.threshold { if n > len(cd.cached) { // strictly we should compare the content of the cached file, but only compare the length for performance cd.cached = kvPairs cd.storeCached(kvPairs) } return kvPairs } if len(cd.cached) == 0 { cd.loadCached() } return cd.cached } func (cd *cachedServiceDiscovery) WatchService() chan []*KVPair { ch := cd.ServiceDiscovery.WatchService() cachedCh := make(chan []*KVPair, 10) cd.chansLock.Lock() cd.chans[cachedCh] = ch cd.chansLock.Unlock() go func() { defer recover() for { kvPairs, ok := <-ch if !ok { close(cachedCh) return } n := len(kvPairs) if n > len(cd.cached) { cd.cached = kvPairs cd.storeCached(kvPairs) } cachedCh <- kvPairs } }() return cachedCh } func (cd *cachedServiceDiscovery) RemoveWatcher(ch chan []*KVPair) { cd.chansLock.Lock() origin := cd.chans[ch] delete(cd.chans, ch) cd.chansLock.Unlock() if origin != nil { cd.ServiceDiscovery.RemoveWatcher(origin) } } func (cd *cachedServiceDiscovery) storeCached(kvPairs []*KVPair) { data, _ := json.Marshal(kvPairs) os.WriteFile(cd.cachedFile, data, 0644) } func (cd *cachedServiceDiscovery) loadCached() (kvPairs []*KVPair) { data, err := os.ReadFile(cd.cachedFile) if err != nil || len(data) == 0 { return } json.Unmarshal(data, &kvPairs) return kvPairs } ================================================ FILE: client/dns_discovery.go ================================================ package client import ( "fmt" "net" "sort" "sync" "time" "github.com/smallnest/rpcx/log" ) // DNSDiscovery is based on DNS a record. // You must set port and network info when you create the DNSDiscovery. type DNSDiscovery struct { domain string network string port int d time.Duration pairsMu sync.RWMutex pairs []*KVPair chans []chan []*KVPair mu sync.Mutex filter ServiceDiscoveryFilter stopCh chan struct{} } // NewDNSDiscovery returns a new DNSDiscovery. func NewDNSDiscovery(domain string, network string, port int, d time.Duration) (*DNSDiscovery, error) { discovery := &DNSDiscovery{domain: domain, network: network, port: port, d: d} discovery.lookup() go discovery.watch() return discovery, nil } // Clone clones this ServiceDiscovery with new servicePath. func (d *DNSDiscovery) Clone(servicePath string) (ServiceDiscovery, error) { return NewDNSDiscovery(d.domain, d.network, d.port, d.d) } // SetFilter sets the filer. func (d *DNSDiscovery) SetFilter(filter ServiceDiscoveryFilter) { d.filter = filter } // GetServices returns the static server func (d *DNSDiscovery) GetServices() []*KVPair { d.pairsMu.RLock() defer d.pairsMu.RUnlock() return d.pairs } // WatchService returns a nil chan. func (d *DNSDiscovery) WatchService() chan []*KVPair { d.mu.Lock() defer d.mu.Unlock() ch := make(chan []*KVPair, 10) d.chans = append(d.chans, ch) return ch } func (d *DNSDiscovery) RemoveWatcher(ch chan []*KVPair) { d.mu.Lock() defer d.mu.Unlock() var chans []chan []*KVPair for _, c := range d.chans { if c == ch { continue } chans = append(chans, c) } d.chans = chans } func (d *DNSDiscovery) lookup() { var pairs []*KVPair // latest servers ips, err := net.LookupIP(d.domain) if err != nil { log.Errorf("failed to lookup %s: %v", d.domain, err) return } for _, ip := range ips { pair := &KVPair{Key: fmt.Sprintf("%s@%s:%d", d.network, ip.String(), d.port)} if d.filter != nil && !d.filter(pair) { continue } pairs = append(pairs, pair) } if len(pairs) > 0 { sort.Slice(pairs, func(i, j int) bool { return pairs[i].Key < pairs[j].Key }) } d.pairsMu.Lock() d.pairs = pairs d.pairsMu.Unlock() d.mu.Lock() for _, ch := range d.chans { ch := ch go func() { defer func() { recover() }() select { case ch <- pairs: case <-time.After(time.Minute): log.Warn("chan is full and new change has been dropped") } }() } d.mu.Unlock() } func (d *DNSDiscovery) watch() { tick := time.NewTicker(d.d) defer tick.Stop() for { select { case <-d.stopCh: return case <-tick.C: d.lookup() } } } func (d *DNSDiscovery) Close() { close(d.stopCh) } ================================================ FILE: client/failmode_enumer.go ================================================ // Code generated by "enumer -type=FailMode"; DO NOT EDIT. package client import ( "fmt" ) const _FailModeName = "FailoverFailfastFailtryFailbackup" var _FailModeIndex = [...]uint8{0, 8, 16, 23, 33} func (i FailMode) String() string { if i < 0 || i >= FailMode(len(_FailModeIndex)-1) { return fmt.Sprintf("FailMode(%d)", i) } return _FailModeName[_FailModeIndex[i]:_FailModeIndex[i+1]] } var _FailModeValues = []FailMode{0, 1, 2, 3} var _FailModeNameToValueMap = map[string]FailMode{ _FailModeName[0:8]: 0, _FailModeName[8:16]: 1, _FailModeName[16:23]: 2, _FailModeName[23:33]: 3, } // FailModeString retrieves an enum value from the enum constants string name. // Throws an error if the param is not part of the enum. func FailModeString(s string) (FailMode, error) { if val, ok := _FailModeNameToValueMap[s]; ok { return val, nil } return 0, fmt.Errorf("%s does not belong to FailMode values", s) } // FailModeValues returns all values of the enum func FailModeValues() []FailMode { return _FailModeValues } // IsAFailMode returns "true" if the value is listed in the enum definition. "false" otherwise func (i FailMode) IsAFailMode() bool { for _, v := range _FailModeValues { if i == v { return true } } return false } ================================================ FILE: client/geo_utils.go ================================================ package client import ( "math" ) // https://gist.github.com/cdipaolo/d3f8db3848278b49db68 func getDistanceFrom(lat1, lon1, lat2, lon2 float64) float64 { var la1, lo1, la2, lo2, r float64 la1 = lat1 * math.Pi / 180 lo1 = lon1 * math.Pi / 180 la2 = lat2 * math.Pi / 180 lo2 = lon2 * math.Pi / 180 r = 6378100 // Earth radius in METERS // calculate h := hsin(la2-la1) + math.Cos(la1)*math.Cos(la2)*hsin(lo2-lo1) return 2 * r * math.Asin(math.Sqrt(h)) } func hsin(theta float64) float64 { return math.Pow(math.Sin(theta/2), 2) } ================================================ FILE: client/hash_utils.go ================================================ package client import ( "fmt" "hash/fnv" ) // Hash consistently chooses a hash bucket number in the range [0, numBuckets) for the given key. numBuckets must be >= 1. func Hash(key uint64, buckets int32) int32 { if buckets <= 0 { buckets = 1 } var b, j int64 for j < int64(buckets) { b = j key = key*2862933555777941757 + 1 j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1))) } return int32(b) } // HashString get a hash value of a string func HashString(s string) uint64 { h := fnv.New64a() h.Write([]byte(s)) return h.Sum64() } // HashServiceAndArgs define a hash function type HashServiceAndArgs func(len int, options ...interface{}) int // ConsistentFunction define a hash function // Return service address, like "tcp@127.0.0.1:8970" type ConsistentAddrStrFunction func(options ...interface{}) string func genKey(options ...interface{}) uint64 { keyString := "" for _, opt := range options { keyString = keyString + "/" + toString(opt) } return HashString(keyString) } // JumpConsistentHash selects a server by serviceMethod and args func JumpConsistentHash(len int, options ...interface{}) int { return int(Hash(genKey(options...), int32(len))) } func toString(obj interface{}) string { return fmt.Sprintf("%v", obj) } ================================================ FILE: client/mdns_discovery.go ================================================ package client import ( "context" "encoding/json" "net/url" "sync" "time" "github.com/grandcat/zeroconf" "github.com/smallnest/rpcx/log" ) type serviceMeta struct { Service string `json:"service,omitempty"` Meta string `json:"meta,omitempty"` ServiceAddress string `json:"service_address,omitempty"` } // MDNSDiscovery is a mdns service discovery. // It always returns the registered servers in mdns. type MDNSDiscovery struct { Timeout time.Duration WatchInterval time.Duration domain string service string pairsMu sync.RWMutex pairs []*KVPair chans []chan []*KVPair mu sync.Mutex filter ServiceDiscoveryFilter stopCh chan struct{} } // NewMDNSDiscovery returns a new MDNSDiscovery. // If domain is empty, use "local." in default. func NewMDNSDiscovery(service string, timeout time.Duration, watchInterval time.Duration, domain string) (*MDNSDiscovery, error) { if domain == "" { domain = "local." } d := &MDNSDiscovery{service: service, Timeout: timeout, WatchInterval: watchInterval, domain: domain} d.stopCh = make(chan struct{}) var err error d.pairsMu.Lock() d.pairs, err = d.browse() d.pairsMu.Unlock() if err != nil { log.Warnf("failed to browse services: %v", err) } go d.watch() return d, nil } // Clone clones this ServiceDiscovery with new servicePath. func (d *MDNSDiscovery) Clone(servicePath string) (ServiceDiscovery, error) { return NewMDNSDiscovery(servicePath, d.Timeout, d.WatchInterval, d.domain) } // SetFilter sets the filer. func (d *MDNSDiscovery) SetFilter(filter ServiceDiscoveryFilter) { d.filter = filter } // GetServices returns the servers func (d *MDNSDiscovery) GetServices() []*KVPair { d.pairsMu.RLock() defer d.pairsMu.RUnlock() return d.pairs } // WatchService returns a nil chan. func (d *MDNSDiscovery) WatchService() chan []*KVPair { d.mu.Lock() defer d.mu.Unlock() ch := make(chan []*KVPair, 10) d.chans = append(d.chans, ch) return ch } func (d *MDNSDiscovery) RemoveWatcher(ch chan []*KVPair) { d.mu.Lock() defer d.mu.Unlock() var chans []chan []*KVPair for _, c := range d.chans { if c == ch { continue } chans = append(chans, c) } d.chans = chans } func (d *MDNSDiscovery) watch() { t := time.NewTicker(d.WatchInterval) for { select { case <-d.stopCh: t.Stop() log.Info("discovery has been closed") return case <-t.C: pairs, err := d.browse() if err == nil { d.pairsMu.Lock() d.pairs = pairs d.pairsMu.Unlock() d.mu.Lock() for _, ch := range d.chans { ch := ch go func() { defer func() { recover() }() select { case ch <- pairs: case <-time.After(time.Minute): log.Warn("chan is full and new change has ben dropped") } }() } d.mu.Unlock() } } } } func (d *MDNSDiscovery) browse() ([]*KVPair, error) { resolver, err := zeroconf.NewResolver(nil) if err != nil { log.Warnf("Failed to initialize resolver: %v", err) return nil, err } entries := make(chan *zeroconf.ServiceEntry) var totalServices []*KVPair var services []*serviceMeta done := make(chan struct{}) go func(results <-chan *zeroconf.ServiceEntry) { for entry := range entries { s, _ := url.QueryUnescape(entry.Text[0]) err := json.Unmarshal([]byte(s), &services) if err != nil { log.Warnf("Failed to browse: %v", err) continue } for _, sm := range services { pair := &KVPair{ Key: sm.ServiceAddress, Value: sm.Meta, } if d.filter != nil && !d.filter(pair) { continue } totalServices = append(totalServices, pair) } } close(done) }(entries) ctx, cancel := context.WithTimeout(context.Background(), d.Timeout) defer cancel() err = resolver.Browse(ctx, "_rpcxservices", d.domain, entries) if err != nil { log.Warnf("Failed to browse: %v", err) } <-done return totalServices, nil } func (d *MDNSDiscovery) Close() { close(d.stopCh) } ================================================ FILE: client/mode.go ================================================ package client // FailMode decides how clients action when clients fail to invoke services type FailMode int const ( // Failover selects another server automaticaly Failover FailMode = iota // Failfast returns error immediately Failfast // Failtry use current client again Failtry // Failbackup select another server if the first server doesn't respond in specified time and use the fast response. Failbackup ) // SelectMode defines the algorithm of selecting a services from candidates. type SelectMode int const ( // RandomSelect is selecting randomly RandomSelect SelectMode = iota // RoundRobin is selecting by round robin RoundRobin // WeightedRoundRobin is selecting by weighted round robin WeightedRoundRobin // WeightedICMP is selecting by weighted Ping time WeightedICMP // ConsistentHash is selecting by hashing ConsistentHash // Closest is selecting the closest server Closest // SelectByUser is selecting by implementation of users SelectByUser = 1000 ) ================================================ FILE: client/multiple_servers_discovery.go ================================================ package client import ( "sync" "time" "github.com/smallnest/rpcx/log" ) // MultipleServersDiscovery is a multiple servers service discovery. // It always returns the current servers and users can change servers dynamically. type MultipleServersDiscovery struct { pairsMu sync.RWMutex pairs []*KVPair chans []chan []*KVPair mu sync.Mutex } // NewMultipleServersDiscovery returns a new MultipleServersDiscovery. func NewMultipleServersDiscovery(pairs []*KVPair) (*MultipleServersDiscovery, error) { return &MultipleServersDiscovery{ pairs: pairs, }, nil } // Clone clones this ServiceDiscovery with new servicePath. func (d *MultipleServersDiscovery) Clone(servicePath string) (ServiceDiscovery, error) { return d, nil } // SetFilter sets the filer. func (d *MultipleServersDiscovery) SetFilter(filter ServiceDiscoveryFilter) { } // GetServices returns the configured server func (d *MultipleServersDiscovery) GetServices() []*KVPair { d.pairsMu.RLock() defer d.pairsMu.RUnlock() return d.pairs } // WatchService returns a nil chan. func (d *MultipleServersDiscovery) WatchService() chan []*KVPair { d.mu.Lock() defer d.mu.Unlock() ch := make(chan []*KVPair, 10) d.chans = append(d.chans, ch) return ch } func (d *MultipleServersDiscovery) RemoveWatcher(ch chan []*KVPair) { d.mu.Lock() defer d.mu.Unlock() var chans []chan []*KVPair for _, c := range d.chans { if c == ch { continue } chans = append(chans, c) } d.chans = chans } // Update is used to update servers at runtime. func (d *MultipleServersDiscovery) Update(pairs []*KVPair) { d.mu.Lock() defer d.mu.Unlock() for _, ch := range d.chans { ch := ch go func() { defer func() { recover() }() select { case ch <- pairs: case <-time.After(time.Minute): log.Warn("chan is full and new change has been dropped") } }() } d.pairsMu.Lock() d.pairs = pairs d.pairsMu.Unlock() } func (d *MultipleServersDiscovery) Close() { } ================================================ FILE: client/oneclient.go ================================================ package client import ( "context" "fmt" "io" "net" "sync" "github.com/smallnest/rpcx/share" multierror "github.com/hashicorp/go-multierror" "github.com/smallnest/rpcx/protocol" ) // OneClient wraps servicesPath and XClients. // Users can use a shared oneclient to access multiple services. type OneClient struct { xclients map[string]XClient mu sync.RWMutex failMode FailMode selectMode SelectMode discovery ServiceDiscovery option Option selectors map[string]Selector Plugins PluginContainer latitude float64 longitude float64 auth string serverMessageChan chan<- *protocol.Message } // NewOneClient creates a OneClient that supports service discovery and service governance. func NewOneClient(failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) *OneClient { return &OneClient{ failMode: failMode, selectMode: selectMode, discovery: discovery, option: option, xclients: make(map[string]XClient), selectors: make(map[string]Selector), } } // NewBidirectionalOneClient creates a new xclient that can receive notifications from servers. func NewBidirectionalOneClient(failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) *OneClient { return &OneClient{ failMode: failMode, selectMode: selectMode, discovery: discovery, option: option, xclients: make(map[string]XClient), selectors: make(map[string]Selector), serverMessageChan: serverMessageChan, } } // SetSelector sets customized selector by users. func (c *OneClient) SetSelector(servicePath string, s Selector) { c.mu.Lock() c.selectors[servicePath] = s if xclient, ok := c.xclients[servicePath]; ok { xclient.SetSelector(s) } c.mu.Unlock() } // SetPlugins sets client's plugins. func (c *OneClient) SetPlugins(plugins PluginContainer) { c.Plugins = plugins c.mu.RLock() for _, v := range c.xclients { v.SetPlugins(plugins) } c.mu.RUnlock() } func (c *OneClient) GetPlugins() PluginContainer { return c.Plugins } // ConfigGeoSelector sets location of client's latitude and longitude, // and use newGeoSelector. func (c *OneClient) ConfigGeoSelector(latitude, longitude float64) { c.selectMode = Closest c.latitude = latitude c.longitude = longitude c.mu.RLock() for _, v := range c.xclients { v.ConfigGeoSelector(latitude, longitude) } c.mu.RUnlock() } // Auth sets s token for Authentication. func (c *OneClient) Auth(auth string) { c.auth = auth c.mu.RLock() for _, v := range c.xclients { v.Auth(auth) } c.mu.RUnlock() } // Go invokes the function asynchronously. It returns the Call structure representing the invocation. The done channel will signal when the call is complete by returning the same Call object. If done is nil, Go will allocate a new channel. If non-nil, done must be buffered or Go will deliberately crash. // It does not use FailMode. func (c *OneClient) Go(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}, done chan *Call) (*Call, error) { c.mu.RLock() xclient := c.xclients[servicePath] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[servicePath] if xclient == nil { xclient, err = c.newXClient(servicePath) c.xclients[servicePath] = xclient } c.mu.Unlock() if err != nil { return nil, err } } return xclient.Go(ctx, serviceMethod, args, reply, done) } func (c *OneClient) newXClient(servicePath string) (xclient XClient, err error) { defer func() { if r := recover(); r != nil { if e, ok := r.(error); ok { err = e } else { err = fmt.Errorf("%v", r) } } }() d, err := c.discovery.Clone(servicePath) if err != nil { return nil, err } if c.serverMessageChan == nil { xclient = NewXClient(servicePath, c.failMode, c.selectMode, d, c.option) } else { xclient = NewBidirectionalXClient(servicePath, c.failMode, c.selectMode, d, c.option, c.serverMessageChan) } if c.Plugins != nil { xclient.SetPlugins(c.Plugins) } if s, ok := c.selectors[servicePath]; ok { xclient.SetSelector(s) } if c.selectMode == Closest { xclient.ConfigGeoSelector(c.latitude, c.longitude) } if c.auth != "" { xclient.Auth(c.auth) } return xclient, err } // Call invokes the named function, waits for it to complete, and returns its error status. // It handles errors base on FailMode. func (c *OneClient) Call(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}) error { c.mu.RLock() xclient := c.xclients[servicePath] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[servicePath] if xclient == nil { xclient, err = c.newXClient(servicePath) c.xclients[servicePath] = xclient } c.mu.Unlock() if err != nil { return err } } return xclient.Call(ctx, serviceMethod, args, reply) } func (c *OneClient) SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) { servicePath := r.ServicePath c.mu.RLock() xclient := c.xclients[servicePath] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[servicePath] if xclient == nil { xclient, err = c.newXClient(servicePath) c.xclients[servicePath] = xclient } c.mu.Unlock() if err != nil { return nil, nil, err } } return xclient.SendRaw(ctx, r) } // Broadcast sends requests to all servers and Success only when all servers return OK. // FailMode and SelectMode are meanless for this method. // Please set timeout to avoid hanging. func (c *OneClient) Broadcast(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}) error { c.mu.RLock() xclient := c.xclients[servicePath] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[servicePath] if xclient == nil { xclient, err = c.newXClient(servicePath) c.xclients[servicePath] = xclient } c.mu.Unlock() if err != nil { return err } } return xclient.Broadcast(ctx, serviceMethod, args, reply) } // Fork sends requests to all servers and Success once one server returns OK. // FailMode and SelectMode are meanless for this method. func (c *OneClient) Fork(ctx context.Context, servicePath string, serviceMethod string, args interface{}, reply interface{}) error { c.mu.RLock() xclient := c.xclients[servicePath] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[servicePath] if xclient == nil { xclient, err = c.newXClient(servicePath) c.xclients[servicePath] = xclient } c.mu.Unlock() if err != nil { return err } } return xclient.Fork(ctx, serviceMethod, args, reply) } func (c *OneClient) SendFile(ctx context.Context, fileName string, rateInBytesPerSecond int64, meta map[string]string) error { c.mu.RLock() xclient := c.xclients[share.SendFileServiceName] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[share.SendFileServiceName] if xclient == nil { xclient, err = c.newXClient(share.SendFileServiceName) c.xclients[share.SendFileServiceName] = xclient } c.mu.Unlock() if err != nil { return err } } return xclient.SendFile(ctx, fileName, rateInBytesPerSecond, meta) } func (c *OneClient) DownloadFile(ctx context.Context, requestFileName string, saveTo io.Writer, meta map[string]string) error { c.mu.RLock() xclient := c.xclients[share.SendFileServiceName] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[share.SendFileServiceName] if xclient == nil { xclient, err = c.newXClient(share.SendFileServiceName) c.xclients[share.SendFileServiceName] = xclient } c.mu.Unlock() if err != nil { return err } } return xclient.DownloadFile(ctx, requestFileName, saveTo, meta) } func (c *OneClient) Stream(ctx context.Context, meta map[string]string) (net.Conn, error) { c.mu.RLock() xclient := c.xclients[share.StreamServiceName] c.mu.RUnlock() if xclient == nil { var err error c.mu.Lock() xclient = c.xclients[share.StreamServiceName] if xclient == nil { xclient, err = c.newXClient(share.StreamServiceName) c.xclients[share.StreamServiceName] = xclient } c.mu.Unlock() if err != nil { return nil, err } } return xclient.Stream(ctx, meta) } // Close closes all xclients and its underlying connections to services. func (c *OneClient) Close() error { var result error c.mu.RLock() for _, v := range c.xclients { err := v.Close() if err != nil { result = multierror.Append(result, err) } } c.mu.RUnlock() return result } ================================================ FILE: client/oneclient_pool.go ================================================ package client import ( "sync/atomic" "github.com/smallnest/rpcx/protocol" ) // OneClientPool is a oneclient pool with fixed size. // It uses roundrobin algorithm to call its xclients. // All oneclients share the same configurations such as ServiceDiscovery and serverMessageChan. type OneClientPool struct { count uint64 index uint64 oneclients []*OneClient auth string Plugins PluginContainer failMode FailMode selectMode SelectMode discovery ServiceDiscovery option Option serverMessageChan chan<- *protocol.Message } // NewOneClientPool creates a fixed size OneClient pool. func NewOneClientPool(count int, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) *OneClientPool { pool := &OneClientPool{ count: uint64(count), oneclients: make([]*OneClient, count), failMode: failMode, selectMode: selectMode, discovery: discovery, option: option, } for i := 0; i < count; i++ { oneclient := NewOneClient(failMode, selectMode, discovery, option) pool.oneclients[i] = oneclient } return pool } // NewBidirectionalOneClientPool creates a BidirectionalOneClient pool with fixed size. func NewBidirectionalOneClientPool(count int, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) *OneClientPool { pool := &OneClientPool{ count: uint64(count), oneclients: make([]*OneClient, count), failMode: failMode, selectMode: selectMode, discovery: discovery, option: option, serverMessageChan: serverMessageChan, } for i := 0; i < count; i++ { oneclient := NewBidirectionalOneClient(failMode, selectMode, discovery, option, serverMessageChan) pool.oneclients[i] = oneclient } return pool } // Auth sets s token for Authentication. func (p *OneClientPool) Auth(auth string) { p.auth = auth for _, v := range p.oneclients { v.Auth(auth) } } // SetPlugins sets client's plugins. func (p *OneClientPool) SetPlugins(plugins PluginContainer) { p.Plugins = plugins for _, v := range p.oneclients { v.SetPlugins(plugins) } } // GetPlugins returns client's plugins. func (p *OneClientPool) GetPlugins() PluginContainer { return p.Plugins } // Get returns a OneClient. // It does not remove this OneClient from its cache so you don't need to put it back. // Don't close this OneClient because maybe other goroutines are using this OneClient. func (p *OneClientPool) Get() *OneClient { i := atomic.AddUint64(&p.index, 1) picked := int(i % p.count) return p.oneclients[picked] } // Close this pool. // Please make sure it won't be used any more. func (p *OneClientPool) Close() { for _, c := range p.oneclients { c.Close() } p.oneclients = nil } ================================================ FILE: client/oneclient_pool_test.go ================================================ package client import ( "testing" ) func TestOneClientPool_SetPlugins(t *testing.T) { // Create a simple discovery pairs := []*KVPair{ {Key: "tcp@127.0.0.1:8972", Value: ""}, } discovery, err := NewMultipleServersDiscovery(pairs) if err != nil { t.Fatalf("failed to create discovery: %v", err) } defer discovery.Close() // Create a pool pool := NewOneClientPool(3, Failtry, RandomSelect, discovery, DefaultOption) defer pool.Close() // Create plugins plugins := NewPluginContainer() tp := &testPlugin{name: "test-plugin"} plugins.Add(tp) // Test SetPlugins pool.SetPlugins(plugins) // Verify plugins are set on pool if pool.GetPlugins() == nil { t.Error("plugins should not be nil after SetPlugins") } if pool.GetPlugins() != plugins { t.Error("pool plugins should be the same as the set plugins") } // Verify plugins are set on all oneclients for i := 0; i < 3; i++ { oneclient := pool.Get() if oneclient.GetPlugins() == nil { t.Errorf("oneclient %d plugins should not be nil", i) } if oneclient.GetPlugins() != plugins { t.Errorf("oneclient %d plugins should be the same as the set plugins", i) } } } func TestOneClientPool_GetPlugins(t *testing.T) { // Create a simple discovery pairs := []*KVPair{ {Key: "tcp@127.0.0.1:8972", Value: ""}, } discovery, err := NewMultipleServersDiscovery(pairs) if err != nil { t.Fatalf("failed to create discovery: %v", err) } defer discovery.Close() // Create a pool pool := NewOneClientPool(2, Failtry, RandomSelect, discovery, DefaultOption) defer pool.Close() // Initially, plugins should be nil if pool.GetPlugins() != nil { t.Error("plugins should be nil initially") } // Create and set plugins plugins := NewPluginContainer() tp := &testPlugin{name: "test-plugin"} plugins.Add(tp) pool.SetPlugins(plugins) // Verify GetPlugins returns the correct plugins retrievedPlugins := pool.GetPlugins() if retrievedPlugins == nil { t.Error("plugins should not be nil after SetPlugins") } if retrievedPlugins != plugins { t.Error("GetPlugins should return the same plugins as SetPlugins") } // Verify plugins contain the test plugin allPlugins := retrievedPlugins.All() if len(allPlugins) != 1 { t.Errorf("expected 1 plugin, got %d", len(allPlugins)) } if p, ok := allPlugins[0].(*testPlugin); !ok || p.name != "test-plugin" { t.Error("plugin should be the test plugin") } } func TestOneClientPool_SetPlugins_Concurrent(t *testing.T) { // Create a simple discovery pairs := []*KVPair{ {Key: "tcp@127.0.0.1:8972", Value: ""}, } discovery, err := NewMultipleServersDiscovery(pairs) if err != nil { t.Fatalf("failed to create discovery: %v", err) } defer discovery.Close() // Create a pool pool := NewOneClientPool(5, Failtry, RandomSelect, discovery, DefaultOption) defer pool.Close() // Test concurrent SetPlugins calls done := make(chan bool, 10) for i := 0; i < 10; i++ { go func(id int) { plugins := NewPluginContainer() tp := &testPlugin{name: "test-plugin"} plugins.Add(tp) pool.SetPlugins(plugins) done <- true }(i) } // Wait for all goroutines to complete for i := 0; i < 10; i++ { <-done } // Verify plugins are set if pool.GetPlugins() == nil { t.Error("plugins should not be nil after concurrent SetPlugins") } } ================================================ FILE: client/peer2peer_discovery.go ================================================ package client // Peer2PeerDiscovery is a peer-to-peer service discovery. // It always returns the static server. type Peer2PeerDiscovery struct { server string metadata string } // NewPeer2PeerDiscovery returns a new Peer2PeerDiscovery. func NewPeer2PeerDiscovery(server, metadata string) (*Peer2PeerDiscovery, error) { return &Peer2PeerDiscovery{server: server, metadata: metadata}, nil } // Clone clones this ServiceDiscovery with new servicePath. func (d *Peer2PeerDiscovery) Clone(servicePath string) (ServiceDiscovery, error) { return d, nil } // SetFilter sets the filer. func (d *Peer2PeerDiscovery) SetFilter(filter ServiceDiscoveryFilter) { } // GetServices returns the static server func (d *Peer2PeerDiscovery) GetServices() []*KVPair { return []*KVPair{&KVPair{Key: d.server, Value: d.metadata}} } // WatchService returns a nil chan. func (d *Peer2PeerDiscovery) WatchService() chan []*KVPair { return nil } func (d *Peer2PeerDiscovery) RemoveWatcher(ch chan []*KVPair) {} func (d *Peer2PeerDiscovery) Close() { } ================================================ FILE: client/ping_utils.go ================================================ package client import ( "context" "net" "strings" "time" ping "github.com/go-ping/ping" ) // weightedICMPSelector selects servers with ping result. type weightedICMPSelector struct { servers []*Weighted wrs *weightedRoundRobinSelector } func newWeightedICMPSelector(servers map[string]string) Selector { ss := createICMPWeighted(servers) wicmps := weightedICMPSelector{ servers: ss, wrs: &weightedRoundRobinSelector{servers: ss}, } wicmps.wrs.servers = ss wicmps.wrs.buildRing() return &wicmps } func (s *weightedICMPSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string { return s.wrs.Select(ctx, servicePath, serviceMethod, args) } func (s *weightedICMPSelector) UpdateServer(servers map[string]string) { ss := createICMPWeighted(servers) s.wrs.servers = ss s.servers = ss s.wrs.buildRing() } func createICMPWeighted(servers map[string]string) []*Weighted { var ss = make([]*Weighted, 0, len(servers)) for k := range servers { w := &Weighted{Server: k, Weight: 1, EffectiveWeight: 1} server := strings.Split(k, "@") host, _, _ := net.SplitHostPort(server[1]) rtt, _ := Ping(host) rtt = CalculateWeight(rtt) w.Weight = rtt w.EffectiveWeight = rtt ss = append(ss, w) } return ss } // Ping gets network traffic by ICMP func Ping(host string) (rtt int, err error) { rtt = 1000 // default and timeout is 1000 ms pinger, err := ping.NewPinger(host) if err != nil { return rtt, err } pinger.Count = 3 pinger.Timeout = 3 * time.Second err = pinger.Run() if err != nil { return rtt, err } stats := pinger.Statistics() // ping failed if len(stats.Rtts) == 0 { return rtt, err } rtt = int(stats.AvgRtt) / 1e6 return rtt, err } // CalculateWeight converts the rtt to weighted by: // 1. weight=191 if t <= 10 // 2. weight=201 -t if 10 < t <=200 // 3. weight=1 if 200 < t < 1000 // 4. weight = 0 if t >= 1000 // // It means servers that ping time t < 10 will be preferred // and servers won't be selected if t > 1000. // It is hard coded based on Ops experience. func CalculateWeight(rtt int) int { switch { case rtt >= 0 && rtt <= 10: return 191 case rtt > 10 && rtt <= 200: return 201 - rtt case rtt > 100 && rtt < 1000: return 1 default: return 0 } } ================================================ FILE: client/plugin.go ================================================ package client import ( "context" "net" "github.com/smallnest/rpcx/protocol" ) // pluginContainer implements PluginContainer interface. type pluginContainer struct { plugins []Plugin } func NewPluginContainer() PluginContainer { return &pluginContainer{} } // Plugin is the client plugin interface. type Plugin interface{} // Add adds a plugin. func (p *pluginContainer) Add(plugin Plugin) { p.plugins = append(p.plugins, plugin) } // Remove removes a plugin by its name. func (p *pluginContainer) Remove(plugin Plugin) { if p.plugins == nil { return } var plugins []Plugin for _, pp := range p.plugins { if pp != plugin { plugins = append(plugins, pp) } } p.plugins = plugins } // All returns all plugins func (p *pluginContainer) All() []Plugin { return p.plugins } // DoPreCall executes before call func (p *pluginContainer) DoPreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PreCallPlugin); ok { err := plugin.PreCall(ctx, servicePath, serviceMethod, args) if err != nil { return err } } } return nil } // DoPostCall executes after call func (p *pluginContainer) DoPostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PostCallPlugin); ok { err = plugin.PostCall(ctx, servicePath, serviceMethod, args, reply, err) if err != nil { return err } } } return nil } // DoConnCreated is called in case of client connection created. func (p *pluginContainer) DoConnCreated(conn net.Conn) (net.Conn, error) { var err error for i := range p.plugins { if plugin, ok := p.plugins[i].(ConnCreatedPlugin); ok { conn, err = plugin.ConnCreated(conn) if err != nil { return conn, err } } } return conn, nil } // DoConnCreateFailed is called in case of client connection create failed. func (p *pluginContainer) DoConnCreateFailed(network, address string) { for i := range p.plugins { if plugin, ok := p.plugins[i].(ConnCreateFailedPlugin); ok { plugin.ConnCreateFailed(network, address) } } } // DoClientConnected is called in case of connected. func (p *pluginContainer) DoClientConnected(conn net.Conn) (net.Conn, error) { var err error for i := range p.plugins { if plugin, ok := p.plugins[i].(ClientConnectedPlugin); ok { conn, err = plugin.ClientConnected(conn) if err != nil { return conn, err } } } return conn, nil } // DoClientConnectionClose is called in case of connection close. func (p *pluginContainer) DoClientConnectionClose(conn net.Conn) error { var err error for i := range p.plugins { if plugin, ok := p.plugins[i].(ClientConnectionClosePlugin); ok { err = plugin.ClientConnectionClose(conn) if err != nil { return err } } } return err } // DoClientBeforeEncode is called when requests are encoded and sent. func (p *pluginContainer) DoClientBeforeEncode(req *protocol.Message) error { var err error for i := range p.plugins { if plugin, ok := p.plugins[i].(ClientBeforeEncodePlugin); ok { err = plugin.ClientBeforeEncode(req) if err != nil { return err } } } return nil } // DoClientAfterDecode is called when requests are decoded and received. func (p *pluginContainer) DoClientAfterDecode(req *protocol.Message) error { var err error for i := range p.plugins { if plugin, ok := p.plugins[i].(ClientAfterDecodePlugin); ok { err = plugin.ClientAfterDecode(req) if err != nil { return err } } } return nil } // DoWrapSelect is called when select a node. func (p *pluginContainer) DoWrapSelect(fn SelectFunc) SelectFunc { rt := fn for i := range p.plugins { if pn, ok := p.plugins[i].(SelectNodePlugin); ok { rt = pn.WrapSelect(rt) } } return rt } type ( // PreCallPlugin is invoked before the client calls a server. PreCallPlugin interface { PreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error } // PostCallPlugin is invoked after the client calls a server. PostCallPlugin interface { PostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) error } // ConnCreatedPlugin is invoked when the client connection has created. ConnCreatedPlugin interface { ConnCreated(net.Conn) (net.Conn, error) } ConnCreateFailedPlugin interface { ConnCreateFailed(network, address string) } // ClientConnectedPlugin is invoked when the client has connected the server. ClientConnectedPlugin interface { ClientConnected(net.Conn) (net.Conn, error) } // ClientConnectionClosePlugin is invoked when the connection is closing. ClientConnectionClosePlugin interface { ClientConnectionClose(net.Conn) error } // ClientBeforeEncodePlugin is invoked when the message is encoded and sent. ClientBeforeEncodePlugin interface { ClientBeforeEncode(*protocol.Message) error } // ClientAfterDecodePlugin is invoked when the message is decoded. ClientAfterDecodePlugin interface { ClientAfterDecode(*protocol.Message) error } // SelectNodePlugin can interrupt selecting of xclient and add customized logics such as skipping some nodes. SelectNodePlugin interface { WrapSelect(SelectFunc) SelectFunc } // PluginContainer represents a plugin container that defines all methods to manage plugins. // And it also defines all extension points. PluginContainer interface { Add(plugin Plugin) Remove(plugin Plugin) All() []Plugin DoConnCreated(net.Conn) (net.Conn, error) DoConnCreateFailed(network, address string) DoClientConnected(net.Conn) (net.Conn, error) DoClientConnectionClose(net.Conn) error DoPreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error DoPostCall(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, err error) error DoClientBeforeEncode(*protocol.Message) error DoClientAfterDecode(*protocol.Message) error DoWrapSelect(SelectFunc) SelectFunc } ) ================================================ FILE: client/selectmode_enumer.go ================================================ // Code generated by "enumer -type=SelectMode"; DO NOT EDIT. package client import ( "fmt" ) const _SelectModeName = "RandomSelectRoundRobinWeightedRoundRobinWeightedICMPConsistentHashClosest" var _SelectModeIndex = [...]uint8{0, 12, 22, 40, 52, 66, 73} func (i SelectMode) String() string { if i < 0 || i >= SelectMode(len(_SelectModeIndex)-1) { return fmt.Sprintf("SelectMode(%d)", i) } return _SelectModeName[_SelectModeIndex[i]:_SelectModeIndex[i+1]] } var _SelectModeValues = []SelectMode{0, 1, 2, 3, 4, 5} var _SelectModeNameToValueMap = map[string]SelectMode{ _SelectModeName[0:12]: 0, _SelectModeName[12:22]: 1, _SelectModeName[22:40]: 2, _SelectModeName[40:52]: 3, _SelectModeName[52:66]: 4, _SelectModeName[66:73]: 5, } // SelectModeString retrieves an enum value from the enum constants string name. // Throws an error if the param is not part of the enum. func SelectModeString(s string) (SelectMode, error) { if val, ok := _SelectModeNameToValueMap[s]; ok { return val, nil } return 0, fmt.Errorf("%s does not belong to SelectMode values", s) } // SelectModeValues returns all values of the enum func SelectModeValues() []SelectMode { return _SelectModeValues } // IsASelectMode returns "true" if the value is listed in the enum definition. "false" otherwise func (i SelectMode) IsASelectMode() bool { for _, v := range _SelectModeValues { if i == v { return true } } return false } ================================================ FILE: client/selector.go ================================================ package client import ( "container/ring" "context" "math" "math/rand" "net/url" "sort" "strconv" "time" "github.com/edwingeng/doublejump" "github.com/valyala/fastrand" ) type SelectFunc func(ctx context.Context, servicePath, serviceMethod string, args interface{}) string // Selector defines selector that selects one service from candidates. type Selector interface { Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string // SelectFunc UpdateServer(servers map[string]string) } func newSelector(selectMode SelectMode, servers map[string]string) Selector { switch selectMode { case RandomSelect: return newRandomSelector(servers) case RoundRobin: return newRoundRobinSelector(servers) case WeightedRoundRobin: return newWeightedRoundRobinSelector(servers) case WeightedICMP: return newWeightedICMPSelector(servers) case ConsistentHash: return newConsistentHashSelector(servers) case SelectByUser: return nil default: return newRandomSelector(servers) } } // randomSelector selects randomly. type randomSelector struct { servers []string } func newRandomSelector(servers map[string]string) Selector { ss := make([]string, 0, len(servers)) for k := range servers { ss = append(ss, k) } return &randomSelector{servers: ss} } func (s *randomSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string { ss := s.servers if len(ss) == 0 { return "" } i := fastrand.Uint32n(uint32(len(ss))) return ss[i] } func (s *randomSelector) UpdateServer(servers map[string]string) { ss := make([]string, 0, len(servers)) for k := range servers { ss = append(ss, k) } s.servers = ss } // roundRobinSelector selects servers with roundrobin. type roundRobinSelector struct { servers []string i int } func newRoundRobinSelector(servers map[string]string) Selector { ss := make([]string, 0, len(servers)) for k := range servers { ss = append(ss, k) } return &roundRobinSelector{servers: ss} } func (s *roundRobinSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string { ss := s.servers if len(ss) == 0 { return "" } i := s.i i = i % len(ss) s.i = i + 1 return ss[i] } func (s *roundRobinSelector) UpdateServer(servers map[string]string) { ss := make([]string, 0, len(servers)) for k := range servers { ss = append(ss, k) } s.servers = ss } // weightedRoundRobinSelector selects servers with weighted. type weightedRoundRobinSelector struct { servers []*Weighted totalWeight int rr *ring.Ring } func newWeightedRoundRobinSelector(servers map[string]string) Selector { ss := createWeighted(servers) s := &weightedRoundRobinSelector{servers: ss} s.buildRing() return s } func (s *weightedRoundRobinSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string { ss := s.servers if len(ss) == 0 { return "" } val := s.rr.Value s.rr = s.rr.Next() return val.(*Weighted).Server } func (s *weightedRoundRobinSelector) UpdateServer(servers map[string]string) { newServer := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector) *s = *newServer } func (s *weightedRoundRobinSelector) buildRing() { s.totalWeight = 0 for _, w := range s.servers { s.totalWeight += w.Weight } s.rr = ring.New(s.totalWeight) for i := 0; i < s.totalWeight; i++ { n := s.next() s.rr.Value = n s.rr = s.rr.Next() } } func (s *weightedRoundRobinSelector) next() *Weighted { if len(s.servers) == 0 { return nil } n := len(s.servers) if n == 0 { return nil } if n == 1 { return s.servers[0] } flag := 0 m := 0 for i := 0; i < n; i++ { s.servers[i].CurrentWeight += s.servers[i].Weight if s.servers[i].CurrentWeight > m { m = s.servers[i].CurrentWeight flag = i } } s.servers[flag].CurrentWeight -= s.totalWeight return s.servers[flag] } func createWeighted(servers map[string]string) []*Weighted { ss := make([]*Weighted, 0, len(servers)) for k, metadata := range servers { w := &Weighted{Server: k, Weight: 1} if v, err := url.ParseQuery(metadata); err == nil { ww := v.Get("weight") if ww != "" { if weight, err := strconv.Atoi(ww); err == nil { w.Weight = weight } } } ss = append(ss, w) } return ss } type geoServer struct { Server string Latitude float64 Longitude float64 } // geoSelector selects servers based on location. type geoSelector struct { servers []*geoServer Latitude float64 Longitude float64 r *rand.Rand } func newGeoSelector(servers map[string]string, latitude, longitude float64) Selector { ss := createGeoServer(servers) r := rand.New(rand.NewSource(time.Now().UnixNano())) return &geoSelector{servers: ss, Latitude: latitude, Longitude: longitude, r: r} } func (s *geoSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string { if len(s.servers) == 0 { return "" } var server []string minNum := math.MaxFloat64 for _, gs := range s.servers { d := getDistanceFrom(s.Latitude, s.Longitude, gs.Latitude, gs.Longitude) if d < minNum { server = []string{gs.Server} minNum = d } else if d == minNum { server = append(server, gs.Server) } } if len(server) == 1 { return server[0] } return server[s.r.Intn(len(server))] } func (s *geoSelector) UpdateServer(servers map[string]string) { ss := createGeoServer(servers) s.servers = ss } func createGeoServer(servers map[string]string) []*geoServer { geoServers := make([]*geoServer, 0, len(servers)) for s, metadata := range servers { if v, err := url.ParseQuery(metadata); err == nil { latStr := v.Get("latitude") lonStr := v.Get("longitude") if latStr == "" || lonStr == "" { continue } lat, err := strconv.ParseFloat(latStr, 64) if err != nil { continue } lon, err := strconv.ParseFloat(lonStr, 64) if err != nil { continue } geoServers = append(geoServers, &geoServer{Server: s, Latitude: lat, Longitude: lon}) } } return geoServers } // consistentHashSelector selects based on JumpConsistentHash. type consistentHashSelector struct { h *doublejump.Hash servers []string } func newConsistentHashSelector(servers map[string]string) Selector { h := doublejump.NewHash() ss := make([]string, 0, len(servers)) for k := range servers { ss = append(ss, k) h.Add(k) } sort.Slice(ss, func(i, j int) bool { return ss[i] < ss[j] }) return &consistentHashSelector{servers: ss, h: h} } func (s *consistentHashSelector) Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string { ss := s.servers if len(ss) == 0 { return "" } key := genKey(servicePath, serviceMethod, args) selected, _ := s.h.Get(key).(string) return selected } func (s *consistentHashSelector) UpdateServer(servers map[string]string) { ss := make([]string, 0, len(servers)) for k := range servers { s.h.Add(k) ss = append(ss, k) } sort.Slice(ss, func(i, j int) bool { return ss[i] < ss[j] }) for _, k := range s.servers { if _, exist := servers[k]; !exist { // remove s.h.Remove(k) } } s.servers = ss } ================================================ FILE: client/selector_test.go ================================================ package client import ( "context" "testing" ) func Test_consistentHashSelector_Select(t *testing.T) { servers := map[string]string{ "tcp@192.168.1.16:9392": "", "tcp@192.168.1.16:9393": "", } s := newConsistentHashSelector(servers).(*consistentHashSelector) key := uint64(9280147620691907957) selected, _ := s.h.Get(key).(string) for i := 0; i < 10000; i++ { selected2, _ := s.h.Get(key).(string) if selected != selected2 { t.Errorf("expected %s but got %s", selected, selected2) } } } func Test_consistentHashSelector_UpdateServer(t *testing.T) { servers := map[string]string{ "tcp@192.168.1.16:9392": "", "tcp@192.168.1.16:9393": "", } s := newConsistentHashSelector(servers).(*consistentHashSelector) if len(s.h.All()) != len(servers) { t.Errorf("NewSelector: expected %d server but got %d", len(servers), len(s.h.All())) } s.UpdateServer(servers) if len(s.h.All()) != len(servers) { t.Errorf("UpdateServer: expected %d server but got %d", len(servers), len(s.h.All())) } } func TestWeightedRoundRobinSelector_Select(t *testing.T) { calc := make(map[string]int) servers := make(map[string]string) servers["ServerA"] = "weight=4" servers["ServerB"] = "weight=2" servers["ServerC"] = "weight=1" weightSelector := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector) ctx := context.Background() for i := 0; i < 7; i++ { s := weightSelector.Select(ctx, "", "", nil) if _, ok := calc[s]; ok { calc[s]++ } else { calc[s] = 1 } } if calc["ServerA"] != 4 { t.Errorf("expected %d but got %d", 4, calc["ServerA"]) } if calc["ServerB"] != 2 { t.Errorf("expected %d but got %d", 2, calc["ServerB"]) } if calc["ServerC"] != 1 { t.Errorf("expected %d but got %d", 1, calc["ServerC"]) } } func TestWeightedRoundRobinSelector_UpdateServer(t *testing.T) { calc := make(map[string]int) servers := make(map[string]string) servers["ServerA"] = "weight=4" servers["ServerB"] = "weight=2" servers["ServerC"] = "weight=1" weightSelector := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector) ctx := context.Background() servers["ServerA"] = "weight=5" weightSelector.UpdateServer(servers) for i := 0; i < 8; i++ { s := weightSelector.Select(ctx, "", "", nil) if _, ok := calc[s]; ok { calc[s]++ } else { calc[s] = 1 } } if calc["ServerA"] != 5 { t.Errorf("expected %d but got %d", 4, calc["ServerA"]) } if calc["ServerB"] != 2 { t.Errorf("expected %d but got %d", 2, calc["ServerB"]) } if calc["ServerC"] != 1 { t.Errorf("expected %d but got %d", 1, calc["ServerC"]) } } func BenchmarkWeightedRoundRobinSelector_Select(b *testing.B) { servers := make(map[string]string) servers["ServerA"] = "weight=4" servers["ServerB"] = "weight=2" servers["ServerC"] = "weight=1" ctx := context.Background() weightSelector := newWeightedRoundRobinSelector(servers).(*weightedRoundRobinSelector) for i := 0; i < b.N; i++ { weightSelector.Select(ctx, "", "", nil) } } // //func TestWeightedICMPSelector(t *testing.T) { // calc := make(map[string]int) // servers := make(map[string]string) // servers["@localhost:3333"] = "" // servers["@www.baidu.com:3334"] = "" // servers["@xxxx.xxxx:333"] = "" // s := newWeightedICMPSelector(servers) // ctx := context.Background() // for i := 0; i < 10; i++ { // host := s.Select(ctx, "", "", nil) // if _, ok := calc[host]; ok { // calc[host]++ // } else { // calc[host] = 0 // } // } // if len(calc) != 2 { // t.Errorf("expected %d but got %d", 2, len(servers)) // } //} //func TestWeightedICMPSelector_UpdateServer(t *testing.T) { // calc := make(map[string]int) // servers := make(map[string]string) // servers["@localhost:3333"] = "" // servers["@www.baidu.com:3334"] = "" // servers["@xxxx.xxxx:333"] = "" // s := newWeightedICMPSelector(servers) // ctx := context.Background() // servers["@www.sina.com:3333"] = "" // s.UpdateServer(servers) // for i := 0; i < 10; i++ { // host := s.Select(ctx, "", "", nil) // if _, ok := calc[host]; ok { // calc[host]++ // } else { // calc[host] = 0 // } // } // if len(calc) != 3 { // t.Errorf("expected %d but got %d", 3, len(servers)) // } //} ================================================ FILE: client/smooth_weighted_round_robin.go ================================================ package client // Weighted is a wrapped server with weight type Weighted struct { Server string Weight int CurrentWeight int EffectiveWeight int } ================================================ FILE: client/xclient.go ================================================ package client import ( "bufio" "context" "errors" "fmt" "io" "net" "net/url" "os" "reflect" "sort" "strings" "sync" "time" "github.com/juju/ratelimit" "golang.org/x/sync/singleflight" ex "github.com/smallnest/rpcx/errors" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" ) const ( FileTransferBufferSize = 1024 ) var ( // ErrXClientShutdown xclient is shutdown. ErrXClientShutdown = errors.New("xClient is shut down") // ErrXClientNoServer selector can't found one server. ErrXClientNoServer = errors.New("can not found any server") // ErrServerUnavailable selected server is unavailable. ErrServerUnavailable = errors.New("selected server is unavailable") ) // Receipt represents the result of the service returned. type Receipt struct { Address string Reply interface{} Error error } // XClient is an interface that used by client with service discovery and service governance. // One XClient is used only for one service. You should create multiple XClient for multiple services. type XClient interface { SetPlugins(plugins PluginContainer) GetPlugins() PluginContainer SetSelector(s Selector) ConfigGeoSelector(latitude, longitude float64) Auth(auth string) Go(ctx context.Context, serviceMethod string, args interface{}, reply interface{}, done chan *Call) (*Call, error) Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error Oneshot(ctx context.Context, serviceMethod string, args interface{}) error Broadcast(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error Fork(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error Inform(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) ([]Receipt, error) SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) SendFile(ctx context.Context, fileName string, rateInBytesPerSecond int64, meta map[string]string) error DownloadFile(ctx context.Context, requestFileName string, saveTo io.Writer, meta map[string]string) error Stream(ctx context.Context, meta map[string]string) (net.Conn, error) Close() error } // SetSelector sets customized selector by users. func (c *xClient) SetSelector(s Selector) { c.mu.RLock() s.UpdateServer(c.servers) c.mu.RUnlock() c.selector = s } // KVPair contains a key and a string. type KVPair struct { Key string Value string } type xClient struct { failMode FailMode selectMode SelectMode cachedClient map[string]RPCClient breakers sync.Map servicePath string option Option mu sync.RWMutex servers map[string]string unstableServers map[string]time.Time // 一些服务器重启,如果和它们建立链接,可能会耗费非常长的时间,这里记录袭来需要临时屏蔽 discovery ServiceDiscovery selector Selector stickyRPCClient RPCClient stickyK string slGroup singleflight.Group isShutdown bool // auth is a string for Authentication, for example, "Bearer mF_9.B5f-4.1JqM" auth string Plugins PluginContainer ch chan []*KVPair serverMessageChan chan<- *protocol.Message } // NewXClient creates a XClient that supports service discovery and service governance. func NewXClient(servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) XClient { client := &xClient{ failMode: failMode, selectMode: selectMode, discovery: discovery, servicePath: servicePath, cachedClient: make(map[string]RPCClient), unstableServers: make(map[string]time.Time), option: option, } pairs := discovery.GetServices() sort.Slice(pairs, func(i, j int) bool { return strings.Compare(pairs[i].Key, pairs[j].Key) <= 0 }) servers := make(map[string]string, len(pairs)) for _, p := range pairs { servers[p.Key] = p.Value } filterByStateAndGroup(client.option.Group, servers) client.servers = servers if selectMode != Closest && selectMode != SelectByUser { client.selector = newSelector(selectMode, servers) } client.Plugins = &pluginContainer{} ch := client.discovery.WatchService() if ch != nil { client.ch = ch go client.watch(ch) } return client } // NewBidirectionalXClient creates a new xclient that can receive notifications from servers. func NewBidirectionalXClient(servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) XClient { client := &xClient{ failMode: failMode, selectMode: selectMode, discovery: discovery, servicePath: servicePath, cachedClient: make(map[string]RPCClient), option: option, unstableServers: make(map[string]time.Time), serverMessageChan: serverMessageChan, } pairs := discovery.GetServices() sort.Slice(pairs, func(i, j int) bool { return strings.Compare(pairs[i].Key, pairs[j].Key) <= 0 }) servers := make(map[string]string, len(pairs)) for _, p := range pairs { servers[p.Key] = p.Value } filterByStateAndGroup(client.option.Group, servers) client.servers = servers if selectMode != Closest && selectMode != SelectByUser { client.selector = newSelector(selectMode, servers) } client.Plugins = &pluginContainer{} ch := client.discovery.WatchService() if ch != nil { client.ch = ch go client.watch(ch) } return client } // SetPlugins sets client's plugins. func (c *xClient) SetPlugins(plugins PluginContainer) { c.Plugins = plugins } func (c *xClient) GetPlugins() PluginContainer { return c.Plugins } // ConfigGeoSelector sets location of client's latitude and longitude, // and use newGeoSelector. func (c *xClient) ConfigGeoSelector(latitude, longitude float64) { c.selector = newGeoSelector(c.servers, latitude, longitude) c.selectMode = Closest } // Auth sets s token for Authentication. func (c *xClient) Auth(auth string) { c.auth = auth } // watch changes of service and update cached clients. func (c *xClient) watch(ch chan []*KVPair) { for pairs := range ch { sort.Slice(pairs, func(i, j int) bool { return strings.Compare(pairs[i].Key, pairs[j].Key) <= 0 }) servers := make(map[string]string, len(pairs)) for _, p := range pairs { servers[p.Key] = p.Value } c.mu.Lock() filterByStateAndGroup(c.option.Group, servers) c.servers = servers if c.selector != nil { c.selector.UpdateServer(servers) } c.mu.Unlock() } } func filterByStateAndGroup(group string, servers map[string]string) { for k, v := range servers { if values, err := url.ParseQuery(v); err == nil { if state := values.Get("state"); state == "inactive" { delete(servers, k) } groups := values["group"] // Directly access the map to get all values associated with "group" as a slice if group != "" { found := false for _, g := range groups { if group == g { found = true break // A matching group is found, stop the search } } if !found { delete(servers, k) // If no matching group is found, delete the corresponding server from the map } } } } } // selects a client from candidates base on c.selectMode func (c *xClient) selectClient(ctx context.Context, servicePath, serviceMethod string, args interface{}) (string, RPCClient, error) { c.mu.Lock() if c.option.Sticky && c.stickyRPCClient != nil { if c.stickyRPCClient.IsClosing() || c.stickyRPCClient.IsShutdown() { c.stickyRPCClient = nil } } if c.option.Sticky && c.stickyRPCClient != nil { c.mu.Unlock() return c.stickyK, c.stickyRPCClient, nil } fn := c.selector.Select if c.Plugins != nil { fn = c.Plugins.DoWrapSelect(fn) } k := fn(ctx, servicePath, serviceMethod, args) c.mu.Unlock() if k == "" { return "", nil, ErrXClientNoServer } client, err := c.getCachedClient(k, servicePath, serviceMethod, args) if c.option.Sticky && client != nil { c.mu.Lock() safeCloseClient(c.stickyRPCClient) c.stickyK = k c.stickyRPCClient = client c.mu.Unlock() } return k, client, err } func safeCloseClient(client RPCClient) { if client == nil { return } defer func() { _ = recover() }() client.Close() } func (c *xClient) getCachedClient(k string, servicePath, serviceMethod string, _ interface{}) (client RPCClient, err error) { var needCallPlugin bool defer func() { if needCallPlugin { _, err = c.Plugins.DoClientConnected(client.GetConn()) } }() if c.isShutdown { return nil, errors.New("this xclient is closed") } // if this client is broken breaker, ok := c.breakers.Load(k) if ok && !breaker.(Breaker).Ready() { return nil, ErrBreakerOpen } c.mu.Lock() defer c.mu.Unlock() client = c.findCachedClient(k, servicePath, serviceMethod) if client != nil { if !client.IsClosing() && !client.IsShutdown() { return client, nil } c.deleteCachedClient(client, k, servicePath, serviceMethod) } client = c.findCachedClient(k, servicePath, serviceMethod) if client == nil || client.IsShutdown() { generatedClient, err, _ := c.slGroup.Do(k, func() (interface{}, error) { return c.generateClient(k, servicePath, serviceMethod) }) if err != nil { c.slGroup.Forget(k) return nil, err } client = generatedClient.(RPCClient) if c.Plugins != nil { needCallPlugin = true } client.RegisterServerMessageChan(c.serverMessageChan) c.setCachedClient(client, k, servicePath, serviceMethod) // forget k only when client is cached c.slGroup.Forget(k) } return client, nil } func (c *xClient) setCachedClient(client RPCClient, k, servicePath, serviceMethod string) { network, _ := splitNetworkAndAddress(k) if builder, ok := getCacheClientBuilder(network); ok { builder.SetCachedClient(client, k, servicePath, serviceMethod) return } c.cachedClient[k] = client } func (c *xClient) findCachedClient(k, servicePath, serviceMethod string) RPCClient { network, _ := splitNetworkAndAddress(k) if builder, ok := getCacheClientBuilder(network); ok { return builder.FindCachedClient(k, servicePath, serviceMethod) } return c.cachedClient[k] } func (c *xClient) deleteCachedClient(client RPCClient, k, servicePath, serviceMethod string) { network, _ := splitNetworkAndAddress(k) if builder, ok := getCacheClientBuilder(network); ok && client != nil { builder.DeleteCachedClient(client, k, servicePath, serviceMethod) client.Close() return } delete(c.cachedClient, k) if client != nil { client.Close() } } func (c *xClient) removeClient(k, servicePath, serviceMethod string, client RPCClient) { c.mu.Lock() if c.option.Sticky { c.stickyK = "" c.stickyRPCClient = nil } cl := c.findCachedClient(k, servicePath, serviceMethod) if cl == client { c.deleteCachedClient(client, k, servicePath, serviceMethod) } c.mu.Unlock() if client != nil { client.UnregisterServerMessageChan() client.Close() } } func (c *xClient) generateClient(k, servicePath, serviceMethod string) (client RPCClient, err error) { network, addr := splitNetworkAndAddress(k) if builder, ok := getCacheClientBuilder(network); ok && builder != nil { return builder.GenerateClient(k, servicePath, serviceMethod) } client = &Client{ option: c.option, Plugins: c.Plugins, } var breaker interface{} if c.option.GenBreaker != nil { breaker, _ = c.breakers.LoadOrStore(k, c.option.GenBreaker()) } err = client.Connect(network, addr) if err != nil { if breaker != nil { breaker.(Breaker).Fail() } return nil, err } return client, err } func (c *xClient) getCachedClientWithoutLock(k, servicePath, serviceMethod string) (RPCClient, bool, error) { var needCallPlugin bool client := c.findCachedClient(k, servicePath, serviceMethod) if client != nil { if !client.IsClosing() && !client.IsShutdown() { return client, needCallPlugin, nil } c.deleteCachedClient(client, k, servicePath, serviceMethod) // double check client = c.findCachedClient(k, servicePath, serviceMethod) } if client == nil || client.IsShutdown() { generatedClient, err, _ := c.slGroup.Do(k, func() (interface{}, error) { return c.generateClient(k, servicePath, serviceMethod) }) if err != nil { c.slGroup.Forget(k) return nil, needCallPlugin, err } client = generatedClient.(RPCClient) if c.Plugins != nil { needCallPlugin = true } client.RegisterServerMessageChan(c.serverMessageChan) c.setCachedClient(client, k, servicePath, serviceMethod) c.slGroup.Forget(k) } return client, needCallPlugin, nil } func splitNetworkAndAddress(server string) (string, string) { ss := strings.SplitN(server, "@", 2) if len(ss) == 1 { return "tcp", server } return ss[0], ss[1] } func setServerTimeout(ctx context.Context) context.Context { if deadline, ok := ctx.Deadline(); ok { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.ServerTimeout] = fmt.Sprintf("%d", time.Until(deadline).Milliseconds()) } return ctx } // Go invokes the function asynchronously. It returns the Call structure representing the invocation. The done channel will signal when the call is complete by returning the same Call object. If done is nil, Go will allocate a new channel. If non-nil, done must be buffered or Go will deliberately crash. // It does not use FailMode. func (c *xClient) Go(ctx context.Context, serviceMethod string, args interface{}, reply interface{}, done chan *Call) (*Call, error) { if c.isShutdown { return nil, ErrXClientShutdown } if c.auth != "" { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.AuthKey] = c.auth } ctx = setServerTimeout(ctx) if share.Trace { log.Debugf("select a client for %s.%s, args: %+v in case of xclient Go", c.servicePath, serviceMethod, args) } _, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args) if err != nil { return nil, err } if share.Trace { log.Debugf("selected a client %s for %s.%s, args: %+v in case of xclient Go", client.RemoteAddr(), c.servicePath, serviceMethod, args) } if done == nil { done = make(chan *Call, 10) } return client.Go(ctx, c.servicePath, serviceMethod, args, reply, done), nil } // Call invokes the named function, waits for it to complete, and returns its error status. // It handles errors base on FailMode. func (c *xClient) Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error { if c.isShutdown { return ErrXClientShutdown } if c.auth != "" { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.AuthKey] = c.auth } ctx = setServerTimeout(ctx) if share.Trace { log.Debugf("select a client for %s.%s, failMode: %v, args: %+v in case of xclient Call", c.servicePath, serviceMethod, c.failMode, args) } var err error k, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args) if err != nil { if c.failMode == Failfast || contextCanceled(err) { return err } } if share.Trace { if client != nil { log.Debugf("selected a client %s for %s.%s, failMode: %v, args: %+v in case of xclient Call", client.RemoteAddr(), c.servicePath, serviceMethod, c.failMode, args) } else { log.Debugf("selected a client %s for %s.%s, failMode: %v, args: %+v in case of xclient Call", "nil", c.servicePath, serviceMethod, c.failMode, args) } } var e error switch c.failMode { case Failtry: retries := c.option.Retries retryInterval := c.option.RetryInterval for retries >= 0 { retries-- if client != nil { err = c.wrapCall(ctx, client, serviceMethod, args, reply) if err == nil { return nil } if contextCanceled(err) { return err } if e, ok := err.(ServiceError); ok && e.IsServiceError() { return err } } if uncoverError(err) { c.removeClient(k, c.servicePath, serviceMethod, client) } client, e = c.getCachedClient(k, c.servicePath, serviceMethod, args) time.Sleep(retryInterval) } if err == nil { err = e } return err case Failover: retries := c.option.Retries retryInterval := c.option.RetryInterval for retries >= 0 { retries-- if client != nil { err = c.wrapCall(ctx, client, serviceMethod, args, reply) if err == nil { return nil } if contextCanceled(err) { return err } if e, ok := err.(ServiceError); ok && e.IsServiceError() { return err } } if uncoverError(err) { c.removeClient(k, c.servicePath, serviceMethod, client) } time.Sleep(retryInterval) // select another server k, client, e = c.selectClient(ctx, c.servicePath, serviceMethod, args) } if err == nil { err = e } return err case Failbackup: ctx, cancelFn := context.WithCancel(ctx) defer cancelFn() call1 := make(chan *Call, 10) call2 := make(chan *Call, 10) var reply1, reply2 interface{} if reply != nil { reply1 = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() reply2 = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() } _, err1 := c.Go(ctx, serviceMethod, args, reply1, call1) t := time.NewTimer(c.option.BackupLatency) select { case <-ctx.Done(): // cancel by context err = ctx.Err() return err case call := <-call1: err = call.Error if err == nil && reply != nil { reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply1).Elem()) } return err case <-t.C: } _, err2 := c.Go(ctx, serviceMethod, args, reply2, call2) if err2 != nil { if uncoverError(err2) { c.removeClient(k, c.servicePath, serviceMethod, client) } err = err1 return err } select { case <-ctx.Done(): // cancel by context err = ctx.Err() case call := <-call1: err = call.Error if err == nil && reply != nil && reply1 != nil { reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply1).Elem()) } case call := <-call2: err = call.Error if err == nil && reply != nil && reply2 != nil { reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(reply2).Elem()) } } return err default: // Failfast err = c.wrapCall(ctx, client, serviceMethod, args, reply) if err != nil { if uncoverError(err) { c.removeClient(k, c.servicePath, serviceMethod, client) } } return err } } // Oneshot invokes the named function, ** DOEST NOT ** wait for it to complete, and returns immediately. func (c *xClient) Oneshot(ctx context.Context, serviceMethod string, args interface{}) error { if c.isShutdown { return ErrXClientShutdown } if c.auth != "" { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.AuthKey] = c.auth } ctx = setServerTimeout(ctx) if share.Trace { log.Debugf("select a client for %s.%s, args: %+v in case of xclient Go", c.servicePath, serviceMethod, args) } _, client, err := c.selectClient(ctx, c.servicePath, serviceMethod, args) if err != nil { return err } if share.Trace { log.Debugf("selected a client %s for %s.%s, args: %+v in case of xclient Go", client.RemoteAddr(), c.servicePath, serviceMethod, args) } client.Go(ctx, c.servicePath, serviceMethod, args, nil, nil) return nil } func uncoverError(err error) bool { if e, ok := err.(ServiceError); ok && e.IsServiceError() { return false } if err == context.DeadlineExceeded { return false } if err == context.Canceled { return false } return true } func contextCanceled(err error) bool { if err == context.DeadlineExceeded { return true } if err == context.Canceled { return true } return false } func (c *xClient) SendRaw(ctx context.Context, r *protocol.Message) (map[string]string, []byte, error) { if c.isShutdown { return nil, nil, ErrXClientShutdown } if c.auth != "" { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.AuthKey] = c.auth } ctx = setServerTimeout(ctx) if share.Trace { log.Debugf("select a client for %s.%s, failMode: %v, args: %+v in case of xclient SendRaw", r.ServicePath, r.ServiceMethod, c.failMode, r.Payload) } var err error k, client, err := c.selectClient(ctx, r.ServicePath, r.ServiceMethod, r.Payload) if err != nil { if c.failMode == Failfast { return nil, nil, err } if contextCanceled(err) { return nil, nil, err } if e, ok := err.(ServiceError); ok && e.IsServiceError() { return nil, nil, err } } if share.Trace { log.Debugf("selected a client %s for %s.%s, failMode: %v, args: %+v in case of xclient Call", client.RemoteAddr(), r.ServicePath, r.ServiceMethod, c.failMode, r.Payload) } var e error switch c.failMode { case Failtry: retries := c.option.Retries for retries >= 0 { retries-- if client != nil { m, payload, err := c.wrapSendRaw(ctx, client, r) if err == nil { return m, payload, nil } if contextCanceled(err) { return nil, nil, err } if _, ok := err.(ServiceError); ok { return nil, nil, err } } if uncoverError(err) { c.removeClient(k, r.ServicePath, r.ServiceMethod, client) } client, e = c.getCachedClient(k, r.ServicePath, r.ServiceMethod, r.Payload) } if err == nil { err = e } return nil, nil, err case Failover: retries := c.option.Retries for retries >= 0 { retries-- if client != nil { m, payload, err := c.wrapSendRaw(ctx, client, r) if err == nil { return m, payload, nil } if contextCanceled(err) { return nil, nil, err } if e, ok := err.(ServiceError); ok && e.IsServiceError() { return nil, nil, err } } if uncoverError(err) { c.removeClient(k, r.ServicePath, r.ServiceMethod, client) } // select another server k, client, e = c.selectClient(ctx, r.ServicePath, r.ServiceMethod, r.Payload) } if err == nil { err = e } return nil, nil, err default: // Failfast m, payload, err := c.wrapSendRaw(ctx, client, r) if err != nil { if uncoverError(err) { c.removeClient(k, r.ServicePath, r.ServiceMethod, client) } } return m, payload, nil } } func (c *xClient) wrapCall(ctx context.Context, client RPCClient, serviceMethod string, args interface{}, reply interface{}) error { if client == nil { return ErrServerUnavailable } if share.Trace { log.Debugf("call a client for %s.%s, args: %+v in case of xclient wrapCall", c.servicePath, serviceMethod, args) } if _, ok := ctx.(*share.Context); !ok { ctx = share.NewContext(ctx) } err := c.Plugins.DoPreCall(ctx, c.servicePath, serviceMethod, args) if err != nil { return err } err = client.Call(ctx, c.servicePath, serviceMethod, args, reply) c.Plugins.DoPostCall(ctx, c.servicePath, serviceMethod, args, reply, err) if share.Trace { log.Debugf("called a client for %s.%s, args: %+v, err: %v in case of xclient wrapCall", c.servicePath, serviceMethod, args, err) } return err } // wrapSendRaw wrap SendRaw to support client plugins func (c *xClient) wrapSendRaw(ctx context.Context, client RPCClient, r *protocol.Message) (map[string]string, []byte, error) { if client == nil { return nil, nil, ErrServerUnavailable } if share.Trace { log.Debugf("call a client for %s.%s, args: %+v in case of xclient wrapSendRaw", c.servicePath, r.ServiceMethod, r.Payload) } ctx = share.NewContext(ctx) err := c.Plugins.DoPreCall(ctx, c.servicePath, r.ServiceMethod, r.Payload) if err != nil { return nil, nil, err } m, payload, err := client.SendRaw(ctx, r) c.Plugins.DoPostCall(ctx, c.servicePath, r.ServiceMethod, r.Payload, nil, err) if share.Trace { log.Debugf("called a client for %s.%s, args: %+v, err: %v in case of xclient wrapSendRaw", c.servicePath, r.ServiceMethod, r.Payload, err) } return m, payload, err } // Broadcast sends requests to all servers and Success only when all servers return OK. // FailMode and SelectMode are meanless for this method. // Please set timeout to avoid hanging. func (c *xClient) Broadcast(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error { if c.isShutdown { return ErrXClientShutdown } if c.auth != "" { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.AuthKey] = c.auth } var replyOnce sync.Once ctx = setServerTimeout(ctx) // add timeout after set server timeout, only prevent client hanging ctx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() callPlugins := make([]RPCClient, 0, len(c.servers)) clients := make(map[string]RPCClient) c.mu.Lock() for k := range c.servers { client, needCallPlugin, err := c.getCachedClientWithoutLock(k, c.servicePath, serviceMethod) if err != nil { continue } clients[k] = client if needCallPlugin { callPlugins = append(callPlugins, client) } } c.mu.Unlock() for i := range callPlugins { if c.Plugins != nil { c.Plugins.DoClientConnected(callPlugins[i].GetConn()) } } if len(clients) == 0 { return ErrXClientNoServer } err := &ex.MultiError{} l := len(clients) done := make(chan bool, l) for k, client := range clients { k := k client := client go func() { var clonedReply interface{} if reply != nil { clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() } e := c.wrapCall(ctx, client, serviceMethod, args, clonedReply) defer func() { done <- (e == nil) }() if e != nil { if uncoverError(e) { c.removeClient(k, c.servicePath, serviceMethod, client) } err.Append(e) } if e == nil && reply != nil && clonedReply != nil { replyOnce.Do(func() { reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem()) }) } }() } check: for { select { case result := <-done: l-- if l == 0 || !result { // all returns or some one returns an error break check } } } select { case <-ctx.Done(): err.Append(errors.New(("timeout"))) default: } return err.ErrorOrNil() } // Fork sends requests to all servers and Success once one server returns OK. // FailMode and SelectMode are meanless for this method. func (c *xClient) Fork(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error { if c.isShutdown { return ErrXClientShutdown } if c.auth != "" { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.AuthKey] = c.auth } ctx = setServerTimeout(ctx) // add timeout after set server timeout, only prevent client hanging ctx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() callPlugins := make([]RPCClient, 0, len(c.servers)) clients := make(map[string]RPCClient) c.mu.Lock() for k := range c.servers { client, needCallPlugin, err := c.getCachedClientWithoutLock(k, c.servicePath, serviceMethod) if err != nil { continue } clients[k] = client if needCallPlugin { callPlugins = append(callPlugins, client) } } c.mu.Unlock() for i := range callPlugins { if c.Plugins != nil { c.Plugins.DoClientConnected(callPlugins[i].GetConn()) } } if len(clients) == 0 { return ErrXClientNoServer } var replyOnce sync.Once err := &ex.MultiError{} l := len(clients) done := make(chan bool, l) for k, client := range clients { k := k client := client go func() { var clonedReply interface{} if reply != nil { clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() } e := c.wrapCall(ctx, client, serviceMethod, args, clonedReply) if e == nil && reply != nil && clonedReply != nil { replyOnce.Do(func() { reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem()) }) } defer func() { done <- (e == nil) }() if e != nil { if uncoverError(e) { c.removeClient(k, c.servicePath, serviceMethod, client) } err.Append(e) } }() } check: for { select { case result := <-done: l-- if result { return nil } if l == 0 { // all returns or some one returns an error break check } } } select { case <-ctx.Done(): err.Append(errors.New(("timeout"))) default: } return err.ErrorOrNil() } // Inform sends requests to all servers and returns all results from services. // FailMode and SelectMode are meanless for this method. // Please set timeout to avoid hanging. func (c *xClient) Inform(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) ([]Receipt, error) { if c.isShutdown { return nil, ErrXClientShutdown } if c.auth != "" { metadata := ctx.Value(share.ReqMetaDataKey) if metadata == nil { metadata = map[string]string{} ctx = context.WithValue(ctx, share.ReqMetaDataKey, metadata) } m := metadata.(map[string]string) m[share.AuthKey] = c.auth } ctx = setServerTimeout(ctx) // add timeout after set server timeout, only prevent client hanging ctx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() callPlugins := make([]RPCClient, 0, len(c.servers)) clients := make(map[string]RPCClient) c.mu.Lock() for k := range c.servers { client, needCallPlugin, err := c.getCachedClientWithoutLock(k, c.servicePath, serviceMethod) if err != nil { continue } clients[k] = client if needCallPlugin { callPlugins = append(callPlugins, client) } } c.mu.Unlock() for i := range callPlugins { if c.Plugins != nil { c.Plugins.DoClientConnected(callPlugins[i].GetConn()) } } if len(clients) == 0 { return nil, ErrXClientNoServer } var receiptsLock sync.Mutex var receipts []Receipt var replyOnce sync.Once err := &ex.MultiError{} l := len(clients) done := make(chan bool, l) for k, client := range clients { k := k client := client go func() { var clonedReply interface{} if reply != nil { clonedReply = reflect.New(reflect.ValueOf(reply).Elem().Type()).Interface() } e := c.wrapCall(ctx, client, serviceMethod, args, clonedReply) defer func() { done <- (e == nil) }() if e != nil { if uncoverError(e) { c.removeClient(k, c.servicePath, serviceMethod, client) } err.Append(e) } if e == nil && reply != nil && clonedReply != nil { replyOnce.Do(func() { reflect.ValueOf(reply).Elem().Set(reflect.ValueOf(clonedReply).Elem()) }) } addr := k ss := strings.SplitN(k, "@", 2) if len(ss) == 2 { addr = ss[1] } receiptsLock.Lock() receipts = append(receipts, Receipt{ Address: addr, Reply: clonedReply, Error: err, }) receiptsLock.Unlock() }() } check: for { select { case <-done: l-- if l == 0 { // all returns or some one returns an error break check } } } select { case <-ctx.Done(): err.Append(errors.New(("timeout"))) default: } return receipts, err.ErrorOrNil() } // SendFile sends a local file to the server. // fileName is the path of local file. // rateInBytesPerSecond can limit bandwidth of sending, 0 means does not limit the bandwidth, unit is bytes / second. func (c *xClient) SendFile(ctx context.Context, fileName string, rateInBytesPerSecond int64, meta map[string]string) error { file, err := os.Open(fileName) if err != nil { return err } defer file.Close() fi, err := os.Stat(fileName) if err != nil { return err } args := share.FileTransferArgs{ FileName: fi.Name(), FileSize: fi.Size(), Meta: meta, } ctx = setServerTimeout(ctx) reply := &share.FileTransferReply{} err = c.Call(ctx, "TransferFile", args, reply) if err != nil { return err } conn, err := net.DialTimeout("tcp", reply.Addr, c.option.ConnectTimeout) if err != nil { return err } defer conn.Close() _, err = conn.Write(reply.Token) if err != nil { return err } var tb *ratelimit.Bucket if rateInBytesPerSecond > 0 { tb = ratelimit.NewBucketWithRate(float64(rateInBytesPerSecond), rateInBytesPerSecond) } sendBuffer := make([]byte, FileTransferBufferSize) loop: for { select { case <-ctx.Done(): err = ctx.Err() break loop default: if tb != nil { tb.Wait(FileTransferBufferSize) } n, err := file.Read(sendBuffer) if err != nil { if err == io.EOF { return nil } else { return err } } if n == 0 { break loop } _, err = conn.Write(sendBuffer[:n]) if err != nil { if err == io.EOF { return nil } else { return err } } } } return nil } func (c *xClient) DownloadFile(ctx context.Context, requestFileName string, saveTo io.Writer, meta map[string]string) error { ctx = setServerTimeout(ctx) args := share.DownloadFileArgs{ FileName: requestFileName, Meta: meta, } reply := &share.FileTransferReply{} err := c.Call(ctx, "DownloadFile", args, reply) if err != nil { return err } conn, err := net.DialTimeout("tcp", reply.Addr, c.option.ConnectTimeout) if err != nil { return err } defer conn.Close() _, err = conn.Write(reply.Token) if err != nil { return err } buf := make([]byte, FileTransferBufferSize) r := bufio.NewReader(conn) loop: for { select { case <-ctx.Done(): err = ctx.Err() break loop default: n, er := r.Read(buf) if n > 0 { _, ew := saveTo.Write(buf[0:n]) if ew != nil { err = ew break loop } } if er != nil { if er != io.EOF { err = er } break loop } } } return err } // Close closes this client and its underlying connections to services. func (c *xClient) Close() error { var errs []error c.mu.Lock() c.isShutdown = true for k, v := range c.cachedClient { e := v.Close() if e != nil { errs = append(errs, e) } delete(c.cachedClient, k) } c.mu.Unlock() go func() { defer func() { recover() }() c.discovery.RemoveWatcher(c.ch) close(c.ch) }() if len(errs) > 0 { return ex.NewMultiError(errs) } return nil } func (c *xClient) Stream(ctx context.Context, meta map[string]string) (net.Conn, error) { args := share.StreamServiceArgs{ Meta: meta, } ctx = setServerTimeout(ctx) reply := &share.StreamServiceReply{} err := c.Call(ctx, "Stream", args, reply) if err != nil { return nil, err } conn, err := net.DialTimeout("tcp", reply.Addr, c.option.ConnectTimeout) if err != nil { return nil, err } _, err = conn.Write(reply.Token) if err != nil { conn.Close() return nil, err } return conn, nil } ================================================ FILE: client/xclient_pool.go ================================================ package client import ( "sync" "sync/atomic" "github.com/smallnest/rpcx/protocol" ) // XClientPool is a xclient pool with fixed size. // It uses roundrobin algorithm to call its xclients. // All xclients share the same configurations such as ServiceDiscovery and serverMessageChan. type XClientPool struct { count uint64 index uint64 xclients []XClient mu sync.RWMutex servicePath string failMode FailMode selectMode SelectMode discovery ServiceDiscovery option Option auth string Plugins PluginContainer serverMessageChan chan<- *protocol.Message } // NewXClientPool creates a fixed size XClient pool. func NewXClientPool(count int, servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) *XClientPool { pool := &XClientPool{ count: uint64(count), xclients: make([]XClient, count), servicePath: servicePath, failMode: failMode, selectMode: selectMode, discovery: discovery, option: option, } for i := 0; i < count; i++ { xclient := NewXClient(servicePath, failMode, selectMode, discovery, option) pool.xclients[i] = xclient } return pool } // NewBidirectionalXClientPool creates a BidirectionalXClient pool with fixed size. func NewBidirectionalXClientPool(count int, servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option, serverMessageChan chan<- *protocol.Message) *XClientPool { pool := &XClientPool{ count: uint64(count), xclients: make([]XClient, count), servicePath: servicePath, failMode: failMode, selectMode: selectMode, discovery: discovery, option: option, serverMessageChan: serverMessageChan, } for i := 0; i < count; i++ { xclient := NewBidirectionalXClient(servicePath, failMode, selectMode, discovery, option, serverMessageChan) pool.xclients[i] = xclient } return pool } // Auth sets s token for Authentication. func (c *XClientPool) Auth(auth string) { c.auth = auth c.mu.RLock() for _, v := range c.xclients { v.Auth(auth) } c.mu.RUnlock() } // SetPlugins sets client's plugins. func (p *XClientPool) SetPlugins(plugins PluginContainer) { p.Plugins = plugins p.mu.RLock() for _, v := range p.xclients { v.SetPlugins(plugins) } p.mu.RUnlock() } // GetPlugins returns client's plugins. func (p *XClientPool) GetPlugins() PluginContainer { return p.Plugins } // Get returns a xclient. // It does not remove this xclient from its cache so you don't need to put it back. // Don't close this xclient because maybe other goroutines are using this xclient. func (p *XClientPool) Get() XClient { i := atomic.AddUint64(&p.index, 1) picked := int(i % p.count) return p.xclients[picked] } // Close this pool. // Please make sure it won't be used any more. func (p *XClientPool) Close() { for _, c := range p.xclients { c.Close() } p.xclients = nil } ================================================ FILE: client/xclient_pool_test.go ================================================ package client import ( "testing" ) // testPlugin is a simple test plugin implementation type testPlugin struct { name string } func TestXClientPool_SetPlugins(t *testing.T) { // Create a simple discovery pairs := []*KVPair{ {Key: "tcp@127.0.0.1:8972", Value: ""}, } discovery, err := NewMultipleServersDiscovery(pairs) if err != nil { t.Fatalf("failed to create discovery: %v", err) } defer discovery.Close() // Create a pool pool := NewXClientPool(3, "Arith", Failtry, RandomSelect, discovery, DefaultOption) defer pool.Close() // Create plugins plugins := NewPluginContainer() tp := &testPlugin{name: "test-plugin"} plugins.Add(tp) // Test SetPlugins pool.SetPlugins(plugins) // Verify plugins are set on pool if pool.GetPlugins() == nil { t.Error("plugins should not be nil after SetPlugins") } if pool.GetPlugins() != plugins { t.Error("pool plugins should be the same as the set plugins") } // Verify plugins are set on all xclients for i := 0; i < 3; i++ { xclient := pool.Get() if xclient.GetPlugins() == nil { t.Errorf("xclient %d plugins should not be nil", i) } if xclient.GetPlugins() != plugins { t.Errorf("xclient %d plugins should be the same as the set plugins", i) } } } func TestXClientPool_GetPlugins(t *testing.T) { // Create a simple discovery pairs := []*KVPair{ {Key: "tcp@127.0.0.1:8972", Value: ""}, } discovery, err := NewMultipleServersDiscovery(pairs) if err != nil { t.Fatalf("failed to create discovery: %v", err) } defer discovery.Close() // Create a pool pool := NewXClientPool(2, "Arith", Failtry, RandomSelect, discovery, DefaultOption) defer pool.Close() // Initially, plugins should be nil if pool.GetPlugins() != nil { t.Error("plugins should be nil initially") } // Create and set plugins plugins := NewPluginContainer() tp := &testPlugin{name: "test-plugin"} plugins.Add(tp) pool.SetPlugins(plugins) // Verify GetPlugins returns the correct plugins retrievedPlugins := pool.GetPlugins() if retrievedPlugins == nil { t.Error("plugins should not be nil after SetPlugins") } if retrievedPlugins != plugins { t.Error("GetPlugins should return the same plugins as SetPlugins") } // Verify plugins contain the test plugin allPlugins := retrievedPlugins.All() if len(allPlugins) != 1 { t.Errorf("expected 1 plugin, got %d", len(allPlugins)) } if p, ok := allPlugins[0].(*testPlugin); !ok || p.name != "test-plugin" { t.Error("plugin should be the test plugin") } } func TestXClientPool_SetPlugins_Concurrent(t *testing.T) { // Create a simple discovery pairs := []*KVPair{ {Key: "tcp@127.0.0.1:8972", Value: ""}, } discovery, err := NewMultipleServersDiscovery(pairs) if err != nil { t.Fatalf("failed to create discovery: %v", err) } defer discovery.Close() // Create a pool pool := NewXClientPool(5, "Arith", Failtry, RandomSelect, discovery, DefaultOption) defer pool.Close() // Test concurrent SetPlugins calls done := make(chan bool, 10) for i := 0; i < 10; i++ { go func(id int) { plugins := NewPluginContainer() tp := &testPlugin{name: "test-plugin"} plugins.Add(tp) pool.SetPlugins(plugins) done <- true }(i) } // Wait for all goroutines to complete for i := 0; i < 10; i++ { <-done } // Verify plugins are set if pool.GetPlugins() == nil { t.Error("plugins should not be nil after concurrent SetPlugins") } } ================================================ FILE: client/xclient_test.go ================================================ package client import ( "context" "errors" "testing" "time" "fmt" testutils "github.com/smallnest/rpcx/_testutils" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/server" "github.com/smallnest/rpcx/share" ) func TestXClient_Thrift(t *testing.T) { s := server.NewServer() s.RegisterName("Arith", new(Arith), "") go s.Serve("tcp", "127.0.0.1:0") defer s.Close() time.Sleep(500 * time.Millisecond) addr := s.Address().String() opt := Option{ Retries: 1, RPCPath: share.DefaultRPCPath, ConnectTimeout: 10 * time.Second, SerializeType: protocol.Thrift, CompressType: protocol.None, BackupLatency: 10 * time.Millisecond, } d, err := NewPeer2PeerDiscovery("tcp@"+addr, "desc=a test service") if err != nil { t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err) } xclient := NewXClient("Arith", Failtry, RandomSelect, d, opt) defer xclient.Close() args := testutils.ThriftArgs_{} args.A = 200 args.B = 100 reply := testutils.ThriftReply{} err = xclient.Call(context.Background(), "ThriftMul", &args, &reply) if err != nil { t.Fatalf("failed to call: %v", err) } fmt.Println(reply.C) if reply.C != 20000 { t.Fatalf("expect 20000 but got %d", reply.C) } } func TestXClient_IT(t *testing.T) { s := server.NewServer() s.RegisterName("Arith", new(Arith), "") go s.Serve("tcp", "127.0.0.1:0") defer s.Close() time.Sleep(500 * time.Millisecond) addr := s.Address().String() d, err := NewPeer2PeerDiscovery("tcp@"+addr, "desc=a test service") if err != nil { t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err) } xclient := NewXClient("Arith", Failtry, RandomSelect, d, DefaultOption) defer xclient.Close() args := &Args{ A: 10, B: 20, } reply := &Reply{} err = xclient.Call(context.Background(), "Mul", args, reply) if err != nil { t.Fatalf("failed to call: %v", err) } if reply.C != 200 { t.Fatalf("expect 200 but got %d", reply.C) } } func TestXClient_filterByStateAndGroup(t *testing.T) { servers := map[string]string{"a": "", "b": "state=inactive&ops=10", "c": "ops=20", "d": "group=test1&group=test&ops=20"} filterByStateAndGroup("test", servers) if _, ok := servers["b"]; ok { t.Error("has not remove inactive node") } if _, ok := servers["a"]; ok { t.Error("has not remove inactive node") } if _, ok := servers["c"]; ok { t.Error("has not remove inactive node") } if _, ok := servers["d"]; !ok { t.Error("node must be removed") } filterByStateAndGroup("test1", servers) if _, ok := servers["d"]; !ok { t.Error("node must be removed") } } func TestUncoverError(t *testing.T) { var e error = strErr("error") if uncoverError(e) { t.Fatalf("expect false but get true") } if uncoverError(context.DeadlineExceeded) { t.Fatalf("expect false but get true") } if uncoverError(context.Canceled) { t.Fatalf("expect false but get true") } e = errors.New("error") if !uncoverError(e) { t.Fatalf("expect true but get false") } } ================================================ FILE: clientplugin/req_rate_limiting_redis.go ================================================ package clientplugin import ( "context" "time" "github.com/go-redis/redis_rate/v10" "github.com/redis/go-redis/v9" "github.com/smallnest/rpcx/client" "github.com/smallnest/rpcx/server" ) var _ client.PreCallPlugin = (*RedisRateLimitingPlugin)(nil) // RedisRateLimitingPlugin can limit requests per unit time type RedisRateLimitingPlugin struct { addrs []string limiter redis_rate.Limiter limit redis_rate.Limit } // NewRedisRateLimitingPlugin creates a new RateLimitingPlugin func NewRedisRateLimitingPlugin(addrs []string, rate int, burst int, period time.Duration) *RedisRateLimitingPlugin { limit := redis_rate.Limit{ Rate: rate, Burst: burst, Period: period, } rdb := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: addrs, }) limiter := redis_rate.NewLimiter(rdb) return &RedisRateLimitingPlugin{ addrs: addrs, limiter: *limiter, limit: limit, } } // PreCall can limit request processing. func (plugin *RedisRateLimitingPlugin) PreCall(ctx context.Context, servicePath, serviceMethod string, args interface{}) error { res, err := plugin.limiter.Allow(ctx, servicePath+"/"+serviceMethod, plugin.limit) if err != nil { return err } if res.Allowed > 0 { return nil } return server.ErrReqReachLimit } ================================================ FILE: codec/codec.go ================================================ package codec import ( "bytes" "context" "encoding/json" "errors" "fmt" "reflect" pb "google.golang.org/protobuf/proto" "github.com/apache/thrift/lib/go/thrift" "github.com/gogo/protobuf/proto" "github.com/tinylib/msgp/msgp" "github.com/vmihailenco/msgpack/v5" ) // Codec defines the interface that decode/encode payload. type Codec interface { Encode(i interface{}) ([]byte, error) Decode(data []byte, i interface{}) error } // ByteCodec uses raw slice pf bytes and don't encode/decode. type ByteCodec struct{} // Encode returns raw slice of bytes. func (c ByteCodec) Encode(i interface{}) ([]byte, error) { if data, ok := i.([]byte); ok { return data, nil } if data, ok := i.(*[]byte); ok { return *data, nil } return nil, fmt.Errorf("%T is not a []byte", i) } // Decode returns raw slice of bytes. func (c ByteCodec) Decode(data []byte, i interface{}) error { reflect.Indirect(reflect.ValueOf(i)).SetBytes(data) return nil } // JSONCodec uses json marshaler and unmarshaler. type JSONCodec struct{} // Encode encodes an object into slice of bytes. func (c JSONCodec) Encode(i interface{}) ([]byte, error) { return json.Marshal(i) } // Decode decodes an object from slice of bytes. func (c JSONCodec) Decode(data []byte, i interface{}) error { d := json.NewDecoder(bytes.NewBuffer(data)) d.UseNumber() return d.Decode(i) } // PBCodec uses protobuf marshaler and unmarshaler. type PBCodec struct{} // Encode encodes an object into slice of bytes. func (c PBCodec) Encode(i interface{}) ([]byte, error) { if m, ok := i.(proto.Marshaler); ok { return m.Marshal() } if m, ok := i.(pb.Message); ok { return pb.Marshal(m) } return nil, fmt.Errorf("%T is not a proto.Marshaler or pb.Message", i) } // Decode decodes an object from slice of bytes. func (c PBCodec) Decode(data []byte, i interface{}) error { if m, ok := i.(proto.Unmarshaler); ok { return m.Unmarshal(data) } if m, ok := i.(pb.Message); ok { return pb.Unmarshal(data, m) } return fmt.Errorf("%T is not a proto.Unmarshaler or pb.Message", i) } // MsgpackCodec uses messagepack marshaler and unmarshaler. type MsgpackCodec struct{} // Encode encodes an object into slice of bytes. func (c MsgpackCodec) Encode(i interface{}) ([]byte, error) { if m, ok := i.(msgp.Marshaler); ok { return m.MarshalMsg(nil) } var buf bytes.Buffer enc := msgpack.NewEncoder(&buf) // enc.UseJSONTag(true) err := enc.Encode(i) return buf.Bytes(), err } // Decode decodes an object from slice of bytes. func (c MsgpackCodec) Decode(data []byte, i interface{}) error { if m, ok := i.(msgp.Unmarshaler); ok { _, err := m.UnmarshalMsg(data) return err } dec := msgpack.NewDecoder(bytes.NewReader(data)) // dec.UseJSONTag(true) err := dec.Decode(i) return err } type ThriftCodec struct{} func (c ThriftCodec) Encode(i interface{}) ([]byte, error) { b := thrift.NewTMemoryBufferLen(1024) p := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{}). GetProtocol(b) t := &thrift.TSerializer{ Transport: b, Protocol: p, } t.Transport.Close() if msg, ok := i.(thrift.TStruct); ok { return t.Write(context.Background(), msg) } return nil, errors.New("type assertion failed") } func (c ThriftCodec) Decode(data []byte, i interface{}) error { t := thrift.NewTMemoryBufferLen(1024) p := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{}). GetProtocol(t) d := &thrift.TDeserializer{ Transport: t, Protocol: p, } d.Transport.Close() return d.Read(context.Background(), i.(thrift.TStruct), data) } ================================================ FILE: codec/codec_test.go ================================================ package codec import ( "testing" "github.com/smallnest/rpcx/codec/testdata" ) type ColorGroup struct { Id int `json:"id" xml:"id,attr" msg:"id"` Name string `json:"name" xml:"name" msg:"name"` Colors []string `json:"colors" xml:"colors" msg:"colors"` } var group = ColorGroup{ Id: 1, Name: "Reds", Colors: []string{"Crimson", "Red", "Ruby", "Maroon"}, } func BenchmarkJSONCodec_Encode(b *testing.B) { var raw = make([]byte, 0, 1024) serializer := JSONCodec{} b.ResetTimer() for i := 0; i < b.N; i++ { raw, _ = serializer.Encode(group) } b.ReportMetric(float64(len(raw)), "bytes") } func BenchmarkPBCodec_Encode(b *testing.B) { var raw = make([]byte, 0, 1024) serializer := PBCodec{} group := testdata.ProtoColorGroup{ Id: 1, Name: "Reds", Colors: []string{"Crimson", "Red", "Ruby", "Maroon"}, } b.ResetTimer() for i := 0; i < b.N; i++ { raw, _ = serializer.Encode(&group) } b.ReportMetric(float64(len(raw)), "bytes") } func BenchmarkMsgpackCodec_Encode(b *testing.B) { var raw = make([]byte, 0, 1024) serializer := MsgpackCodec{} b.ResetTimer() for i := 0; i < b.N; i++ { raw, _ = serializer.Encode(group) } b.ReportMetric(float64(len(raw)), "bytes") } func BenchmarkThriftCodec_Encode(b *testing.B) { var bb = make([]byte, 0, 1024) serializer := ThriftCodec{} thriftColorGroup := testdata.ThriftColorGroup{ ID: 1, Name: "Reds", Colors: []string{"Crimson", "Red", "Ruby", "Maroon"}, } b.ResetTimer() for i := 0; i < b.N; i++ { bb, _ = serializer.Encode(&thriftColorGroup) } b.ReportMetric(float64(len(bb)), "bytes") } func BenchmarkJSONCodec_Decode(b *testing.B) { serializer := JSONCodec{} bytes, _ := serializer.Encode(group) result := ColorGroup{} b.ResetTimer() for i := 0; i < b.N; i++ { _ = serializer.Decode(bytes, &result) } } func BenchmarkPBCodec_Decode(b *testing.B) { serializer := PBCodec{} bytes, _ := serializer.Encode(group) result := ColorGroup{} b.ResetTimer() for i := 0; i < b.N; i++ { _ = serializer.Decode(bytes, &result) } } func BenchmarkMsgpackCodec_Decode(b *testing.B) { serializer := MsgpackCodec{} bytes, _ := serializer.Encode(group) result := ColorGroup{} b.ResetTimer() for i := 0; i < b.N; i++ { _ = serializer.Decode(bytes, &result) } } func BenchmarkThriftCodec_Decode(b *testing.B) { serializer := ThriftCodec{} thriftColorGroup := testdata.ThriftColorGroup{ ID: 1, Name: "Reds", Colors: []string{"Crimson", "Red", "Ruby", "Maroon"}, } bytes, _ := serializer.Encode(&thriftColorGroup) result := testdata.ThriftColorGroup{} b.ResetTimer() for i := 0; i < b.N; i++ { _ = serializer.Decode(bytes, &result) } } ================================================ FILE: codec/testdata/GoUnusedProtection__.go ================================================ // Code generated by Thrift Compiler (0.14.0). DO NOT EDIT. package testdata var GoUnusedProtection__ int; ================================================ FILE: codec/testdata/gen.sh ================================================ # generate .go files from IDL protoc -I. --go_out=. --go_opt=module="testdata" ./protobuf.proto thrift -r -out ../ --gen go ./thrift_colorgroup.thrift # # run benchmarks # go test -bench=. -run=none # # clean files # rm -rf ./testdata/*.go ================================================ FILE: codec/testdata/protobuf.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.33.0 // protoc v5.26.0 // source: protobuf.proto package testdata import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type ProtoColorGroup struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Colors []string `protobuf:"bytes,3,rep,name=colors,proto3" json:"colors,omitempty"` } func (x *ProtoColorGroup) Reset() { *x = ProtoColorGroup{} if protoimpl.UnsafeEnabled { mi := &file_protobuf_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ProtoColorGroup) String() string { return protoimpl.X.MessageStringOf(x) } func (*ProtoColorGroup) ProtoMessage() {} func (x *ProtoColorGroup) ProtoReflect() protoreflect.Message { mi := &file_protobuf_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ProtoColorGroup.ProtoReflect.Descriptor instead. func (*ProtoColorGroup) Descriptor() ([]byte, []int) { return file_protobuf_proto_rawDescGZIP(), []int{0} } func (x *ProtoColorGroup) GetId() int32 { if x != nil { return x.Id } return 0 } func (x *ProtoColorGroup) GetName() string { if x != nil { return x.Name } return "" } func (x *ProtoColorGroup) GetColors() []string { if x != nil { return x.Colors } return nil } var File_protobuf_proto protoreflect.FileDescriptor var file_protobuf_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x22, 0x4d, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_protobuf_proto_rawDescOnce sync.Once file_protobuf_proto_rawDescData = file_protobuf_proto_rawDesc ) func file_protobuf_proto_rawDescGZIP() []byte { file_protobuf_proto_rawDescOnce.Do(func() { file_protobuf_proto_rawDescData = protoimpl.X.CompressGZIP(file_protobuf_proto_rawDescData) }) return file_protobuf_proto_rawDescData } var file_protobuf_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_protobuf_proto_goTypes = []interface{}{ (*ProtoColorGroup)(nil), // 0: testdata.ProtoColorGroup } var file_protobuf_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_protobuf_proto_init() } func file_protobuf_proto_init() { if File_protobuf_proto != nil { return } if !protoimpl.UnsafeEnabled { file_protobuf_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ProtoColorGroup); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_protobuf_proto_rawDesc, NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_protobuf_proto_goTypes, DependencyIndexes: file_protobuf_proto_depIdxs, MessageInfos: file_protobuf_proto_msgTypes, }.Build() File_protobuf_proto = out.File file_protobuf_proto_rawDesc = nil file_protobuf_proto_goTypes = nil file_protobuf_proto_depIdxs = nil } ================================================ FILE: codec/testdata/protobuf.proto ================================================ syntax = "proto3"; package testdata; option go_package = "./testdata"; message ProtoColorGroup { int32 id = 1; string name = 2; repeated string colors = 3; } ================================================ FILE: codec/testdata/thrift_colorgroup-consts.go ================================================ // Code generated by Thrift Compiler (0.14.0). DO NOT EDIT. package testdata import( "bytes" "context" "fmt" "time" "github.com/apache/thrift/lib/go/thrift" ) // (needed to ensure safety because of naive import list construction.) var _ = thrift.ZERO var _ = fmt.Printf var _ = context.Background var _ = time.Now var _ = bytes.Equal func init() { } ================================================ FILE: codec/testdata/thrift_colorgroup.go ================================================ // Code generated by Thrift Compiler (0.14.0). DO NOT EDIT. package testdata import( "bytes" "context" "fmt" "time" "github.com/apache/thrift/lib/go/thrift" ) // (needed to ensure safety because of naive import list construction.) var _ = thrift.ZERO var _ = fmt.Printf var _ = context.Background var _ = time.Now var _ = bytes.Equal // Attributes: // - ID // - Name // - Colors type ThriftColorGroup struct { ID int32 `thrift:"id,1" db:"id" json:"id"` Name string `thrift:"name,2" db:"name" json:"name"` Colors []string `thrift:"colors,3" db:"colors" json:"colors"` } func NewThriftColorGroup() *ThriftColorGroup { return &ThriftColorGroup{} } func (p *ThriftColorGroup) GetID() int32 { return p.ID } func (p *ThriftColorGroup) GetName() string { return p.Name } func (p *ThriftColorGroup) GetColors() []string { return p.Colors } func (p *ThriftColorGroup) Read(ctx context.Context, iprot thrift.TProtocol) error { if _, err := iprot.ReadStructBegin(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) } for { _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx) if err != nil { return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) } if fieldTypeId == thrift.STOP { break; } switch fieldId { case 1: if fieldTypeId == thrift.I32 { if err := p.ReadField1(ctx, iprot); err != nil { return err } } else { if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } case 2: if fieldTypeId == thrift.STRING { if err := p.ReadField2(ctx, iprot); err != nil { return err } } else { if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } case 3: if fieldTypeId == thrift.LIST { if err := p.ReadField3(ctx, iprot); err != nil { return err } } else { if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } default: if err := iprot.Skip(ctx, fieldTypeId); err != nil { return err } } if err := iprot.ReadFieldEnd(ctx); err != nil { return err } } if err := iprot.ReadStructEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) } return nil } func (p *ThriftColorGroup) ReadField1(ctx context.Context, iprot thrift.TProtocol) error { if v, err := iprot.ReadI32(ctx); err != nil { return thrift.PrependError("error reading field 1: ", err) } else { p.ID = v } return nil } func (p *ThriftColorGroup) ReadField2(ctx context.Context, iprot thrift.TProtocol) error { if v, err := iprot.ReadString(ctx); err != nil { return thrift.PrependError("error reading field 2: ", err) } else { p.Name = v } return nil } func (p *ThriftColorGroup) ReadField3(ctx context.Context, iprot thrift.TProtocol) error { _, size, err := iprot.ReadListBegin(ctx) if err != nil { return thrift.PrependError("error reading list begin: ", err) } tSlice := make([]string, 0, size) p.Colors = tSlice for i := 0; i < size; i ++ { var _elem0 string if v, err := iprot.ReadString(ctx); err != nil { return thrift.PrependError("error reading field 0: ", err) } else { _elem0 = v } p.Colors = append(p.Colors, _elem0) } if err := iprot.ReadListEnd(ctx); err != nil { return thrift.PrependError("error reading list end: ", err) } return nil } func (p *ThriftColorGroup) Write(ctx context.Context, oprot thrift.TProtocol) error { if err := oprot.WriteStructBegin(ctx, "ThriftColorGroup"); err != nil { return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) } if p != nil { if err := p.writeField1(ctx, oprot); err != nil { return err } if err := p.writeField2(ctx, oprot); err != nil { return err } if err := p.writeField3(ctx, oprot); err != nil { return err } } if err := oprot.WriteFieldStop(ctx); err != nil { return thrift.PrependError("write field stop error: ", err) } if err := oprot.WriteStructEnd(ctx); err != nil { return thrift.PrependError("write struct stop error: ", err) } return nil } func (p *ThriftColorGroup) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) { if err := oprot.WriteFieldBegin(ctx, "id", thrift.I32, 1); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:id: ", p), err) } if err := oprot.WriteI32(ctx, int32(p.ID)); err != nil { return thrift.PrependError(fmt.Sprintf("%T.id (1) field write error: ", p), err) } if err := oprot.WriteFieldEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field end error 1:id: ", p), err) } return err } func (p *ThriftColorGroup) writeField2(ctx context.Context, oprot thrift.TProtocol) (err error) { if err := oprot.WriteFieldBegin(ctx, "name", thrift.STRING, 2); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field begin error 2:name: ", p), err) } if err := oprot.WriteString(ctx, string(p.Name)); err != nil { return thrift.PrependError(fmt.Sprintf("%T.name (2) field write error: ", p), err) } if err := oprot.WriteFieldEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field end error 2:name: ", p), err) } return err } func (p *ThriftColorGroup) writeField3(ctx context.Context, oprot thrift.TProtocol) (err error) { if err := oprot.WriteFieldBegin(ctx, "colors", thrift.LIST, 3); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field begin error 3:colors: ", p), err) } if err := oprot.WriteListBegin(ctx, thrift.STRING, len(p.Colors)); err != nil { return thrift.PrependError("error writing list begin: ", err) } for _, v := range p.Colors { if err := oprot.WriteString(ctx, string(v)); err != nil { return thrift.PrependError(fmt.Sprintf("%T. (0) field write error: ", p), err) } } if err := oprot.WriteListEnd(ctx); err != nil { return thrift.PrependError("error writing list end: ", err) } if err := oprot.WriteFieldEnd(ctx); err != nil { return thrift.PrependError(fmt.Sprintf("%T write field end error 3:colors: ", p), err) } return err } func (p *ThriftColorGroup) Equals(other *ThriftColorGroup) bool { if p == other { return true } else if p == nil || other == nil { return false } if p.ID != other.ID { return false } if p.Name != other.Name { return false } if len(p.Colors) != len(other.Colors) { return false } for i, _tgt := range p.Colors { _src1 := other.Colors[i] if _tgt != _src1 { return false } } return true } func (p *ThriftColorGroup) String() string { if p == nil { return "" } return fmt.Sprintf("ThriftColorGroup(%+v)", *p) } ================================================ FILE: codec/testdata/thrift_colorgroup.thrift ================================================ namespace go testdata struct ThriftColorGroup { 1: i32 id = 0, 2: string name, 3: list colors, } ================================================ FILE: errors/error.go ================================================ package errors import ( "fmt" "sync" ) // MultiError holds multiple errors type MultiError struct { Errors []error mu sync.Mutex } // Error returns the message of the actual error func (e *MultiError) Error() string { return fmt.Sprintf("%v", e.Errors) } func (e *MultiError) Append(err error) { e.mu.Lock() defer e.mu.Unlock() e.Errors = append(e.Errors, err) } func (e *MultiError) ErrorOrNil() error { if e == nil || len(e.Errors) == 0 { return nil } return e } // NewMultiError creates and returns an Error with error splice func NewMultiError(errors []error) *MultiError { return &MultiError{Errors: errors} } ================================================ FILE: errors/error_test.go ================================================ package errors import ( "errors" "fmt" "testing" "github.com/stretchr/testify/assert" ) func TestNewMultiError(t *testing.T) { var errorSet []error errorSet = append(errorSet, errors.New("invalid")) errorSet = append(errorSet, errors.New("fatal")) multiError := NewMultiError(errorSet) assert.Equal(t, fmt.Sprintf("%v", errorSet), multiError.Error(), "Test NewMultiError()") } func TestMultiError_Append(t *testing.T) { multiErrors := MultiError{} multiErrors.Errors = append(multiErrors.Errors, errors.New("invalid")) multiErrors.Errors = append(multiErrors.Errors, errors.New("fatal")) assert.Equal(t, 2, len(multiErrors.Errors), "Test Append()") } func TestMultiError_Error(t *testing.T) { multiErrors := MultiError{} multiErrors.Errors = append(multiErrors.Errors, errors.New("invalid")) multiErrors.Errors = append(multiErrors.Errors, errors.New("fatal")) assert.Equal(t, "[invalid fatal]", multiErrors.Error(), "Test Error()") } func TestMultiError_ErrorOrNil(t *testing.T) { multiErrors := MultiError{} assert.Equal(t, nil, multiErrors.ErrorOrNil(), "Test ErrorOrNil() nil") multiErrors.Errors = append(multiErrors.Errors, errors.New("invalid")) multiErrors.Errors = append(multiErrors.Errors, errors.New("fatal")) assert.Equal(t, &multiErrors, multiErrors.ErrorOrNil(), "Test ErrorOrNil() error") } ================================================ FILE: go.mod ================================================ module github.com/smallnest/rpcx go 1.24 require ( github.com/akutz/memconn v0.1.0 github.com/alitto/pond v1.9.2 github.com/apache/thrift v0.21.0 github.com/edwingeng/doublejump v1.0.1 github.com/fatih/color v1.18.0 github.com/go-ping/ping v1.2.0 github.com/go-redis/redis_rate/v10 v10.0.1 github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5 github.com/gogo/protobuf v1.3.2 github.com/golang/snappy v0.0.4 github.com/grandcat/zeroconf v1.0.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/golang-lru v1.0.2 github.com/juju/ratelimit v1.0.2 github.com/julienschmidt/httprouter v1.3.0 github.com/kavu/go_reuseport v1.5.0 github.com/kr/pretty v0.3.1 github.com/quic-go/quic-go v0.57.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/redis/go-redis/v9 v9.7.3 github.com/rpcxio/libkv v0.5.1 github.com/rs/cors v1.11.1 github.com/rubyist/circuitbreaker v2.2.1+incompatible github.com/smallnest/quick v0.2.0 github.com/smallnest/rsocket v0.0.0-20241130031020-4a72eb6ff62a github.com/soheilhy/cmux v0.1.5 github.com/stretchr/testify v1.11.1 github.com/tinylib/msgp v1.2.5 github.com/twpayne/go-jsonstruct/v3 v3.1.0 github.com/valyala/fastrand v1.1.0 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/xtaci/kcp-go v5.4.20+incompatible golang.org/x/net v0.43.0 golang.org/x/sync v0.16.0 google.golang.org/protobuf v1.36.4 ) require ( github.com/cenk/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/reedsolomon v1.12.4 // indirect github.com/kr/text v0.2.0 // indirect github.com/libp2p/go-sockaddr v0.2.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/miekg/dns v1.1.63 // indirect github.com/onsi/gomega v1.36.2 // indirect github.com/peterbourgon/g2s v0.0.0-20140925154142-ec76db4c1ac1 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.6/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenk/backoff v2.2.1+incompatible h1:djdFT7f4gF2ttuzRKPbMOWgZajgesItGLwG5FTQKmmE= github.com/cenk/backoff v2.2.1+incompatible/go.mod h1:7FtoeaSnHoZnmZzz47cM35Y9nSW7tNyaidugnHTaFDE= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-jump v0.0.0-20170409065014-e1f439676b57/go.mod h1:4hKCXuwrJoYvHZxJ86+bRVTOMyJ0Ej+RqfSm8mHi6KA= github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 h1:0wH6nO9QEa02Qx8sIQGw6ieKdz+BXjpccSOo9vXNl4U= github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0/go.mod h1:4hKCXuwrJoYvHZxJ86+bRVTOMyJ0Ej+RqfSm8mHi6KA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/edwingeng/doublejump v1.0.1 h1:wJ6QgNyyF23Of9vw+ThbwJ/obe9KdxaWEg/Brpv5S1o= github.com/edwingeng/doublejump v1.0.1/go.mod h1:ykMWX8JWePtMtk2OGjNE9kwtgpI+SF2FNIyXV4gS36k= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ= github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= github.com/go-redis/redis/v8 v8.8.2/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis_rate/v10 v10.0.1 h1:calPxi7tVlxojKunJwQ72kwfozdy25RjA0bCj1h0MUo= github.com/go-redis/redis_rate/v10 v10.0.1/go.mod h1:EMiuO9+cjRkR7UvdvwMO7vbgqJkltQHtwbdIQvaBKIU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5 h1:5zELAgnSz0gqmr4Q5DWCoOzNHoeBAxVUXB7LS1eG+sw= github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5/go.mod h1:ermjEDUoT/fS+3Ona5Vd6t6mZkw1eHp99ILO5jGRBkM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/hashicorp/consul/api v1.8.1/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk= github.com/hashicorp/consul/sdk v0.7.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kavu/go_reuseport v1.5.0 h1:UNuiY2OblcqAtVDE8Gsg1kZz8zbBWg907sP1ceBV+bk= github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA= github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-sockaddr v0.2.0 h1:Alhhj6lGxVAon9O32tOO89T601EugSx6YiGjy5BVjWk= github.com/libp2p/go-sockaddr v0.2.0/go.mod h1:5NxulaB17yJ07IpzRIleys4un0PJ7WLWgMDLBBWrGw8= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/peterbourgon/g2s v0.0.0-20140925154142-ec76db4c1ac1 h1:5Dl+ADmsGerAqHwWzyLqkNaUBQ+48DQwfDCaW1gHAQM= github.com/peterbourgon/g2s v0.0.0-20140925154142-ec76db4c1ac1/go.mod h1:1VcHEd3ro4QMoHfiNl/j7Jkln9+KQuorp0PItHMJYNg= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rpcxio/libkv v0.5.1 h1:M0/QqwTcdXz7us0NB+2i8Kq5+wikTm7zZ4Hyb/jNgME= github.com/rpcxio/libkv v0.5.1/go.mod h1:zHGgtLr3cFhGtbalum0BrMPOjhFZFJXCKiws/25ewls= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rubyist/circuitbreaker v2.2.1+incompatible h1:KUKd/pV8Geg77+8LNDwdow6rVCAYOp8+kHUyFvL6Mhk= github.com/rubyist/circuitbreaker v2.2.1+incompatible/go.mod h1:Ycs3JgJADPuzJDwffe12k6BZT8hxVi6lFK+gWYJLN4A= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20201211165307-7117e9ea2414/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smallnest/quick v0.2.0 h1:AEvm7ZovZ6Utv+asFDBh866G4ufMNhRNMKbZHVMFYPE= github.com/smallnest/quick v0.2.0/go.mod h1:ODNivpfZTaMgYrNb/fhDtqoEe2TTPxSRo8JaIT/QThI= github.com/smallnest/rsocket v0.0.0-20241130031020-4a72eb6ff62a h1:GI6kCNC5AVFbKA6ZKbVd4r+fk+Z7XZCRQm9LURZY4t4= github.com/smallnest/rsocket v0.0.0-20241130031020-4a72eb6ff62a/go.mod h1:VJeIKKrDEzT4ZNVe87JN9uRLw1XLp/ZnnE9PfsyJ1jY= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU= github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI= github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twpayne/go-jsonstruct/v3 v3.1.0 h1:NDK73LAXyMKW/FlkxopMKjOcIGw5HgV0ayUWS1xll68= github.com/twpayne/go-jsonstruct/v3 v3.1.0/go.mod h1:2pXzrqn1yuRpon60R7JTPQTxYBiWLMSsrh/CDmk1i0Q= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xtaci/kcp-go v5.4.20+incompatible h1:TN1uey3Raw0sTz0Fg8GkfM0uH3YwzhnZWQ1bABv5xAg= github.com/xtaci/kcp-go v5.4.20+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= ================================================ FILE: log/default_logger.go ================================================ package log import ( "fmt" "io" "log" "os" "strconv" "github.com/fatih/color" ) // The default logger output at level error, to turn everything on like this: // export RPCX_LOG_LEVEL=99 // type of log level type Level int // log level constant const ( LvPanic Level = iota LvFatal LvError LvWarn LvInfo LvDebug LvMax ) type outputFn func(calldepth int, s string) error func dropOutput(calldepth int, s string) error { return nil } type defaultLogger struct { *log.Logger out [LvMax]outputFn } func NewDefaultLogger(out io.Writer, prefix string, flag int, lv Level) *defaultLogger { // set default level to error level := lv // read level from system enviroment to override levelStr := os.Getenv("RPCX_LOG_LEVEL") if len(levelStr) != 0 { if vl, err := strconv.Atoi(levelStr); err == nil { level = Level(vl) } } l := &defaultLogger{} l.Logger = log.New(out, prefix, flag) for i := int(LvFatal); i < int(LvMax); i++ { // always enable fatal and panic // enable only lv <= RPCX_LOG_LEVEL if i <= int(level) { l.out[i] = l.Output } else { l.out[i] = dropOutput } } return l } func (l *defaultLogger) Debug(v ...interface{}) { _ = l.out[int(LvDebug)](calldepth, header("DEBUG", fmt.Sprint(v...))) } func (l *defaultLogger) Debugf(format string, v ...interface{}) { _ = l.out[int(LvDebug)](calldepth, header("DEBUG", fmt.Sprintf(format, v...))) } func (l *defaultLogger) Info(v ...interface{}) { _ = l.out[int(LvInfo)](calldepth, header(color.GreenString("INFO "), fmt.Sprint(v...))) } func (l *defaultLogger) Infof(format string, v ...interface{}) { _ = l.out[int(LvInfo)](calldepth, header(color.GreenString("INFO "), fmt.Sprintf(format, v...))) } func (l *defaultLogger) Warn(v ...interface{}) { _ = l.out[int(LvWarn)](calldepth, header(color.YellowString("WARN "), fmt.Sprint(v...))) } func (l *defaultLogger) Warnf(format string, v ...interface{}) { _ = l.out[int(LvWarn)](calldepth, header(color.YellowString("WARN "), fmt.Sprintf(format, v...))) } func (l *defaultLogger) Error(v ...interface{}) { _ = l.out[int(LvError)](calldepth, header(color.RedString("ERROR"), fmt.Sprint(v...))) } func (l *defaultLogger) Errorf(format string, v ...interface{}) { _ = l.out[int(LvError)](calldepth, header(color.RedString("ERROR"), fmt.Sprintf(format, v...))) } func (l *defaultLogger) Fatal(v ...interface{}) { _ = l.Logger.Output(calldepth, header(color.MagentaString("FATAL"), fmt.Sprint(v...))) os.Exit(1) } func (l *defaultLogger) Fatalf(format string, v ...interface{}) { _ = l.Logger.Output(calldepth, header(color.MagentaString("FATAL"), fmt.Sprintf(format, v...))) os.Exit(1) } func (l *defaultLogger) Panic(v ...interface{}) { l.Logger.Panic(v...) } func (l *defaultLogger) Panicf(format string, v ...interface{}) { l.Logger.Panicf(format, v...) } func header(lvl, msg string) string { return fmt.Sprintf("%s: %s", lvl, msg) } ================================================ FILE: log/dummy_logger.go ================================================ package log type dummyLogger struct{} func (l *dummyLogger) Debug(v ...interface{}) { } func (l *dummyLogger) Debugf(format string, v ...interface{}) { } func (l *dummyLogger) Info(v ...interface{}) { } func (l *dummyLogger) Infof(format string, v ...interface{}) { } func (l *dummyLogger) Warn(v ...interface{}) { } func (l *dummyLogger) Warnf(format string, v ...interface{}) { } func (l *dummyLogger) Error(v ...interface{}) { } func (l *dummyLogger) Errorf(format string, v ...interface{}) { } func (l *dummyLogger) Fatal(v ...interface{}) { } func (l *dummyLogger) Fatalf(format string, v ...interface{}) { } func (l *dummyLogger) Panic(v ...interface{}) { } func (l *dummyLogger) Panicf(format string, v ...interface{}) { } ================================================ FILE: log/logger.go ================================================ package log import ( "log" "os" ) const ( calldepth = 3 ) var l Logger = NewDefaultLogger(os.Stdout, "", log.LstdFlags|log.Lshortfile, LvError) type Logger interface { Debug(v ...interface{}) Debugf(format string, v ...interface{}) Info(v ...interface{}) Infof(format string, v ...interface{}) Warn(v ...interface{}) Warnf(format string, v ...interface{}) Error(v ...interface{}) Errorf(format string, v ...interface{}) Fatal(v ...interface{}) Fatalf(format string, v ...interface{}) Panic(v ...interface{}) Panicf(format string, v ...interface{}) } func SetLogger(logger Logger) { l = logger } func GetLogger() Logger { return l } func SetDummyLogger() { l = &dummyLogger{} } func Debug(v ...interface{}) { l.Debug(v...) } func Debugf(format string, v ...interface{}) { l.Debugf(format, v...) } func Info(v ...interface{}) { l.Info(v...) } func Infof(format string, v ...interface{}) { l.Infof(format, v...) } func Warn(v ...interface{}) { l.Warn(v...) } func Warnf(format string, v ...interface{}) { l.Warnf(format, v...) } func Error(v ...interface{}) { l.Error(v...) } func Errorf(format string, v ...interface{}) { l.Errorf(format, v...) } func Fatal(v ...interface{}) { l.Fatal(v...) } func Fatalf(format string, v ...interface{}) { l.Fatalf(format, v...) } func Panic(v ...interface{}) { l.Panic(v...) } func Panicf(format string, v ...interface{}) { l.Panicf(format, v...) } ================================================ FILE: protocol/compressor.go ================================================ package protocol import ( "bytes" "io" "github.com/golang/snappy" "github.com/smallnest/rpcx/util" ) // Compressor defines a common compression interface. type Compressor interface { Zip([]byte) ([]byte, error) Unzip([]byte) ([]byte, error) } // GzipCompressor implements gzip compressor. type GzipCompressor struct { } func (c GzipCompressor) Zip(data []byte) ([]byte, error) { return util.Zip(data) } func (c GzipCompressor) Unzip(data []byte) ([]byte, error) { return util.Unzip(data) } type RawDataCompressor struct { } func (c RawDataCompressor) Zip(data []byte) ([]byte, error) { return data, nil } func (c RawDataCompressor) Unzip(data []byte) ([]byte, error) { return data, nil } // SnappyCompressor implements snappy compressor type SnappyCompressor struct { } func (c *SnappyCompressor) Zip(data []byte) ([]byte, error) { if len(data) == 0 { return data, nil } var buffer bytes.Buffer writer := snappy.NewBufferedWriter(&buffer) _, err := writer.Write(data) if err != nil { writer.Close() return nil, err } err = writer.Close() if err != nil { return nil, err } return buffer.Bytes(), nil } func (c *SnappyCompressor) Unzip(data []byte) ([]byte, error) { if len(data) == 0 { return data, nil } reader := snappy.NewReader(bytes.NewReader(data)) out, err := io.ReadAll(reader) if err != nil { return nil, err } return out, err } ================================================ FILE: protocol/compressor_test.go ================================================ package protocol import ( "reflect" "testing" "github.com/smallnest/rpcx/codec" "github.com/smallnest/rpcx/protocol/testdata" ) func newBenchmarkMessage() *testdata.BenchmarkMessage { var theAnswer = "Answer to the Ultimate Question of Life, the Universe, and Everything" var args testdata.BenchmarkMessage v := reflect.ValueOf(&args).Elem() for k := 0; k < v.NumField(); k++ { field := v.Field(k) fieldName := v.Type().Field(k).Name // filter unexported fields if fieldName[0] >= 97 && fieldName[0] <= 122 { continue } switch field.Kind() { case reflect.Int, reflect.Int32, reflect.Int64: field.SetInt(31415926) case reflect.Bool: field.SetBool(true) case reflect.String: field.SetString(theAnswer) } } return &args } func BenchmarkGzipCompressor_Zip(b *testing.B) { compressor := GzipCompressor{} serializer := codec.PBCodec{} raw, _ := serializer.Encode(newBenchmarkMessage()) zipped := make([]byte, 1024) b.ResetTimer() for i := 0; i < b.N; i++ { zipped, _ = compressor.Zip(raw) } b.ReportMetric(float64(len(zipped)), "bytes") } func BenchmarkRawDataCompressor_Zip(b *testing.B) { compressor := RawDataCompressor{} serializer := codec.PBCodec{} raw, _ := serializer.Encode(newBenchmarkMessage()) zipped := make([]byte, 1024) b.ResetTimer() for i := 0; i < b.N; i++ { zipped, _ = compressor.Zip(raw) } b.ReportMetric(float64(len(zipped)), "bytes") } func BenchmarkSnappyCompressor_Zip(b *testing.B) { compressor := SnappyCompressor{} serializer := codec.PBCodec{} raw, _ := serializer.Encode(newBenchmarkMessage()) zipped := make([]byte, 1024) b.ResetTimer() for i := 0; i < b.N; i++ { zipped, _ = compressor.Zip(raw) } b.ReportMetric(float64(len(zipped)), "bytes") } func BenchmarkGzipCompressor_Unzip(b *testing.B) { compressor := GzipCompressor{} serializer := codec.PBCodec{} raw, _ := serializer.Encode(newBenchmarkMessage()) zipped, _ := compressor.Zip(raw) b.ResetTimer() for i := 0; i < b.N; i++ { raw, _ = compressor.Unzip(zipped) } b.ReportMetric(float64(len(raw)), "bytes") } func BenchmarkRawDataCompressor_Unzip(b *testing.B) { compressor := RawDataCompressor{} serializer := codec.PBCodec{} raw, _ := serializer.Encode(newBenchmarkMessage()) zipped, _ := compressor.Zip(raw) b.ResetTimer() for i := 0; i < b.N; i++ { raw, _ = compressor.Unzip(zipped) } b.ReportMetric(float64(len(raw)), "bytes") } func BenchmarkSnappyCompressor_Unzip(b *testing.B) { compressor := SnappyCompressor{} serializer := codec.PBCodec{} raw, _ := serializer.Encode(newBenchmarkMessage()) zipped, _ := compressor.Zip(raw) b.ResetTimer() for i := 0; i < b.N; i++ { raw, _ = compressor.Unzip(zipped) } b.ReportMetric(float64(len(raw)), "bytes") } ================================================ FILE: protocol/message.go ================================================ package protocol import ( "bytes" "encoding/binary" "errors" "fmt" "io" "runtime" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/util" ) var bufferPool = util.NewLimitedPool(512, 4096) // Compressors are compressors supported by rpcx. You can add customized compressor in Compressors. var Compressors = map[CompressType]Compressor{ None: &RawDataCompressor{}, Gzip: &GzipCompressor{}, } // MaxMessageLength is the max length of a message. // Default is 0 that means does not limit length of messages. // It is used to validate when read messages from io.Reader. var MaxMessageLength = 0 const ( magicNumber byte = 0x08 ) func MagicNumber() byte { return magicNumber } var ( // ErrMetaKVMissing some keys or values are missing. ErrMetaKVMissing = errors.New("wrong metadata lines. some keys or values are missing") // ErrMessageTooLong message is too long ErrMessageTooLong = errors.New("message is too long") ErrUnsupportedCompressor = errors.New("unsupported compressor") ) const ( // ServiceError contains error info of service invocation ServiceError = "__rpcx_error__" ) // MessageType is message type of requests and responses. type MessageType byte const ( // Request is message type of request Request MessageType = iota // Response is message type of response Response ) // MessageStatusType is status of messages. type MessageStatusType byte const ( // Normal is normal requests and responses. Normal MessageStatusType = iota // Error indicates some errors occur. Error ) // CompressType defines decompression type. type CompressType byte const ( // None does not compress. None CompressType = iota // Gzip uses gzip compression. Gzip ) // SerializeType defines serialization type of payload. type SerializeType byte const ( // SerializeNone uses raw []byte and don't serialize/deserialize SerializeNone SerializeType = iota // JSON for payload. JSON // ProtoBuffer for payload. ProtoBuffer // MsgPack for payload MsgPack // Thrift // Thrift for payload Thrift ) // Message is the generic type of Request and Response. type Message struct { *Header ServicePath string ServiceMethod string Metadata map[string]string Payload []byte data []byte } // NewMessage creates an empty message. func NewMessage() *Message { header := Header([12]byte{}) header[0] = magicNumber return &Message{ Header: &header, } } // Header is the first part of Message and has fixed size. // Format: type Header [12]byte // CheckMagicNumber checks whether header starts rpcx magic number. func (h Header) CheckMagicNumber() bool { return h[0] == magicNumber } // Version returns version of rpcx protocol. func (h Header) Version() byte { return h[1] } // SetVersion sets version for this header. func (h *Header) SetVersion(v byte) { h[1] = v } // MessageType returns the message type. func (h Header) MessageType() MessageType { return MessageType(h[2]&0x80) >> 7 } // SetMessageType sets message type. func (h *Header) SetMessageType(mt MessageType) { h[2] = h[2] | (byte(mt) << 7) } // IsHeartbeat returns whether the message is heartbeat message. func (h Header) IsHeartbeat() bool { return h[2]&0x40 == 0x40 } // SetHeartbeat sets the heartbeat flag. func (h *Header) SetHeartbeat(hb bool) { if hb { h[2] = h[2] | 0x40 } else { h[2] = h[2] &^ 0x40 } } // IsOneway returns whether the message is one-way message. // If true, server won't send responses. func (h Header) IsOneway() bool { return h[2]&0x20 == 0x20 } // SetOneway sets the oneway flag. func (h *Header) SetOneway(oneway bool) { if oneway { h[2] = h[2] | 0x20 } else { h[2] = h[2] &^ 0x20 } } // CompressType returns compression type of messages. func (h Header) CompressType() CompressType { return CompressType((h[2] & 0x1C) >> 2) } // SetCompressType sets the compression type. func (h *Header) SetCompressType(ct CompressType) { h[2] = (h[2] &^ 0x1C) | ((byte(ct) << 2) & 0x1C) } // MessageStatusType returns the message status type. func (h Header) MessageStatusType() MessageStatusType { return MessageStatusType(h[2] & 0x03) } // SetMessageStatusType sets message status type. func (h *Header) SetMessageStatusType(mt MessageStatusType) { h[2] = (h[2] &^ 0x03) | (byte(mt) & 0x03) } // SerializeType returns serialization type of payload. func (h Header) SerializeType() SerializeType { return SerializeType((h[3] & 0xF0) >> 4) } // SetSerializeType sets the serialization type. func (h *Header) SetSerializeType(st SerializeType) { h[3] = (h[3] &^ 0xF0) | (byte(st) << 4) } // Seq returns sequence number of messages. func (h Header) Seq() uint64 { return binary.BigEndian.Uint64(h[4:]) } // SetSeq sets sequence number. func (h *Header) SetSeq(seq uint64) { binary.BigEndian.PutUint64(h[4:], seq) } // Clone clones from an message. func (m Message) Clone() *Message { header := *m.Header c := NewMessage() header.SetCompressType(None) c.Header = &header c.ServicePath = m.ServicePath c.ServiceMethod = m.ServiceMethod return c } // Encode encodes messages. func (m Message) Encode() []byte { data := m.EncodeSlicePointer() return *data } // EncodeSlicePointer encodes messages as a byte slice pointer we can use pool to improve. func (m Message) EncodeSlicePointer() *[]byte { var bb = bytes.NewBuffer(make([]byte, 0, len(m.Metadata)*64)) encodeMetadata(m.Metadata, bb) meta := bb.Bytes() spL := len(m.ServicePath) smL := len(m.ServiceMethod) var err error payload := m.Payload if m.CompressType() != None { compressor := Compressors[m.CompressType()] if compressor == nil { m.SetCompressType(None) } else { payload, err = compressor.Zip(m.Payload) if err != nil { m.SetCompressType(None) payload = m.Payload } } } totalL := (4 + spL) + (4 + smL) + (4 + len(meta)) + (4 + len(payload)) // header + dataLen + spLen + sp + smLen + sm + metaL + meta + payloadLen + payload metaStart := 12 + 4 + (4 + spL) + (4 + smL) payLoadStart := metaStart + (4 + len(meta)) l := 12 + 4 + totalL data := bufferPool.Get(l) copy(*data, m.Header[:]) // totalLen binary.BigEndian.PutUint32((*data)[12:16], uint32(totalL)) binary.BigEndian.PutUint32((*data)[16:20], uint32(spL)) copy((*data)[20:20+spL], util.StringToSliceByte(m.ServicePath)) binary.BigEndian.PutUint32((*data)[20+spL:24+spL], uint32(smL)) copy((*data)[24+spL:metaStart], util.StringToSliceByte(m.ServiceMethod)) binary.BigEndian.PutUint32((*data)[metaStart:metaStart+4], uint32(len(meta))) copy((*data)[metaStart+4:], meta) binary.BigEndian.PutUint32((*data)[payLoadStart:payLoadStart+4], uint32(len(payload))) copy((*data)[payLoadStart+4:], payload) return data } // PutData puts the byte slice into pool. func PutData(data *[]byte) { bufferPool.Put(data) } // WriteTo writes message to writers. func (m Message) WriteTo(w io.Writer) (int64, error) { nn, err := w.Write(m.Header[:]) n := int64(nn) if err != nil { return n, err } bb := bytes.NewBuffer(make([]byte, 0, len(m.Metadata)*64)) encodeMetadata(m.Metadata, bb) meta := bb.Bytes() spL := len(m.ServicePath) smL := len(m.ServiceMethod) payload := m.Payload if m.CompressType() != None { compressor := Compressors[m.CompressType()] if compressor == nil { return n, ErrUnsupportedCompressor } payload, err = compressor.Zip(m.Payload) if err != nil { return n, err } } totalL := (4 + spL) + (4 + smL) + (4 + len(meta)) + (4 + len(payload)) err = binary.Write(w, binary.BigEndian, uint32(totalL)) if err != nil { return n, err } // write servicePath and serviceMethod err = binary.Write(w, binary.BigEndian, uint32(len(m.ServicePath))) if err != nil { return n, err } _, err = w.Write(util.StringToSliceByte(m.ServicePath)) if err != nil { return n, err } err = binary.Write(w, binary.BigEndian, uint32(len(m.ServiceMethod))) if err != nil { return n, err } _, err = w.Write(util.StringToSliceByte(m.ServiceMethod)) if err != nil { return n, err } // write meta err = binary.Write(w, binary.BigEndian, uint32(len(meta))) if err != nil { return n, err } _, err = w.Write(meta) if err != nil { return n, err } // write payload err = binary.Write(w, binary.BigEndian, uint32(len(payload))) if err != nil { return n, err } nn, err = w.Write(payload) return int64(nn), err } // len,string,len,string,...... func encodeMetadata(m map[string]string, bb *bytes.Buffer) { if len(m) == 0 { return } d := make([]byte, 4) for k, v := range m { binary.BigEndian.PutUint32(d, uint32(len(k))) bb.Write(d) bb.Write(util.StringToSliceByte(k)) binary.BigEndian.PutUint32(d, uint32(len(v))) bb.Write(d) bb.Write(util.StringToSliceByte(v)) } } func decodeMetadata(l uint32, data []byte) (map[string]string, error) { m := make(map[string]string, 10) n := uint32(0) for n < l { // parse one key and value // key sl := binary.BigEndian.Uint32(data[n : n+4]) n = n + 4 if n+sl > l-4 { return m, ErrMetaKVMissing } k := string(data[n : n+sl]) n = n + sl // value sl = binary.BigEndian.Uint32(data[n : n+4]) n = n + 4 if n+sl > l { return m, ErrMetaKVMissing } v := string(data[n : n+sl]) n = n + sl m[k] = v } return m, nil } // Read reads a message from r. func Read(r io.Reader) (*Message, error) { msg := NewMessage() err := msg.Decode(r) if err != nil { return nil, err } return msg, nil } // Decode decodes a message from reader. func (m *Message) Decode(r io.Reader) error { defer func() { if err := recover(); err != nil { var errStack = make([]byte, 1024) n := runtime.Stack(errStack, true) log.Errorf("panic in message decode: %v, stack: %s", err, errStack[:n]) } }() // parse header _, err := io.ReadFull(r, m.Header[:1]) if err != nil { return err } if !m.Header.CheckMagicNumber() { return fmt.Errorf("wrong magic number: %v", m.Header[0]) } _, err = io.ReadFull(r, m.Header[1:]) if err != nil { return err } // total lenData := make([]byte, 4) _, err = io.ReadFull(r, lenData) if err != nil { return err } l := binary.BigEndian.Uint32(lenData) if MaxMessageLength > 0 && int(l) > MaxMessageLength { return ErrMessageTooLong } totalL := int(l) if cap(m.data) >= totalL { // reuse data m.data = m.data[:totalL] } else { m.data = make([]byte, totalL) } data := m.data _, err = io.ReadFull(r, data) if err != nil { return err } n := 0 // parse servicePath l = binary.BigEndian.Uint32(data[n:4]) n = n + 4 nEnd := n + int(l) m.ServicePath = util.SliceByteToString(data[n:nEnd]) n = nEnd // parse serviceMethod l = binary.BigEndian.Uint32(data[n : n+4]) n = n + 4 nEnd = n + int(l) m.ServiceMethod = util.SliceByteToString(data[n:nEnd]) n = nEnd // parse meta l = binary.BigEndian.Uint32(data[n : n+4]) n = n + 4 nEnd = n + int(l) if l > 0 { m.Metadata, err = decodeMetadata(l, data[n:nEnd]) if err != nil { return err } } n = nEnd // parse payload l = binary.BigEndian.Uint32(data[n : n+4]) _ = l n = n + 4 m.Payload = data[n:] if m.CompressType() != None { compressor := Compressors[m.CompressType()] if compressor == nil { return ErrUnsupportedCompressor } m.Payload, err = compressor.Unzip(m.Payload) if err != nil { return err } } return err } // Reset clean data of this message but keep allocated data func (m *Message) Reset() { resetHeader(m.Header) m.Metadata = nil m.Payload = []byte{} m.data = m.data[:0] m.ServicePath = "" m.ServiceMethod = "" } var ( zeroHeaderArray Header zeroHeader = zeroHeaderArray[1:] ) func resetHeader(h *Header) { copy(h[1:], zeroHeader) } ================================================ FILE: protocol/message_chan_test.go ================================================ package protocol import ( "fmt" "strings" "sync/atomic" "testing" "time" ) func TestChanValue(t *testing.T) { var ct uint64 ch := make(chan Message, 10000) go func(ch <-chan Message) { for range ch { atomic.AddUint64(&ct, 1) } }(ch) go func(ch chan Message) { m := strings.Repeat("_", 100) p := strings.Repeat("_", 100) payload := make([]byte, 1024) for { ch <- Message{ServiceMethod: m, ServicePath: p, Payload: payload} } }(ch) for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println(atomic.LoadUint64(&ct)) atomic.StoreUint64(&ct, 0) } } func TestChanPtr(t *testing.T) { var ct uint64 ch := make(chan *Message, 10000) go func(ch <-chan *Message) { for range ch { atomic.AddUint64(&ct, 1) } }(ch) go func(ch chan *Message) { m := strings.Repeat("_", 100) p := strings.Repeat("_", 100) payload := make([]byte, 1024) for { ch <- &Message{ServiceMethod: m, ServicePath: p,Payload: payload} } }(ch) for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println(atomic.LoadUint64(&ct)) atomic.StoreUint64(&ct, 0) } } func BenchmarkChanValue(b *testing.B) { ch := make(chan Message, 10000) var ct uint64 go func(ch <-chan Message) { for range ch { atomic.AddUint64(&ct, 1) } }(ch) b.ReportAllocs() b.ResetTimer() m := strings.Repeat("_", 100) p := strings.Repeat("_", 100) payload := make([]byte, 1024) for i := 0; i < b.N; i++ { ch <- Message{ServiceMethod: m, ServicePath: p,Payload: payload} } } func BenchmarkChanPtr(b *testing.B) { ch := make(chan *Message, 10000) var ct uint64 go func(ch <-chan *Message) { for range ch { atomic.AddUint64(&ct, 1) } }(ch) b.ReportAllocs() b.ResetTimer() m := strings.Repeat("_", 100) p := strings.Repeat("_", 100) payload := make([]byte, 1024) for i := 0; i < b.N; i++ { ch <- &Message{ServiceMethod: m, ServicePath: p,Payload: payload} } } ================================================ FILE: protocol/message_test.go ================================================ package protocol import ( "bytes" "testing" ) func TestMessage(t *testing.T) { req := NewMessage() req.SetVersion(0) req.SetMessageType(Request) req.SetHeartbeat(false) req.SetOneway(false) req.SetCompressType(None) req.SetMessageStatusType(Normal) req.SetSerializeType(JSON) req.SetSeq(1234567890) m := make(map[string]string) req.ServicePath = "Arith" req.ServiceMethod = "Add" m["__ID"] = "6ba7b810-9dad-11d1-80b4-00c04fd430c9" req.Metadata = m payload := `{ "A": 1, "B": 2, } ` req.Payload = []byte(payload) var buf bytes.Buffer _, err := req.WriteTo(&buf) if err != nil { t.Fatal(err) } res, err := Read(&buf) if err != nil { t.Fatal(err) } res.SetMessageType(Response) if res.Version() != 0 { t.Errorf("expect 0 but got %d", res.Version()) } if res.Seq() != 1234567890 { t.Errorf("expect 1234567890 but got %d", res.Seq()) } if res.ServicePath != "Arith" || res.ServiceMethod != "Add" || res.Metadata["__ID"] != "6ba7b810-9dad-11d1-80b4-00c04fd430c9" { t.Errorf("got wrong metadata: %v", res.Metadata) } if string(res.Payload) != payload { t.Errorf("got wrong payload: %v", string(res.Payload)) } } ================================================ FILE: protocol/testdata/benchmark.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.33.0 // protoc v5.26.0 // source: benchmark.proto package testdata import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type BenchmarkMessage struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Field1 string `protobuf:"bytes,1,opt,name=field1,proto3" json:"field1,omitempty"` Field9 string `protobuf:"bytes,9,opt,name=field9,proto3" json:"field9,omitempty"` Field18 string `protobuf:"bytes,18,opt,name=field18,proto3" json:"field18,omitempty"` Field80 bool `protobuf:"varint,80,opt,name=field80,proto3" json:"field80,omitempty"` Field81 bool `protobuf:"varint,81,opt,name=field81,proto3" json:"field81,omitempty"` Field2 int32 `protobuf:"varint,2,opt,name=field2,proto3" json:"field2,omitempty"` Field3 int32 `protobuf:"varint,3,opt,name=field3,proto3" json:"field3,omitempty"` Field280 int32 `protobuf:"varint,280,opt,name=field280,proto3" json:"field280,omitempty"` Field6 int32 `protobuf:"varint,6,opt,name=field6,proto3" json:"field6,omitempty"` Field22 int64 `protobuf:"varint,22,opt,name=field22,proto3" json:"field22,omitempty"` Field4 string `protobuf:"bytes,4,opt,name=field4,proto3" json:"field4,omitempty"` Field5 []uint64 `protobuf:"fixed64,5,rep,packed,name=field5,proto3" json:"field5,omitempty"` Field59 bool `protobuf:"varint,59,opt,name=field59,proto3" json:"field59,omitempty"` Field7 string `protobuf:"bytes,7,opt,name=field7,proto3" json:"field7,omitempty"` Field16 int32 `protobuf:"varint,16,opt,name=field16,proto3" json:"field16,omitempty"` Field130 int32 `protobuf:"varint,130,opt,name=field130,proto3" json:"field130,omitempty"` Field12 bool `protobuf:"varint,12,opt,name=field12,proto3" json:"field12,omitempty"` Field17 bool `protobuf:"varint,17,opt,name=field17,proto3" json:"field17,omitempty"` Field13 bool `protobuf:"varint,13,opt,name=field13,proto3" json:"field13,omitempty"` Field14 bool `protobuf:"varint,14,opt,name=field14,proto3" json:"field14,omitempty"` Field104 int32 `protobuf:"varint,104,opt,name=field104,proto3" json:"field104,omitempty"` Field100 int32 `protobuf:"varint,100,opt,name=field100,proto3" json:"field100,omitempty"` Field101 int32 `protobuf:"varint,101,opt,name=field101,proto3" json:"field101,omitempty"` Field102 string `protobuf:"bytes,102,opt,name=field102,proto3" json:"field102,omitempty"` Field103 string `protobuf:"bytes,103,opt,name=field103,proto3" json:"field103,omitempty"` Field29 int32 `protobuf:"varint,29,opt,name=field29,proto3" json:"field29,omitempty"` Field30 bool `protobuf:"varint,30,opt,name=field30,proto3" json:"field30,omitempty"` Field60 int32 `protobuf:"varint,60,opt,name=field60,proto3" json:"field60,omitempty"` Field271 int32 `protobuf:"varint,271,opt,name=field271,proto3" json:"field271,omitempty"` Field272 int32 `protobuf:"varint,272,opt,name=field272,proto3" json:"field272,omitempty"` Field150 int32 `protobuf:"varint,150,opt,name=field150,proto3" json:"field150,omitempty"` Field23 int32 `protobuf:"varint,23,opt,name=field23,proto3" json:"field23,omitempty"` Field24 bool `protobuf:"varint,24,opt,name=field24,proto3" json:"field24,omitempty"` Field25 int32 `protobuf:"varint,25,opt,name=field25,proto3" json:"field25,omitempty"` Field78 bool `protobuf:"varint,78,opt,name=field78,proto3" json:"field78,omitempty"` Field67 int32 `protobuf:"varint,67,opt,name=field67,proto3" json:"field67,omitempty"` Field68 int32 `protobuf:"varint,68,opt,name=field68,proto3" json:"field68,omitempty"` Field128 int32 `protobuf:"varint,128,opt,name=field128,proto3" json:"field128,omitempty"` Field129 string `protobuf:"bytes,129,opt,name=field129,proto3" json:"field129,omitempty"` Field131 int32 `protobuf:"varint,131,opt,name=field131,proto3" json:"field131,omitempty"` } func (x *BenchmarkMessage) Reset() { *x = BenchmarkMessage{} if protoimpl.UnsafeEnabled { mi := &file_benchmark_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *BenchmarkMessage) String() string { return protoimpl.X.MessageStringOf(x) } func (*BenchmarkMessage) ProtoMessage() {} func (x *BenchmarkMessage) ProtoReflect() protoreflect.Message { mi := &file_benchmark_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use BenchmarkMessage.ProtoReflect.Descriptor instead. func (*BenchmarkMessage) Descriptor() ([]byte, []int) { return file_benchmark_proto_rawDescGZIP(), []int{0} } func (x *BenchmarkMessage) GetField1() string { if x != nil { return x.Field1 } return "" } func (x *BenchmarkMessage) GetField9() string { if x != nil { return x.Field9 } return "" } func (x *BenchmarkMessage) GetField18() string { if x != nil { return x.Field18 } return "" } func (x *BenchmarkMessage) GetField80() bool { if x != nil { return x.Field80 } return false } func (x *BenchmarkMessage) GetField81() bool { if x != nil { return x.Field81 } return false } func (x *BenchmarkMessage) GetField2() int32 { if x != nil { return x.Field2 } return 0 } func (x *BenchmarkMessage) GetField3() int32 { if x != nil { return x.Field3 } return 0 } func (x *BenchmarkMessage) GetField280() int32 { if x != nil { return x.Field280 } return 0 } func (x *BenchmarkMessage) GetField6() int32 { if x != nil { return x.Field6 } return 0 } func (x *BenchmarkMessage) GetField22() int64 { if x != nil { return x.Field22 } return 0 } func (x *BenchmarkMessage) GetField4() string { if x != nil { return x.Field4 } return "" } func (x *BenchmarkMessage) GetField5() []uint64 { if x != nil { return x.Field5 } return nil } func (x *BenchmarkMessage) GetField59() bool { if x != nil { return x.Field59 } return false } func (x *BenchmarkMessage) GetField7() string { if x != nil { return x.Field7 } return "" } func (x *BenchmarkMessage) GetField16() int32 { if x != nil { return x.Field16 } return 0 } func (x *BenchmarkMessage) GetField130() int32 { if x != nil { return x.Field130 } return 0 } func (x *BenchmarkMessage) GetField12() bool { if x != nil { return x.Field12 } return false } func (x *BenchmarkMessage) GetField17() bool { if x != nil { return x.Field17 } return false } func (x *BenchmarkMessage) GetField13() bool { if x != nil { return x.Field13 } return false } func (x *BenchmarkMessage) GetField14() bool { if x != nil { return x.Field14 } return false } func (x *BenchmarkMessage) GetField104() int32 { if x != nil { return x.Field104 } return 0 } func (x *BenchmarkMessage) GetField100() int32 { if x != nil { return x.Field100 } return 0 } func (x *BenchmarkMessage) GetField101() int32 { if x != nil { return x.Field101 } return 0 } func (x *BenchmarkMessage) GetField102() string { if x != nil { return x.Field102 } return "" } func (x *BenchmarkMessage) GetField103() string { if x != nil { return x.Field103 } return "" } func (x *BenchmarkMessage) GetField29() int32 { if x != nil { return x.Field29 } return 0 } func (x *BenchmarkMessage) GetField30() bool { if x != nil { return x.Field30 } return false } func (x *BenchmarkMessage) GetField60() int32 { if x != nil { return x.Field60 } return 0 } func (x *BenchmarkMessage) GetField271() int32 { if x != nil { return x.Field271 } return 0 } func (x *BenchmarkMessage) GetField272() int32 { if x != nil { return x.Field272 } return 0 } func (x *BenchmarkMessage) GetField150() int32 { if x != nil { return x.Field150 } return 0 } func (x *BenchmarkMessage) GetField23() int32 { if x != nil { return x.Field23 } return 0 } func (x *BenchmarkMessage) GetField24() bool { if x != nil { return x.Field24 } return false } func (x *BenchmarkMessage) GetField25() int32 { if x != nil { return x.Field25 } return 0 } func (x *BenchmarkMessage) GetField78() bool { if x != nil { return x.Field78 } return false } func (x *BenchmarkMessage) GetField67() int32 { if x != nil { return x.Field67 } return 0 } func (x *BenchmarkMessage) GetField68() int32 { if x != nil { return x.Field68 } return 0 } func (x *BenchmarkMessage) GetField128() int32 { if x != nil { return x.Field128 } return 0 } func (x *BenchmarkMessage) GetField129() string { if x != nil { return x.Field129 } return "" } func (x *BenchmarkMessage) GetField131() int32 { if x != nil { return x.Field131 } return 0 } var File_benchmark_proto protoreflect.FileDescriptor var file_benchmark_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb4, 0x08, 0x0a, 0x10, 0x42, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x39, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x39, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x38, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x38, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x38, 0x30, 0x18, 0x50, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x38, 0x30, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x38, 0x31, 0x18, 0x51, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x38, 0x31, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x38, 0x30, 0x18, 0x98, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x38, 0x30, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x32, 0x18, 0x16, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x32, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x34, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x34, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x18, 0x05, 0x20, 0x03, 0x28, 0x06, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x39, 0x18, 0x3b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x35, 0x39, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x37, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x37, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x36, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x36, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x33, 0x30, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x33, 0x30, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x37, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x37, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x33, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x33, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x34, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x34, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x34, 0x18, 0x68, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x34, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x30, 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x30, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x31, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x31, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x32, 0x18, 0x66, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x32, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x33, 0x18, 0x67, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x30, 0x33, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x39, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x39, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x30, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x33, 0x30, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x30, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x30, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x37, 0x31, 0x18, 0x8f, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x37, 0x31, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x37, 0x32, 0x18, 0x90, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x37, 0x32, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x35, 0x30, 0x18, 0x96, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x35, 0x30, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x33, 0x18, 0x17, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x33, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x34, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x34, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x35, 0x18, 0x19, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x35, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x37, 0x38, 0x18, 0x4e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x37, 0x38, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x37, 0x18, 0x43, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x37, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x38, 0x18, 0x44, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x36, 0x38, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x38, 0x18, 0x80, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x38, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x39, 0x18, 0x81, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x32, 0x39, 0x12, 0x1b, 0x0a, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x33, 0x31, 0x18, 0x83, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x33, 0x31, 0x42, 0x0e, 0x48, 0x01, 0x5a, 0x0a, 0x2e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_benchmark_proto_rawDescOnce sync.Once file_benchmark_proto_rawDescData = file_benchmark_proto_rawDesc ) func file_benchmark_proto_rawDescGZIP() []byte { file_benchmark_proto_rawDescOnce.Do(func() { file_benchmark_proto_rawDescData = protoimpl.X.CompressGZIP(file_benchmark_proto_rawDescData) }) return file_benchmark_proto_rawDescData } var file_benchmark_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_benchmark_proto_goTypes = []interface{}{ (*BenchmarkMessage)(nil), // 0: testdata.BenchmarkMessage } var file_benchmark_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_benchmark_proto_init() } func file_benchmark_proto_init() { if File_benchmark_proto != nil { return } if !protoimpl.UnsafeEnabled { file_benchmark_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BenchmarkMessage); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_benchmark_proto_rawDesc, NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_benchmark_proto_goTypes, DependencyIndexes: file_benchmark_proto_depIdxs, MessageInfos: file_benchmark_proto_msgTypes, }.Build() File_benchmark_proto = out.File file_benchmark_proto_rawDesc = nil file_benchmark_proto_goTypes = nil file_benchmark_proto_depIdxs = nil } ================================================ FILE: protocol/testdata/benchmark.proto ================================================ syntax = "proto3"; package testdata; option optimize_for = SPEED; option go_package = "./testdata"; message BenchmarkMessage { string field1 = 1; string field9 = 9; string field18 = 18; bool field80 = 80; bool field81 = 81; int32 field2 = 2; int32 field3 = 3; int32 field280 = 280; int32 field6 = 6; int64 field22 = 22; string field4 = 4; repeated fixed64 field5 = 5; bool field59 = 59; string field7 = 7; int32 field16 = 16; int32 field130 = 130; bool field12 = 12; bool field17 = 17; bool field13 = 13; bool field14 = 14; int32 field104 = 104; int32 field100 = 100; int32 field101 = 101; string field102 = 102; string field103 = 103; int32 field29 = 29; bool field30 = 30; int32 field60 = 60; int32 field271 = 271; int32 field272 = 272; int32 field150 = 150; int32 field23 = 23; bool field24 = 24; int32 field25 = 25; bool field78 = 78; int32 field67 = 67; int32 field68 = 68; int32 field128 = 128; string field129 = 129; int32 field131 = 131; } ================================================ FILE: protocol/testdata/gen.sh ================================================ # curl -O https://raw.githubusercontent.com/rpcxio/rpcx-benchmark/master/proto/benchmark.proto # generate .go files from IDL protoc -I. --go_out=. --go_opt=module="testdata" ./benchmark.proto ================================================ FILE: reflection/server_reflection.go ================================================ package reflection import ( "context" "encoding/json" "fmt" "path/filepath" "reflect" "strings" "text/template" "unicode" "unicode/utf8" "github.com/smallnest/rpcx/log" "github.com/twpayne/go-jsonstruct/v3" ) var ( typeOfError = reflect.TypeFor[error]() typeOfContext = reflect.TypeFor[context.Context]() ) // ServiceInfo service info. type ServiceInfo struct { Name string PkgPath string Methods []*MethodInfo } // MethodInfo method info type MethodInfo struct { Name string ReqName string Req string ReplyName string Reply string } var siTemplate = `package {{.PkgPath}} type {{.Name}} struct{} {{$name := .Name}} {{range .Methods}} {{.Req}} {{.Reply}} type (s *{{$name}}) {{.Name}}(ctx context.Context, arg *{{.ReqName}}, reply *{{.ReplyName}}) error { return nil } {{end}} ` func (si ServiceInfo) String() string { tpl := template.Must(template.New("service").Parse(siTemplate)) var buf strings.Builder _ = tpl.Execute(&buf, si) return buf.String() } type Reflection struct { Services map[string]*ServiceInfo } func New() *Reflection { return &Reflection{ Services: make(map[string]*ServiceInfo), } } func (r *Reflection) Register(name string, rcvr interface{}, metadata string) error { si := &ServiceInfo{} val := reflect.ValueOf(rcvr) typ := reflect.TypeOf(rcvr) vTyp := reflect.Indirect(val).Type() si.Name = vTyp.Name() pkg := vTyp.PkgPath() if strings.Index(pkg, ".") > 0 { pkg = pkg[strings.LastIndex(pkg, ".")+1:] } pkg = filepath.Base(pkg) pkg = strings.ReplaceAll(pkg, "-", "_") si.PkgPath = pkg for m := 0; m < val.NumMethod(); m++ { method := typ.Method(m) mtype := method.Type if method.PkgPath != "" { continue } if mtype.NumIn() != 4 { continue } // First arg must be context.Context ctxType := mtype.In(1) if !ctxType.Implements(typeOfContext) { continue } // Second arg need not be a pointer. argType := mtype.In(2) if !isExportedOrBuiltinType(argType) { continue } // Third arg must be a pointer. replyType := mtype.In(3) if replyType.Kind() != reflect.Ptr { continue } // Reply type must be exported. if !isExportedOrBuiltinType(replyType) { continue } // Method needs one out. if mtype.NumOut() != 1 { continue } // The return type of the method must be error. if returnType := mtype.Out(0); returnType != typeOfError { continue } mi := &MethodInfo{} mi.Name = method.Name if argType.Kind() == reflect.Ptr { argType = argType.Elem() } replyType = replyType.Elem() mi.ReqName = argType.Name() mi.Req = generateTypeDefination(mi.ReqName, si.PkgPath, generateJSON(argType)) mi.ReplyName = replyType.Name() mi.Reply = generateTypeDefination(mi.ReplyName, si.PkgPath, generateJSON(replyType)) si.Methods = append(si.Methods, mi) } if len(si.Methods) > 0 { r.Services[name] = si } return nil } func (r *Reflection) Unregister(name string) error { delete(r.Services, name) return nil } func (r *Reflection) GetService(ctx context.Context, s string, reply *string) error { si, ok := r.Services[s] if !ok { return fmt.Errorf("not found service %s", s) } *reply = si.String() return nil } func (r *Reflection) GetServices(ctx context.Context, s string, reply *string) error { var buf strings.Builder pkg := `package ` for _, si := range r.Services { if pkg == `package ` { pkg = pkg + si.PkgPath + "\n\n" } buf.WriteString(strings.ReplaceAll(si.String(), pkg, "")) } if pkg != `package ` { *reply = pkg + buf.String() } else { *reply = buf.String() } return nil } func generateTypeDefination(name, pkg string, jsonValue string) string { jsonValue = strings.TrimSpace(jsonValue) if jsonValue == "" || jsonValue == `""` { return "" } r := strings.NewReader(jsonValue) generator := jsonstruct.NewGenerator( jsonstruct.WithPackageName(pkg), jsonstruct.WithTypeName(name), jsonstruct.WithStructTagNames(nil), jsonstruct.WithIntType("int64"), ) if err := generator.ObserveJSONReader(r); err != nil { log.Errorf("failed to observe json: %v", err) return "" } output, err := generator.Generate() if err != nil { log.Errorf("failed to generate json: %v", err) return "" } rt := strings.ReplaceAll(string(output), "``", "") return strings.ReplaceAll(rt, "package "+pkg+"\n\n", "") } func generateJSON(typ reflect.Type) string { v := reflect.New(typ).Interface() data, _ := json.Marshal(v) return string(data) } func isExported(name string) bool { rune, _ := utf8.DecodeRuneInString(name) return unicode.IsUpper(rune) } func isExportedOrBuiltinType(t reflect.Type) bool { for t.Kind() == reflect.Ptr { t = t.Elem() } return isExported(t.Name()) || t.PkgPath() == "" } ================================================ FILE: reflection/server_reflection_test.go ================================================ package reflection import ( "context" "reflect" "testing" "github.com/kr/pretty" testutils "github.com/smallnest/rpcx/_testutils" "github.com/stretchr/testify/assert" ) type PBArith int func (t *PBArith) Mul(ctx context.Context, args *testutils.ProtoArgs, reply *testutils.ProtoReply) error { reply.C = args.A * args.B return nil } func TestReflection_Register(t *testing.T) { r := New() arith := PBArith(0) err := r.Register("Arith", &arith, "") if err != nil { t.Fatal(err) } pretty.Println(r.Services["Arith"].String()) } type Args struct { Aa int B string C bool } func Test_generateJSON(t *testing.T) { argsType := reflect.TypeOf(&Args{}).Elem() jsonData := generateJSON(argsType) assert.Equal(t, `{"Aa":0,"B":"","C":false}`, jsonData) def := generateTypeDefination("Args", "test", jsonData) result := "type Args struct {\n\tAa int64 \n\tB string \n\tC bool \n}\n" assert.Equal(t, result, def) } ================================================ FILE: server/context.go ================================================ package server import ( "fmt" "net" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" ) // Context represents a rpcx FastCall context. type Context struct { conn net.Conn req *protocol.Message ctx *share.Context async bool } // NewContext creates a server.Context for Handler. func NewContext(ctx *share.Context, conn net.Conn, req *protocol.Message, async bool) *Context { return &Context{conn: conn, req: req, ctx: ctx, async: async} } // Get returns value for key. func (ctx *Context) Get(key interface{}) interface{} { return ctx.ctx.Value(key) } // SetValue sets the kv pair. func (ctx *Context) SetValue(key, val interface{}) { if key == nil || val == nil { return } ctx.ctx.SetValue(key, val) } // DeleteKey delete the kv pair by key. func (ctx *Context) DeleteKey(key interface{}) { if ctx.ctx == nil || key == nil { return } ctx.ctx.DeleteKey(key) } // Payload returns the payload. func (ctx *Context) Payload() []byte { return ctx.req.Payload } // Metadata returns the metadata. func (ctx *Context) Metadata() map[string]string { return ctx.req.Metadata } // ServicePath returns the ServicePath. func (ctx *Context) ServicePath() string { return ctx.req.ServicePath } // ServiceMethod returns the ServiceMethod. func (ctx *Context) ServiceMethod() string { return ctx.req.ServiceMethod } // Bind parses the body data and stores the result to v. func (ctx *Context) Bind(v interface{}) error { req := ctx.req if v != nil { codec := share.Codecs[req.SerializeType()] if codec == nil { return fmt.Errorf("can not find codec for %d", req.SerializeType()) } err := codec.Decode(req.Payload, v) if err != nil { return err } } return nil } func (ctx *Context) Write(v interface{}) error { req := ctx.req if req.IsOneway() { // no need to send response return nil } codec := share.Codecs[req.SerializeType()] if codec == nil { return fmt.Errorf("can not find codec for %d", req.SerializeType()) } res := req.Clone() res.SetMessageType(protocol.Response) if v != nil { data, err := codec.Encode(v) if err != nil { return err } res.Payload = data } resMetadata := ctx.Get(share.ResMetaDataKey) if resMetadata != nil { resMetaInCtx := resMetadata.(map[string]string) meta := res.Metadata if meta == nil { res.Metadata = resMetaInCtx } else { for k, v := range resMetaInCtx { if meta[k] == "" { meta[k] = v } } } } if len(res.Payload) > 1024 && req.CompressType() != protocol.None { res.SetCompressType(req.CompressType()) } respData := res.EncodeSlicePointer() var err error if ctx.async { go func() { _, err = ctx.conn.Write(*respData) protocol.PutData(respData) }() } else { _, err = ctx.conn.Write(*respData) protocol.PutData(respData) } return err } func (ctx *Context) WriteError(err error) error { req := ctx.req if req.IsOneway() { // no need to send response return nil } codec := share.Codecs[req.SerializeType()] if codec == nil { return fmt.Errorf("can not find codec for %d", req.SerializeType()) } res := req.Clone() res.SetMessageType(protocol.Response) resMetadata := ctx.Get(share.ResMetaDataKey) if resMetadata != nil { resMetaInCtx := resMetadata.(map[string]string) meta := res.Metadata if meta == nil { res.Metadata = resMetaInCtx } else { for k, v := range resMetaInCtx { if meta[k] == "" { meta[k] = v } } } } res.SetMessageStatusType(protocol.Error) res.Metadata[protocol.ServiceError] = err.Error() respData := res.EncodeSlicePointer() ctx.conn.Write(*respData) protocol.PutData(respData) return nil } ================================================ FILE: server/converter.go ================================================ package server import ( "io" "net/http" "net/url" "strconv" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" ) const ( XVersion = "X-RPCX-Version" XMessageType = "X-RPCX-MessageType" XHeartbeat = "X-RPCX-Heartbeat" XOneway = "X-RPCX-Oneway" XMessageStatusType = "X-RPCX-MessageStatusType" XSerializeType = "X-RPCX-SerializeType" XCompressType = "X-RPCX-CompressType" XMessageID = "X-RPCX-MessageID" XServicePath = "X-RPCX-ServicePath" XServiceMethod = "X-RPCX-ServiceMethod" XMeta = "X-RPCX-Meta" XErrorMessage = "X-RPCX-ErrorMessage" ) // HTTPRequest2RpcxRequest converts a http request to a rpcx request. func HTTPRequest2RpcxRequest(r *http.Request) (*protocol.Message, error) { req := protocol.NewMessage() req.SetMessageType(protocol.Request) h := r.Header seq := h.Get(XMessageID) if seq != "" { id, err := strconv.ParseUint(seq, 10, 64) if err != nil { return nil, err } req.SetSeq(id) } heartbeat := h.Get(XHeartbeat) if heartbeat != "" { req.SetHeartbeat(true) } oneway := h.Get(XOneway) if oneway != "" { req.SetOneway(true) } st := h.Get(XSerializeType) if st != "" { rst, err := strconv.Atoi(st) if err != nil { return nil, err } req.SetSerializeType(protocol.SerializeType(rst)) } compressType := h.Get(XCompressType) if compressType != "" { ct, err := strconv.Atoi(compressType) if err != nil { return nil, err } req.SetCompressType(protocol.CompressType(ct)) } meta := h.Get(XMeta) if meta != "" { metadata, err := url.ParseQuery(meta) if err != nil { return nil, err } mm := make(map[string]string) for k, v := range metadata { if len(v) > 0 { mm[k] = v[0] } } req.Metadata = mm } auth := h.Get("Authorization") if auth != "" { if req.Metadata == nil { req.Metadata = make(map[string]string) } req.Metadata[share.AuthKey] = auth } req.ServicePath = h.Get(XServicePath) req.ServiceMethod = h.Get(XServiceMethod) payload, err := io.ReadAll(r.Body) if err != nil { return nil, err } req.Payload = payload return req, nil } // func RpcxResponse2HttpResponse(res *protocol.Message) (url.Values, []byte, error) { // m := make(url.Values) // m.Set(XVersion, strconv.Itoa(int(res.Version()))) // if res.IsHeartbeat() { // m.Set(XHeartbeat, "true") // } // if res.IsOneway() { // m.Set(XOneway, "true") // } // if res.MessageStatusType() == protocol.Error { // m.Set(XMessageStatusType, "Error") // } else { // m.Set(XMessageStatusType, "Normal") // } // if res.CompressType() == protocol.Gzip { // m.Set("Content-Encoding", "gzip") // } // m.Set(XSerializeType, strconv.Itoa(int(res.SerializeType()))) // m.Set(XMessageID, strconv.FormatUint(res.Seq(), 10)) // m.Set(XServicePath, res.ServicePath) // m.Set(XServiceMethod, res.ServiceMethod) // return m, res.Payload, nil // } ================================================ FILE: server/converter_test.go ================================================ package server import ( "bytes" "net/http" "testing" "github.com/smallnest/rpcx/codec" "github.com/smallnest/rpcx/share" "github.com/stretchr/testify/assert" ) func TestHTTPRequest2RpcxRequest(t *testing.T) { cc := &codec.MsgpackCodec{} args := &Args{ A: 10, B: 20, } data, _ := cc.Encode(args) req, err := http.NewRequest("POST", "http://127.0.0.1:8972/", bytes.NewReader(data)) if err != nil { t.Fatal("failed to create request: ", err) return } h := req.Header h.Set(XMessageID, "10000") h.Set(XHeartbeat, "0") h.Set(XOneway, "0") h.Set(XSerializeType, "3") h.Set(XMeta, "Meta") h.Set("Authorization", "Authorization") h.Set(XServicePath, "ProxyServer") h.Set(XServiceMethod, "GetAdData") rpcxReq, err := HTTPRequest2RpcxRequest(req) if err != nil { t.Fatal("HTTPRequest2RpcxRequest() error") } assert.NotNil(t, rpcxReq.Metadata) assert.Equal(t, h.Get("Authorization"), rpcxReq.Metadata[share.AuthKey]) assert.Equal(t, h.Get(XServicePath), rpcxReq.ServicePath) assert.Equal(t, h.Get(XServiceMethod), rpcxReq.ServiceMethod) } ================================================ FILE: server/file_transfer.go ================================================ package server import ( "context" "crypto/rand" "io" "net" "sync" "time" lru "github.com/hashicorp/golang-lru" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/share" ) // FileTransferHandler handles uploading file. Must close the connection after it finished. type FileTransferHandler func(conn net.Conn, args *share.FileTransferArgs) // DownloadFileHandler handles downloading file. Must close the connection after it finished. type DownloadFileHandler func(conn net.Conn, args *share.DownloadFileArgs) type tokenInfo struct { token []byte args *share.FileTransferArgs } type downloadTokenInfo struct { token []byte args *share.DownloadFileArgs } // FileTransfer support transfer files from clients. // It registers a file transfer service and listens a on the given port. // Clients will invokes this service to get the token and send the token and the file to this port. type FileTransfer struct { Addr string AdvertiseAddr string handler FileTransferHandler downloadFileHandler DownloadFileHandler cachedTokens *lru.Cache service *FileTransferService startOnce sync.Once ln net.Listener done chan struct{} } type FileTransferService struct { FileTransfer *FileTransfer } // NewFileTransfer creates a FileTransfer with given parameters. func NewFileTransfer(addr string, handler FileTransferHandler, downloadFileHandler DownloadFileHandler, waitNum int) *FileTransfer { cachedTokens, _ := lru.New(waitNum) fi := &FileTransfer{ Addr: addr, handler: handler, downloadFileHandler: downloadFileHandler, cachedTokens: cachedTokens, } fi.service = &FileTransferService{ FileTransfer: fi, } return fi } // EnableFileTransfer supports filetransfer service in this server. func (s *Server) EnableFileTransfer(serviceName string, fileTransfer *FileTransfer) { if serviceName == "" { serviceName = share.SendFileServiceName } _ = fileTransfer.Start() _ = s.RegisterName(serviceName, fileTransfer.service, "") } func (s *FileTransferService) TransferFile(ctx context.Context, args *share.FileTransferArgs, reply *share.FileTransferReply) error { token := make([]byte, 32) _, err := rand.Read(token) if err != nil { return err } *reply = share.FileTransferReply{ Token: token, Addr: s.FileTransfer.Addr, } if s.FileTransfer.AdvertiseAddr != "" { reply.Addr = s.FileTransfer.AdvertiseAddr } cloned := args.Clone() s.FileTransfer.cachedTokens.Add(string(token), &tokenInfo{token, cloned}) return nil } func (s *FileTransferService) DownloadFile(ctx context.Context, args *share.DownloadFileArgs, reply *share.FileTransferReply) error { token := make([]byte, 32) _, err := rand.Read(token) if err != nil { return err } *reply = share.FileTransferReply{ Token: token, Addr: s.FileTransfer.Addr, } if s.FileTransfer.AdvertiseAddr != "" { reply.Addr = s.FileTransfer.AdvertiseAddr } cloned := args.Clone() s.FileTransfer.cachedTokens.Add(string(token), &downloadTokenInfo{token, cloned}) return nil } func (s *FileTransfer) Start() error { s.startOnce.Do(func() { go s.start() }) return nil } func (s *FileTransfer) start() error { ln, err := net.Listen("tcp", s.Addr) if err != nil { return err } s.ln = ln var tempDelay time.Duration for { select { case <-s.done: return nil default: conn, e := ln.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Errorf("filetransfer: accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 if tc, ok := conn.(*net.TCPConn); ok { tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(3 * time.Minute) tc.SetLinger(10) } token := make([]byte, 32) _, err := io.ReadFull(conn, token) if err != nil { conn.Close() log.Errorf("failed to read token from %s", conn.RemoteAddr().String()) continue } tokenStr := string(token) info, ok := s.cachedTokens.Get(tokenStr) if !ok { conn.Close() log.Errorf("failed to read token from %s", conn.RemoteAddr().String()) continue } s.cachedTokens.Remove(tokenStr) switch ti := info.(type) { case *tokenInfo: if s.handler == nil { conn.Close() continue } go s.handler(conn, ti.args) case *downloadTokenInfo: if s.downloadFileHandler == nil { conn.Close() continue } go s.downloadFileHandler(conn, ti.args) default: conn.Close() } } } } func (s *FileTransfer) Stop() error { close(s.done) // notify Accept() to return if s.ln != nil { s.ln.Close() } return nil } ================================================ FILE: server/gateway.go ================================================ package server import ( "bufio" "context" "errors" "fmt" "io" "net" "net/http" "net/url" "strings" "time" "github.com/julienschmidt/httprouter" "github.com/rs/cors" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" "github.com/soheilhy/cmux" ) func (s *Server) startGateway(network string, ln net.Listener) net.Listener { if network != "tcp" && network != "tcp4" && network != "tcp6" && network != "reuseport" { // log.Infof("network is not tcp/tcp4/tcp6 so can not start gateway") return ln } m := cmux.New(ln) rpcxLn := m.Match(rpcxPrefixByteMatcher()) // mux Plugins if s.Plugins != nil { s.Plugins.MuxMatch(m) } if !s.DisableJSONRPC { jsonrpc2Ln := m.Match(cmux.HTTP1HeaderField("X-JSONRPC-2.0", "true")) go s.startJSONRPC2(jsonrpc2Ln) } if !s.DisableHTTPGateway { httpLn := m.Match(cmux.HTTP1Fast()) // X-RPCX-MessageID go s.startHTTP1APIGateway(httpLn) } go m.Serve() return rpcxLn } func http1Path(prefix string) cmux.Matcher { return func(r io.Reader) bool { return matchHTTP1Field(r, prefix, func(gotValue string) bool { br := bufio.NewReader(&io.LimitedReader{R: r, N: 1024}) l, part, err := br.ReadLine() if err != nil || part { return false } _, uri, _, ok := parseRequestLine(string(l)) if !ok { return false } if strings.HasPrefix(uri, prefix) { return true } u, err := url.Parse(uri) if err != nil { return false } return strings.HasPrefix(u.Path, prefix) }) } } // grabbed from net/http. func parseRequestLine(line string) (method, uri, proto string, ok bool) { s1 := strings.Index(line, " ") s2 := strings.Index(line[s1+1:], " ") if s1 < 0 || s2 < 0 { return } s2 += s1 + 1 return line[:s1], line[s1+1 : s2], line[s2+1:], true } func http1HeaderExist(name string) cmux.Matcher { return func(r io.Reader) bool { return matchHTTP1Field(r, name, func(gotValue string) bool { req, err := http.ReadRequest(bufio.NewReader(r)) if err != nil { return false } return req.Header.Get(name) != "" }) } } func matchHTTP1Field(r io.Reader, name string, matches func(string) bool) (matched bool) { req, err := http.ReadRequest(bufio.NewReader(r)) if err != nil { return false } return matches(req.Header.Get(name)) } func rpcxPrefixByteMatcher() cmux.Matcher { magic := protocol.MagicNumber() return func(r io.Reader) bool { buf := make([]byte, 1) n, _ := r.Read(buf) return n == 1 && buf[0] == magic } } func (s *Server) startHTTP1APIGateway(ln net.Listener) { router := httprouter.New() router.POST("/*servicePath", s.handleGatewayRequest) router.GET("/*servicePath", s.handleGatewayRequest) router.PUT("/*servicePath", s.handleGatewayRequest) if s.corsOptions != nil { opt := cors.Options(*s.corsOptions) c := cors.New(opt) mux := c.Handler(router) s.mu.Lock() s.gatewayHTTPServer = &http.Server{Handler: mux} s.mu.Unlock() } else { s.mu.Lock() s.gatewayHTTPServer = &http.Server{Handler: router} s.mu.Unlock() } if err := s.gatewayHTTPServer.Serve(ln); err != nil { if errors.Is(err, ErrServerClosed) || errors.Is(err, cmux.ErrListenerClosed) || errors.Is(err, cmux.ErrServerClosed) { log.Info("gateway server closed") } else { log.Warnf("error in gateway Serve: %T %s", err, err) } } } func (s *Server) closeHTTP1APIGateway(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() if s.gatewayHTTPServer != nil { return s.gatewayHTTPServer.Shutdown(ctx) } return nil } func (s *Server) handleGatewayRequest(w http.ResponseWriter, r *http.Request, params httprouter.Params) { ctx := share.WithValue(r.Context(), RemoteConnContextKey, r.RemoteAddr) // notice: It is a string, different with TCP (net.Conn) err := s.Plugins.DoPreReadRequest(ctx) if err != nil { http.Error(w, err.Error(), 500) return } err = s.Plugins.DoPostHTTPRequest(ctx, r, params) if err != nil { http.Error(w, err.Error(), 500) return } if r.Header.Get(XServicePath) == "" { servicePath := params.ByName("servicePath") servicePath = strings.TrimPrefix(servicePath, "/") r.Header.Set(XServicePath, servicePath) } servicePath := r.Header.Get(XServicePath) wh := w.Header() req, err := HTTPRequest2RpcxRequest(r) // set headers wh.Set(XVersion, r.Header.Get(XVersion)) wh.Set(XMessageID, r.Header.Get(XMessageID)) if err == nil && servicePath == "" { err = errors.New("empty servicepath") } else { wh.Set(XServicePath, servicePath) } if err == nil && r.Header.Get(XServiceMethod) == "" { err = errors.New("empty servicemethod") } else { wh.Set(XServiceMethod, r.Header.Get(XServiceMethod)) } if err == nil && r.Header.Get(XSerializeType) == "" { err = errors.New("empty serialized type") } else { wh.Set(XSerializeType, r.Header.Get(XSerializeType)) } if err != nil { rh := r.Header for k, v := range rh { if strings.HasPrefix(k, "X-RPCX-") && len(v) > 0 { wh.Set(k, v[0]) } } wh.Set(XMessageStatusType, "Error") wh.Set(XErrorMessage, err.Error()) return } err = s.Plugins.DoPostReadRequest(ctx, req, nil) if err != nil { s.Plugins.DoPreWriteResponse(ctx, req, nil, err) http.Error(w, err.Error(), 500) s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err) return } ctx.SetValue(StartRequestContextKey, time.Now().UnixNano()) err = s.auth(ctx, req) if err != nil { s.Plugins.DoPreWriteResponse(ctx, req, nil, err) wh.Set(XMessageStatusType, "Error") wh.Set(XErrorMessage, err.Error()) w.WriteHeader(401) s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err) return } resMetadata := make(map[string]string) newCtx := share.WithLocalValue(share.WithLocalValue(ctx, share.ReqMetaDataKey, req.Metadata), share.ResMetaDataKey, resMetadata) res, err := s.handleRequest(newCtx, req) if err != nil { // call DoPreWriteResponse s.Plugins.DoPreWriteResponse(ctx, req, nil, err) if s.HandleServiceError != nil { s.HandleServiceError(err) } else { log.Warnf("rpcx: gateway request: %v", err) } wh.Set(XMessageStatusType, "Error") wh.Set(XErrorMessage, err.Error()) w.WriteHeader(500) // call DoPostWriteResponse s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err) return } // will set res to call s.Plugins.DoPreWriteResponse(newCtx, req, res, nil) if len(resMetadata) > 0 { // copy meta in context to request meta := res.Metadata if meta == nil { res.Metadata = resMetadata } else { for k, v := range resMetadata { meta[k] = v } } } meta := url.Values{} for k, v := range res.Metadata { meta.Add(k, v) } wh.Set(XMeta, meta.Encode()) if res.CompressType() != protocol.None { wh.Set(XCompressType, fmt.Sprintf("%d", res.CompressType())) } w.Write(res.Payload) s.Plugins.DoPostWriteResponse(newCtx, req, res, err) } ================================================ FILE: server/jsonrpc2.go ================================================ package server import ( "context" "encoding/json" "errors" "io" "net" "net/http" "net/url" "strings" "github.com/rs/cors" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" "github.com/soheilhy/cmux" ) func (s *Server) jsonrpcHandler(w http.ResponseWriter, r *http.Request) { data, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var req = &jsonrpcRequest{} err = json.Unmarshal(data, req) if err != nil { var res = &jsonrpcRespone{} res.Error = &JSONRPCError{ Code: CodeParseJSONRPCError, Message: err.Error(), } writeResponse(w, res) return } conn := r.Context().Value(HttpConnContextKey).(net.Conn) ctx := share.WithValue(r.Context(), RemoteConnContextKey, conn) if req.ID != nil { res := s.handleJSONRPCRequest(ctx, req, r.Header) writeResponse(w, res) return } // notification go s.handleJSONRPCRequest(ctx, req, r.Header) } func (s *Server) handleJSONRPCRequest(ctx context.Context, r *jsonrpcRequest, header http.Header) *jsonrpcRespone { s.Plugins.DoPreReadRequest(ctx) var res = &jsonrpcRespone{} res.ID = r.ID req := protocol.NewMessage() if req.Metadata == nil { req.Metadata = make(map[string]string) } if r.ID == nil { req.SetOneway(true) } req.SetMessageType(protocol.Request) req.SetSerializeType(protocol.JSON) lastDot := strings.LastIndex(r.Method, ".") if lastDot <= 0 { res.Error = &JSONRPCError{ Code: CodeMethodNotFound, Message: "must contains servicepath and method", } return res } req.ServicePath = r.Method[:lastDot] req.ServiceMethod = r.Method[lastDot+1:] req.Payload = *r.Params // meta meta := header.Get(XMeta) if meta != "" { metadata, _ := url.ParseQuery(meta) for k, v := range metadata { if len(v) > 0 { req.Metadata[k] = v[0] } } } auth := header.Get("Authorization") if auth != "" { req.Metadata[share.AuthKey] = auth } err := s.Plugins.DoPostReadRequest(ctx, req, nil) if err != nil { res.Error = &JSONRPCError{ Code: CodeInternalJSONRPCError, Message: err.Error(), } return res } err = s.auth(ctx, req) if err != nil { s.Plugins.DoPreWriteResponse(ctx, req, nil, err) res.Error = &JSONRPCError{ Code: CodeInternalJSONRPCError, Message: err.Error(), } s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err) return res } resp, err := s.handleRequest(ctx, req) if r.ID == nil { return nil } s.Plugins.DoPreWriteResponse(ctx, req, nil, err) if err != nil { res.Error = &JSONRPCError{ Code: CodeInternalJSONRPCError, Message: err.Error(), } s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err) return res } result := json.RawMessage(resp.Payload) res.Result = &result s.Plugins.DoPostWriteResponse(ctx, req, req.Clone(), err) return res } func writeResponse(w http.ResponseWriter, res *jsonrpcRespone) { data, err := json.Marshal(res) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Context-Type", "application/json") w.Write(data) } type CORSOptions = cors.Options // AllowAllCORSOptions returns a option that allows access. func AllowAllCORSOptions() *CORSOptions { return &CORSOptions{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{ http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, }, AllowedHeaders: []string{"*"}, AllowCredentials: false, } } // SetCORS sets CORS options. // for example: // // cors.Options{ // AllowedOrigins: []string{"foo.com"}, // AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete}, // AllowCredentials: true, // } func (s *Server) SetCORS(options *CORSOptions) { s.corsOptions = options } func (s *Server) startJSONRPC2(ln net.Listener) { newServer := http.NewServeMux() newServer.HandleFunc("/", s.jsonrpcHandler) srv := http.Server{ConnContext: func(ctx context.Context, c net.Conn) context.Context { return context.WithValue(ctx, HttpConnContextKey, c) }} if s.corsOptions != nil { opt := cors.Options(*s.corsOptions) c := cors.New(opt) mux := c.Handler(newServer) srv.Handler = mux } else { srv.Handler = newServer } s.jsonrpcHTTPServerLock.Lock() s.jsonrpcHTTPServer = &srv s.jsonrpcHTTPServerLock.Unlock() if err := s.jsonrpcHTTPServer.Serve(ln); !errors.Is(err, cmux.ErrServerClosed) { log.Errorf("error in JSONRPC server: %T %s", err, err) } } func (s *Server) closeJSONRPC2(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() if s.jsonrpcHTTPServer != nil { return s.jsonrpcHTTPServer.Shutdown(ctx) } return nil } ================================================ FILE: server/jsonrpc2_wire.go ================================================ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package server import ( "encoding/json" "fmt" "strconv" ) // this file contains the go forms of the wire specification // see http://www.jsonrpc.org/specification for details const ( // CodeUnknownJSONRPCError should be used for all non coded errors. CodeUnknownJSONRPCError = -32001 // CodeParseJSONRPCError is used when invalid JSON was received by the server. CodeParseJSONRPCError = -32700 // CodeInvalidjsonrpcRequest is used when the JSON sent is not a valid jsonrpcRequest object. CodeInvalidjsonrpcRequest = -32600 // CodeMethodNotFound should be returned by the handler when the method does // not exist / is not available. CodeMethodNotFound = -32601 // CodeInvalidParams should be returned by the handler when method // parameter(s) were invalid. CodeInvalidParams = -32602 // CodeInternalJSONRPCError is not currently returned but defined for completeness. CodeInternalJSONRPCError = -32603 ) // jsonrpcRequest is sent to a server to represent a Call or Notify operation. type jsonrpcRequest struct { // VersionTag is always encoded as the string "2.0" VersionTag VersionTag `json:"jsonrpc"` // Method is a string containing the method name to invoke. Method string `json:"method"` // Params is either a struct or an array with the parameters of the method. Params *json.RawMessage `json:"params,omitempty"` // The id of this request, used to tie the jsonrpcRespone back to the request. // Will be either a string or a number. If not set, the jsonrpcRequest is a notify, // and no response is possible. ID *ID `json:"id,omitempty"` } // jsonrpcRespone is a reply to a jsonrpcRequest. // It will always have the ID field set to tie it back to a request, and will // have either the Result or JSONRPCError fields set depending on whether it is a // success or failure response. type jsonrpcRespone struct { // VersionTag is always encoded as the string "2.0" VersionTag VersionTag `json:"jsonrpc"` // Result is the response value, and is required on success. Result *json.RawMessage `json:"result,omitempty"` // Error is a structured error response if the call fails. Error *JSONRPCError `json:"error,omitempty"` // ID must be set and is the identifier of the jsonrpcRequest this is a response to. ID *ID `json:"id,omitempty"` } // JSONRPCError represents a structured error in a jsonrpcRespone. type JSONRPCError struct { // Code is an error code indicating the type of failure. Code int64 `json:"code"` // Message is a short description of the error. Message string `json:"message"` // Data is optional structured data containing additional information about the error. Data *json.RawMessage `json:"data"` } // VersionTag is a special 0 sized struct that encodes as the jsonrpc version // tag. // It will fail during decode if it is not the correct version tag in the // stream. type VersionTag struct{} // ID is a jsonrpcRequest identifier. // Only one of either the Name or Number members will be set, using the // number form if the Name is the empty string. type ID struct { Name string Number int64 } // IsNotify returns true if this request is a notification. func (r *jsonrpcRequest) IsNotify() bool { return r.ID == nil } func (err *JSONRPCError) JSONRPCError() string { if err == nil { return "" } return err.Message } func (VersionTag) MarshalJSON() ([]byte, error) { return json.Marshal("2.0") } func (VersionTag) UnmarshalJSON(data []byte) error { version := "" if err := json.Unmarshal(data, &version); err != nil { return err } if version != "2.0" { return fmt.Errorf("invalid RPC version %v", version) } return nil } // String returns a string representation of the ID. // The representation is non ambiguous, string forms are quoted, number forms // are preceded by a # func (id *ID) String() string { if id == nil { return "" } if id.Name != "" { return strconv.Quote(id.Name) } return "#" + strconv.FormatInt(id.Number, 10) } func (id *ID) MarshalJSON() ([]byte, error) { if id.Name != "" { return json.Marshal(id.Name) } return json.Marshal(id.Number) } func (id *ID) UnmarshalJSON(data []byte) error { *id = ID{} if err := json.Unmarshal(data, &id.Number); err == nil { return nil } return json.Unmarshal(data, &id.Name) } ================================================ FILE: server/kcp.go ================================================ // +build kcp package server import ( "errors" "net" kcp "github.com/xtaci/kcp-go" ) func init() { makeListeners["kcp"] = kcpMakeListener } func kcpMakeListener(s *Server, address string) (ln net.Listener, err error) { if s.options == nil || s.options["BlockCrypt"] == nil { return nil, errors.New("KCP BlockCrypt must be configured in server.Options") } return kcp.ListenWithOptions(address, s.options["BlockCrypt"].(kcp.BlockCrypt), 10, 3) } // WithBlockCrypt sets kcp.BlockCrypt. func WithBlockCrypt(bc kcp.BlockCrypt) OptionFn { return func(s *Server) { s.options["BlockCrypt"] = bc } } ================================================ FILE: server/listener.go ================================================ package server import ( "crypto/tls" "errors" "fmt" "net" ) var makeListeners = make(map[string]MakeListener) func init() { makeListeners["tcp"] = tcpMakeListener("tcp") makeListeners["tcp4"] = tcpMakeListener("tcp4") makeListeners["tcp6"] = tcpMakeListener("tcp6") makeListeners["http"] = tcpMakeListener("tcp") makeListeners["ws"] = tcpMakeListener("tcp") makeListeners["wss"] = tcpMakeListener("tcp") } // RegisterMakeListener registers a MakeListener for network. func RegisterMakeListener(network string, ml MakeListener) { makeListeners[network] = ml } // MakeListener defines a listener generator. type MakeListener func(s *Server, address string) (ln net.Listener, err error) // block can be nil if the caller wishes to skip encryption in kcp. // tlsConfig can be nil iff we are not using network "quic". func (s *Server) makeListener(network, address string) (ln net.Listener, err error) { ml := makeListeners[network] if ml == nil { return nil, fmt.Errorf("can not make listener for %s", network) } if network == "wss" && s.tlsConfig == nil { return nil, errors.New("must set tlsconfig for wss") } return ml(s, address) } func tcpMakeListener(network string) MakeListener { return func(s *Server, address string) (ln net.Listener, err error) { if s.tlsConfig == nil { ln, err = net.Listen(network, address) } else { ln, err = tls.Listen(network, address, s.tlsConfig) } return ln, err } } ================================================ FILE: server/listener_linux.go ================================================ //go:build linux // +build linux package server import ( "net" "runtime" "time" uringnet "github.com/godzie44/go-uring/net" "github.com/godzie44/go-uring/reactor" "github.com/godzie44/go-uring/uring" "github.com/smallnest/rpcx/log" ) func init() { makeListeners["iouring"] = iouringMakeListener } // iouringMakeListener creates a new listener using io_uring. // You can use RegisterMakeListener to register a customized iouring Listener creator. // experimental func iouringMakeListener(s *Server, address string) (ln net.Listener, err error) { n := runtime.GOMAXPROCS(-1) var opts []uring.SetupOption opts = append(opts, uring.WithSQPoll(time.Millisecond*100)) rings, closeRings, err := uring.CreateMany(n, uring.MaxEntries>>3, n, opts...) if err != nil { return nil, err } netReactor, err := reactor.NewNet(rings, reactor.WithLogger(&uringLogger{log.GetLogger()})) if err != nil { return nil, err } ln, err = uringnet.NewListener(net.ListenConfig{}, address, netReactor) if err != nil { return nil, err } return &uringnetListener{Listener: ln, closeRings: closeRings}, nil } type uringnetListener struct { net.Listener closeRings uring.Defer } func (cl *uringnetListener) Close() error { cl.closeRings() cl.Listener.Close() return nil } type uringLogger struct { log.Logger } func (l *uringLogger) Log(keyvals ...interface{}) { l.Logger.Info(keyvals...) } ================================================ FILE: server/listener_rdma.go ================================================ //go:build rdma // +build rdma package server import ( "net" "os" "strconv" "github.com/smallnest/rsocket" ) func init() { makeListeners["rdma"] = rdmaMakeListener } func rdmaMakeListener(s *Server, address string) (ln net.Listener, err error) { host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } p, err := strconv.Atoi(port) if err != nil { return nil, err } backlog := os.Getenv("RDMA_BACKLOG") if backlog == "" { backlog = "128" } blog, _ := strconv.Atoi(backlog) if blog == 0 { blog = 128 } return rsocket.NewTCPListener(host, p, blog) } ================================================ FILE: server/listener_unix.go ================================================ //go:build !windows // +build !windows package server import ( "net" reuseport "github.com/kavu/go_reuseport" ) func init() { makeListeners["reuseport"] = reuseportMakeListener makeListeners["unix"] = unixMakeListener } func reuseportMakeListener(s *Server, address string) (ln net.Listener, err error) { var network string if validIP6(address) { network = "tcp6" } else { network = "tcp4" } return reuseport.NewReusablePortListener(network, address) } func unixMakeListener(s *Server, address string) (ln net.Listener, err error) { laddr, err := net.ResolveUnixAddr("unix", address) if err != nil { return nil, err } return net.ListenUnix("unix", laddr) } ================================================ FILE: server/memconn.go ================================================ package server import ( "net" "github.com/akutz/memconn" ) func init() { makeListeners["memu"] = memconnMakeListener } func memconnMakeListener(s *Server, address string) (ln net.Listener, err error) { return memconn.Listen("memu", address) } ================================================ FILE: server/option.go ================================================ package server import ( "crypto/tls" "time" "github.com/alitto/pond" ) // OptionFn configures options of server. type OptionFn func(*Server) // // WithOptions sets multiple options. // func WithOptions(ops map[string]interface{}) OptionFn { // return func(s *Server) { // for k, v := range ops { // s.options[k] = v // } // } // } // WithTLSConfig sets tls.Config. func WithTLSConfig(cfg *tls.Config) OptionFn { return func(s *Server) { s.tlsConfig = cfg } } // WithReadTimeout sets readTimeout. func WithReadTimeout(readTimeout time.Duration) OptionFn { return func(s *Server) { s.readTimeout = readTimeout } } // WithWriteTimeout sets writeTimeout. func WithWriteTimeout(writeTimeout time.Duration) OptionFn { return func(s *Server) { s.writeTimeout = writeTimeout } } // WithPool sets goroutine pool. func WithPool(maxWorkers, maxCapacity int, options ...pond.Option) OptionFn { return func(s *Server) { s.pool = pond.New(maxWorkers, maxCapacity, options...) } } // WithCustomPool uses a custom goroutine pool. func WithCustomPool(pool WorkerPool) OptionFn { return func(s *Server) { s.pool = pool } } // WithAsyncWrite sets AsyncWrite to true. func WithAsyncWrite() OptionFn { return func(s *Server) { s.AsyncWrite = true } } ================================================ FILE: server/option_test.go ================================================ package server import ( "crypto/tls" "testing" "time" "github.com/stretchr/testify/assert" ) func TestOption(t *testing.T) { server := NewServer() cert, _ := tls.LoadX509KeyPair("server.pem", "server.key") config := &tls.Config{Certificates: []tls.Certificate{cert}} o := WithTLSConfig(config) o(server) assert.Equal(t, config, server.tlsConfig) o = WithReadTimeout(time.Second) o(server) assert.Equal(t, time.Second, server.readTimeout) o = WithWriteTimeout(time.Second) o(server) assert.Equal(t, time.Second, server.writeTimeout) } ================================================ FILE: server/options.go ================================================ package server import "time" // WithTCPKeepAlivePeriod sets tcp keepalive period. func WithTCPKeepAlivePeriod(period time.Duration) OptionFn { return func(s *Server) { s.options["TCPKeepAlivePeriod"] = period } } ================================================ FILE: server/plugin.go ================================================ package server import ( "context" "net" "net/http" "github.com/julienschmidt/httprouter" "github.com/smallnest/rpcx/errors" "github.com/smallnest/rpcx/protocol" "github.com/soheilhy/cmux" ) // PluginContainer represents a plugin container that defines all methods to manage plugins. // And it also defines all extension points. type PluginContainer interface { Add(plugin Plugin) Remove(plugin Plugin) All() []Plugin DoRegister(name string, rcvr interface{}, metadata string) error DoRegisterFunction(serviceName, fname string, fn interface{}, metadata string) error DoUnregister(name string) error DoPostConnAccept(net.Conn) (net.Conn, bool) DoPostConnClose(net.Conn) bool DoPreReadRequest(ctx context.Context) error DoPostReadRequest(ctx context.Context, r *protocol.Message, e error) error DoPostHTTPRequest(ctx context.Context, r *http.Request, params httprouter.Params) error DoPreHandleRequest(ctx context.Context, req *protocol.Message) error DoPreCall(ctx context.Context, serviceName, methodName string, args interface{}) (interface{}, error) DoPostCall(ctx context.Context, serviceName, methodName string, args, reply interface{}, err error) (interface{}, error) DoPreWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error DoPostWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error DoPreWriteRequest(ctx context.Context) error DoPostWriteRequest(ctx context.Context, r *protocol.Message, e error) error DoHeartbeatRequest(ctx context.Context, req *protocol.Message) error MuxMatch(m cmux.CMux) } // Plugin is the server plugin interface. type Plugin interface{} type ( // RegisterPlugin is . RegisterPlugin interface { Register(name string, rcvr interface{}, metadata string) error Unregister(name string) error } // RegisterFunctionPlugin is . RegisterFunctionPlugin interface { RegisterFunction(serviceName, fname string, fn interface{}, metadata string) error } // PostConnAcceptPlugin represents connection accept plugin. // if returns false, it means subsequent IPostConnAcceptPlugins should not continue to handle this conn // and this conn has been closed. PostConnAcceptPlugin interface { HandleConnAccept(net.Conn) (net.Conn, bool) } // PostConnClosePlugin represents client connection close plugin. PostConnClosePlugin interface { HandleConnClose(net.Conn) bool } // PreReadRequestPlugin represents . PreReadRequestPlugin interface { PreReadRequest(ctx context.Context) error } // PostReadRequestPlugin represents . PostReadRequestPlugin interface { PostReadRequest(ctx context.Context, r *protocol.Message, e error) error } // PostHTTPRequestPlugin represents . PostHTTPRequestPlugin interface { PostHTTPRequest(ctx context.Context, r *http.Request, params httprouter.Params) error } // PreHandleRequestPlugin represents . PreHandleRequestPlugin interface { PreHandleRequest(ctx context.Context, r *protocol.Message) error } PreCallPlugin interface { PreCall(ctx context.Context, serviceName, methodName string, args interface{}) (interface{}, error) } PostCallPlugin interface { PostCall(ctx context.Context, serviceName, methodName string, args, reply interface{}, err error) (interface{}, error) } // PreWriteResponsePlugin represents . PreWriteResponsePlugin interface { PreWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error } // PostWriteResponsePlugin represents . PostWriteResponsePlugin interface { PostWriteResponse(context.Context, *protocol.Message, *protocol.Message, error) error } // PreWriteRequestPlugin represents . PreWriteRequestPlugin interface { PreWriteRequest(ctx context.Context) error } // PostWriteRequestPlugin represents . PostWriteRequestPlugin interface { PostWriteRequest(ctx context.Context, r *protocol.Message, e error) error } // HeartbeatPlugin is . HeartbeatPlugin interface { HeartbeatRequest(ctx context.Context, req *protocol.Message) error } CMuxPlugin interface { MuxMatch(m cmux.CMux) } ) // pluginContainer implements PluginContainer interface. type pluginContainer struct { plugins []Plugin } // Add adds a plugin. func (p *pluginContainer) Add(plugin Plugin) { p.plugins = append(p.plugins, plugin) } // Remove removes a plugin by it's name. func (p *pluginContainer) Remove(plugin Plugin) { if p.plugins == nil { return } plugins := make([]Plugin, 0, len(p.plugins)) for _, p := range p.plugins { if p != plugin { plugins = append(plugins, p) } } p.plugins = plugins } func (p *pluginContainer) All() []Plugin { return p.plugins } // DoRegister invokes DoRegister plugin. func (p *pluginContainer) DoRegister(name string, rcvr interface{}, metadata string) error { var es []error for _, rp := range p.plugins { if plugin, ok := rp.(RegisterPlugin); ok { err := plugin.Register(name, rcvr, metadata) if err != nil { es = append(es, err) } } } if len(es) > 0 { return errors.NewMultiError(es) } return nil } // DoRegisterFunction invokes DoRegisterFunction plugin. func (p *pluginContainer) DoRegisterFunction(serviceName, fname string, fn interface{}, metadata string) error { var es []error for _, rp := range p.plugins { if plugin, ok := rp.(RegisterFunctionPlugin); ok { err := plugin.RegisterFunction(serviceName, fname, fn, metadata) if err != nil { es = append(es, err) } } } if len(es) > 0 { return errors.NewMultiError(es) } return nil } // DoUnregister invokes RegisterPlugin. func (p *pluginContainer) DoUnregister(name string) error { var es []error for _, rp := range p.plugins { if plugin, ok := rp.(RegisterPlugin); ok { err := plugin.Unregister(name) if err != nil { es = append(es, err) } } } if len(es) > 0 { return errors.NewMultiError(es) } return nil } // DoPostConnAccept handles accepted conn func (p *pluginContainer) DoPostConnAccept(conn net.Conn) (net.Conn, bool) { var flag bool for i := range p.plugins { if plugin, ok := p.plugins[i].(PostConnAcceptPlugin); ok { conn, flag = plugin.HandleConnAccept(conn) if !flag { // interrupt conn.Close() return conn, false } } } return conn, true } // DoPostConnClose handles closed conn func (p *pluginContainer) DoPostConnClose(conn net.Conn) bool { var flag bool for i := range p.plugins { if plugin, ok := p.plugins[i].(PostConnClosePlugin); ok { flag = plugin.HandleConnClose(conn) if !flag { return false } } } return true } // DoPreReadRequest invokes PreReadRequest plugin. func (p *pluginContainer) DoPreReadRequest(ctx context.Context) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PreReadRequestPlugin); ok { err := plugin.PreReadRequest(ctx) if err != nil { return err } } } return nil } // DoPostReadRequest invokes PostReadRequest plugin. func (p *pluginContainer) DoPostReadRequest(ctx context.Context, r *protocol.Message, e error) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PostReadRequestPlugin); ok { err := plugin.PostReadRequest(ctx, r, e) if err != nil { return err } } } return nil } // DoPostHTTPRequest invokes PostHTTPRequest plugin. func (p *pluginContainer) DoPostHTTPRequest(ctx context.Context, r *http.Request, params httprouter.Params) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PostHTTPRequestPlugin); ok { err := plugin.PostHTTPRequest(ctx, r, params) if err != nil { return err } } } return nil } // DoPreHandleRequest invokes PreHandleRequest plugin. func (p *pluginContainer) DoPreHandleRequest(ctx context.Context, r *protocol.Message) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PreHandleRequestPlugin); ok { err := plugin.PreHandleRequest(ctx, r) if err != nil { return err } } } return nil } // DoPreCall invokes PreCallPlugin plugin. func (p *pluginContainer) DoPreCall(ctx context.Context, serviceName, methodName string, args interface{}) (interface{}, error) { var err error for i := range p.plugins { if plugin, ok := p.plugins[i].(PreCallPlugin); ok { args, err = plugin.PreCall(ctx, serviceName, methodName, args) if err != nil { return args, err } } } return args, err } // DoPostCall invokes PostCallPlugin plugin. func (p *pluginContainer) DoPostCall(ctx context.Context, serviceName, methodName string, args, reply interface{}, err error) (interface{}, error) { var e error for i := range p.plugins { if plugin, ok := p.plugins[i].(PostCallPlugin); ok { reply, e = plugin.PostCall(ctx, serviceName, methodName, args, reply, err) if e != nil { return reply, e } } } return reply, e } // DoPreWriteResponse invokes PreWriteResponse plugin. func (p *pluginContainer) DoPreWriteResponse(ctx context.Context, req *protocol.Message, res *protocol.Message, err error) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PreWriteResponsePlugin); ok { err := plugin.PreWriteResponse(ctx, req, res, err) if err != nil { return err } } } return nil } // DoPostWriteResponse invokes PostWriteResponse plugin. func (p *pluginContainer) DoPostWriteResponse(ctx context.Context, req *protocol.Message, resp *protocol.Message, e error) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PostWriteResponsePlugin); ok { err := plugin.PostWriteResponse(ctx, req, resp, e) if err != nil { return err } } } return nil } // DoPreWriteRequest invokes PreWriteRequest plugin. func (p *pluginContainer) DoPreWriteRequest(ctx context.Context) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PreWriteRequestPlugin); ok { err := plugin.PreWriteRequest(ctx) if err != nil { return err } } } return nil } // DoPostWriteRequest invokes PostWriteRequest plugin. func (p *pluginContainer) DoPostWriteRequest(ctx context.Context, r *protocol.Message, e error) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(PostWriteRequestPlugin); ok { err := plugin.PostWriteRequest(ctx, r, e) if err != nil { return err } } } return nil } // DoHeartbeatRequest invokes HeartbeatRequest plugin. func (p *pluginContainer) DoHeartbeatRequest(ctx context.Context, r *protocol.Message) error { for i := range p.plugins { if plugin, ok := p.plugins[i].(HeartbeatPlugin); ok { err := plugin.HeartbeatRequest(ctx, r) if err != nil { return err } } } return nil } // MuxMatch adds cmux Match. func (p *pluginContainer) MuxMatch(m cmux.CMux) { for i := range p.plugins { if plugin, ok := p.plugins[i].(CMuxPlugin); ok { plugin.MuxMatch(m) } } } ================================================ FILE: server/plugin_test.go ================================================ package server import ( "context" "net" "sync" "testing" "time" "github.com/smallnest/rpcx/client" "github.com/smallnest/rpcx/protocol" ) type HeartbeatHandler struct{} func (h *HeartbeatHandler) HeartbeatRequest(ctx context.Context, req *protocol.Message) error { conn := ctx.Value(RemoteConnContextKey).(net.Conn) println("OnHeartbeat:", conn.RemoteAddr().String()) return nil } // TestPluginHeartbeat: go test -v -test.run TestPluginHeartbeat func TestPluginHeartbeat(t *testing.T) { h := &HeartbeatHandler{} s := NewServer( WithReadTimeout(time.Duration(5)*time.Second), WithWriteTimeout(time.Duration(5)*time.Second), ) s.Plugins.Add(h) s.RegisterName("Arith", new(Arith), "") wg := sync.WaitGroup{} wg.Add(2) go func() { // server defer wg.Done() err := s.Serve("tcp", "127.0.0.1:9001") if err != nil { t.Log(err.Error()) } }() go func() { // wait for server start complete time.Sleep(time.Second) defer wg.Done() // client opts := client.DefaultOption opts.Heartbeat = true opts.HeartbeatInterval = time.Second opts.IdleTimeout = time.Duration(5) * time.Second opts.ConnectTimeout = time.Duration(5) * time.Second // PeerDiscovery d, err := client.NewPeer2PeerDiscovery("tcp@127.0.0.1:9001", "") if err != nil { t.Fatalf("failed to NewPeer2PeerDiscovery: %v", err) } c := client.NewXClient("Arith", client.Failtry, client.RoundRobin, d, opts) i := 0 for { i++ resp := &Reply{} c.Call(context.Background(), "Mul", &Args{A: 1, B: 5}, resp) t.Log("call Mul resp:", resp.C) time.Sleep(time.Second) if i > 10 { break } } c.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() s.Shutdown(ctx) }() wg.Wait() } ================================================ FILE: server/pool.go ================================================ package server import ( "reflect" "sync" ) // Reset defines Reset method for pooled object. type Reset interface { Reset() } var reflectTypePools = &typePools{ pools: make(map[reflect.Type]*sync.Pool), New: func(t reflect.Type) interface{} { var argv reflect.Value if t.Kind() == reflect.Ptr { // reply must be ptr argv = reflect.New(t.Elem()) } else { argv = reflect.New(t) } return argv.Interface() }, } type typePools struct { mu sync.RWMutex pools map[reflect.Type]*sync.Pool New func(t reflect.Type) interface{} } func (p *typePools) Init(t reflect.Type) { tp := &sync.Pool{} tp.New = func() interface{} { return p.New(t) } p.mu.Lock() defer p.mu.Unlock() p.pools[t] = tp } func (p *typePools) Put(t reflect.Type, x interface{}) { if o, ok := x.(Reset); ok { o.Reset() p.mu.RLock() pool := p.pools[t] p.mu.RUnlock() pool.Put(x) } } func (p *typePools) Get(t reflect.Type) interface{} { p.mu.RLock() pool := p.pools[t] p.mu.RUnlock() return pool.Get() } ================================================ FILE: server/pool_test.go ================================================ package server import ( "reflect" "testing" "github.com/stretchr/testify/assert" ) type Elem struct { // magicNumber the Magic Number magicNumber int } func (e Elem) Reset() { } func TestPool(t *testing.T) { elem := Elem{42} elemType := reflect.TypeOf(elem) // init Elem pool reflectTypePools.Init(elemType) reflectTypePools.Put(elemType, elem) // Get() will remove element from pool assert.Equal(t, elem.magicNumber, reflectTypePools.Get(elemType).(Elem).magicNumber) } ================================================ FILE: server/quic.go ================================================ //go:build quic // +build quic package server import ( "errors" "net" "github.com/smallnest/quick" ) func init() { makeListeners["quic"] = quicMakeListener } func quicMakeListener(s *Server, address string) (ln net.Listener, err error) { if s.tlsConfig == nil { return nil, errors.New("TLSConfig must be configured in server.Options") } if len(s.tlsConfig.NextProtos) == 0 { s.tlsConfig.NextProtos = []string{"rpcx"} } return quick.Listen("udp", address, s.tlsConfig, nil) } ================================================ FILE: server/router.go ================================================ package server // UpdateHandler 批量更新router // 服务器使用plugin热更时,批量替换特定接口 func (s *Server) UpdateHandler(router map[string]Handler) { newRouter := make(map[string]Handler) for k, v := range s.router { newRouter[k] = v } for k, v := range router { newRouter[k] = v } s.router = newRouter } ================================================ FILE: server/server.go ================================================ package server import ( "bufio" "context" "crypto/tls" "errors" "fmt" "io" "net" "net/http" "os" "os/exec" "reflect" "regexp" "runtime" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" "github.com/soheilhy/cmux" "golang.org/x/net/websocket" ) // ErrServerClosed is returned by the Server's Serve, ListenAndServe after a call to Shutdown or Close. var ( ErrServerClosed = errors.New("http: Server closed") ErrReqReachLimit = errors.New("request reached rate limit") ) const ( // ReaderBuffsize is used for bufio reader. ReaderBuffsize = 1024 // WriterBuffsize is used for bufio writer. WriterBuffsize = 1024 // // WriteChanSize is used for response. // WriteChanSize = 1024 * 1024 ) // contextKey is a value for use with context.WithValue. It's used as // a pointer so it fits in an interface{} without allocation. type contextKey struct { name string } func (k *contextKey) String() string { return "rpcx context value " + k.name } var ( // RemoteConnContextKey is a context key. It can be used in // services with context.WithValue to access the connection arrived on. // The associated value will be of type net.Conn. RemoteConnContextKey = &contextKey{"remote-conn"} // StartRequestContextKey records the start time StartRequestContextKey = &contextKey{"start-parse-request"} // StartSendRequestContextKey records the start time StartSendRequestContextKey = &contextKey{"start-send-request"} // TagContextKey is used to record extra info in handling services. Its value is a map[string]interface{} TagContextKey = &contextKey{"service-tag"} // HttpConnContextKey is used to store http connection. HttpConnContextKey = &contextKey{"http-conn"} ) type Handler func(ctx *Context) error type WorkerPool interface { Submit(task func()) StopAndWaitFor(deadline time.Duration) Stop() context.Context StopAndWait() } // Server is rpcx server that use TCP or UDP. type Server struct { ln net.Listener readTimeout time.Duration writeTimeout time.Duration gatewayHTTPServer *http.Server jsonrpcHTTPServerLock sync.Mutex jsonrpcHTTPServer *http.Server DisableHTTPGateway bool // disable http invoke or not. DisableJSONRPC bool // disable json rpc or not. AsyncWrite bool // set true if your server only serves few clients pool WorkerPool serviceMapMu sync.RWMutex serviceMap map[string]*service router map[string]Handler mu sync.RWMutex activeConn map[net.Conn]struct{} doneChan chan struct{} seq atomic.Uint64 inShutdown int32 onShutdown []func(s *Server) onRestart []func(s *Server) // TLSConfig for creating tls tcp connection. tlsConfig *tls.Config // BlockCrypt for kcp.BlockCrypt options map[string]interface{} // CORS options corsOptions *CORSOptions Plugins PluginContainer // AuthFunc can be used to auth. AuthFunc func(ctx context.Context, req *protocol.Message, token string) error handlerMsgNum int32 requestCount atomic.Uint64 // HandleServiceError is used to get all service errors. You can use it write logs or others. HandleServiceError func(error) // ServerErrorFunc is a customized error handlers and you can use it to return customized error strings to clients. // If not set, it use err.Error() ServerErrorFunc func(res *protocol.Message, err error) string // The server is started. Started chan struct{} unregisterAllOnce sync.Once } // NewServer returns a server. func NewServer(options ...OptionFn) *Server { s := &Server{ Plugins: &pluginContainer{}, options: make(map[string]interface{}), activeConn: make(map[net.Conn]struct{}), doneChan: make(chan struct{}), serviceMap: make(map[string]*service), router: make(map[string]Handler), AsyncWrite: false, // 除非你想做进一步的优化测试,否则建议你设置为false Started: make(chan struct{}), } for _, op := range options { op(s) } if s.options["TCPKeepAlivePeriod"] == nil { s.options["TCPKeepAlivePeriod"] = 3 * time.Minute } return s } // Address returns listened address. func (s *Server) Address() net.Addr { s.mu.RLock() defer s.mu.RUnlock() if s.ln == nil { return nil } return s.ln.Addr() } func (s *Server) AddHandler(servicePath, serviceMethod string, handler func(*Context) error) { s.router[servicePath+"."+serviceMethod] = handler } // ActiveClientConn returns active connections. func (s *Server) ActiveClientConn() []net.Conn { s.mu.RLock() defer s.mu.RUnlock() result := make([]net.Conn, 0, len(s.activeConn)) for clientConn := range s.activeConn { result = append(result, clientConn) } return result } // SendMessage a request to the specified client. // The client is designated by the conn. // conn can be gotten from context in services: // // ctx.Value(RemoteConnContextKey) // // servicePath, serviceMethod, metadata can be set to zero values. func (s *Server) SendMessage(conn net.Conn, servicePath, serviceMethod string, metadata map[string]string, data []byte) error { ctx := share.WithValue(context.Background(), StartSendRequestContextKey, time.Now().UnixNano()) s.Plugins.DoPreWriteRequest(ctx) req := protocol.NewMessage() req.SetMessageType(protocol.Request) seq := s.seq.Add(1) req.SetSeq(seq) req.SetOneway(true) req.SetSerializeType(protocol.SerializeNone) req.ServicePath = servicePath req.ServiceMethod = serviceMethod req.Metadata = metadata req.Payload = data b := req.EncodeSlicePointer() _, err := conn.Write(*b) protocol.PutData(b) s.Plugins.DoPostWriteRequest(ctx, req, err) return err } func (s *Server) getDoneChan() <-chan struct{} { return s.doneChan } // Serve starts and listens RPC requests. // It is blocked until receiving connections from clients. func (s *Server) Serve(network, address string) (err error) { var ln net.Listener ln, err = s.makeListener(network, address) if err != nil { return err } defer s.UnregisterAll() if network == "http" { s.serveByHTTP(ln, "") return nil } if network == "ws" || network == "wss" { s.serveByWS(ln, "") return nil } // try to start gateway ln = s.startGateway(network, ln) return s.serveListener(ln) } // ServeListener listens RPC requests. // It is blocked until receiving connections from clients. func (s *Server) ServeListener(network string, ln net.Listener) (err error) { defer s.UnregisterAll() if network == "http" { s.serveByHTTP(ln, "") return nil } // try to start gateway ln = s.startGateway(network, ln) return s.serveListener(ln) } // serveListener accepts incoming connections on the Listener ln, // creating a new service goroutine for each. // The service goroutines read requests and then call services to reply to them. func (s *Server) serveListener(ln net.Listener) error { var tempDelay time.Duration s.mu.Lock() s.ln = ln close(s.Started) s.mu.Unlock() for { conn, e := ln.Accept() if e != nil { if s.isShutdown() { <-s.doneChan return ErrServerClosed } if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Errorf("rpcx: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } if errors.Is(e, cmux.ErrListenerClosed) { return ErrServerClosed } return e } tempDelay = 0 if tc, ok := conn.(*net.TCPConn); ok { period := s.options["TCPKeepAlivePeriod"] if period != nil { tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(period.(time.Duration)) tc.SetLinger(10) } } conn, ok := s.Plugins.DoPostConnAccept(conn) if !ok { conn.Close() continue } s.mu.Lock() s.activeConn[conn] = struct{}{} s.mu.Unlock() if share.Trace { log.Debugf("server accepted an conn: %v", conn.RemoteAddr().String()) } go s.serveConn(conn) } } // serveByHTTP serves by HTTP. // if rpcPath is an empty string, use share.DefaultRPCPath. func (s *Server) serveByHTTP(ln net.Listener, rpcPath string) { s.ln = ln if rpcPath == "" { rpcPath = share.DefaultRPCPath } mux := http.NewServeMux() mux.Handle(rpcPath, s) srv := &http.Server{Handler: mux} srv.Serve(ln) } func (s *Server) serveByWS(ln net.Listener, rpcPath string) { s.ln = ln if rpcPath == "" { rpcPath = share.DefaultRPCPath } mux := http.NewServeMux() mux.Handle(rpcPath, websocket.Handler(s.ServeWS)) srv := &http.Server{Handler: mux} srv.Serve(ln) } func (s *Server) sendResponse(ctx *share.Context, conn net.Conn, err error, req, res *protocol.Message) { if len(res.Payload) > 1024 && req.CompressType() != protocol.None { res.SetCompressType(req.CompressType()) } s.Plugins.DoPreWriteResponse(ctx, req, res, err) data := res.EncodeSlicePointer() if s.AsyncWrite { if s.pool != nil { s.pool.Submit(func() { if s.writeTimeout != 0 { conn.SetWriteDeadline(time.Now().Add(s.writeTimeout)) } conn.Write(*data) protocol.PutData(data) }) } else { go func() { if s.writeTimeout != 0 { conn.SetWriteDeadline(time.Now().Add(s.writeTimeout)) } conn.Write(*data) protocol.PutData(data) }() } } else { if s.writeTimeout != 0 { conn.SetWriteDeadline(time.Now().Add(s.writeTimeout)) } conn.Write(*data) protocol.PutData(data) } s.Plugins.DoPostWriteResponse(ctx, req, res, err) } func (s *Server) serveConn(conn net.Conn) { if s.isShutdown() { s.closeConn(conn) return } defer func() { if err := recover(); err != nil { const size = 64 << 10 buf := make([]byte, size) ss := runtime.Stack(buf, false) if ss > size { ss = size } buf = buf[:ss] log.Errorf("serving %s panic error: %s, stack:\n %s", conn.RemoteAddr(), err, buf) } if share.Trace { log.Debugf("server closed conn: %v", conn.RemoteAddr().String()) } // make sure all inflight requests are handled and all drained if s.isShutdown() { <-s.doneChan } s.closeConn(conn) }() if tlsConn, ok := conn.(*tls.Conn); ok { if d := s.readTimeout; d != 0 { conn.SetReadDeadline(time.Now().Add(d)) } if d := s.writeTimeout; d != 0 { conn.SetWriteDeadline(time.Now().Add(d)) } if err := tlsConn.Handshake(); err != nil { log.Errorf("rpcx: TLS handshake error from %s: %v", conn.RemoteAddr(), err) return } } r := bufio.NewReaderSize(conn, ReaderBuffsize) // read requests and handle it for { if s.isShutdown() { return } t0 := time.Now() if s.readTimeout != 0 { conn.SetReadDeadline(t0.Add(s.readTimeout)) } // create a rpcx Context ctx := share.WithValue(context.Background(), RemoteConnContextKey, conn) // read a request from the underlying connection req, err := s.readRequest(ctx, r) if err != nil { if errors.Is(err, io.EOF) { log.Infof("client has closed this connection: %s", conn.RemoteAddr().String()) } else if errors.Is(err, net.ErrClosed) { log.Infof("rpcx: connection %s is closed", conn.RemoteAddr().String()) } else if errors.Is(err, ErrReqReachLimit) { if !req.IsOneway() { // return a error response res := req.Clone() res.SetMessageType(protocol.Response) s.handleError(res, err) s.sendResponse(ctx, conn, err, req, res) } else { // Oneway and only call the plugins s.Plugins.DoPreWriteResponse(ctx, req, nil, err) } continue } else { // wrong data log.Warnf("rpcx: failed to read request: %v", err) } if s.HandleServiceError != nil { s.HandleServiceError(err) } return } if share.Trace { log.Debugf("server received an request %+v from conn: %v", req, conn.RemoteAddr().String()) } ctx = share.WithLocalValue(ctx, StartRequestContextKey, time.Now().UnixNano()) closeConn := false if !req.IsHeartbeat() { err = s.auth(ctx, req) closeConn = err != nil } if err != nil { if !req.IsOneway() { // return a error response res := req.Clone() res.SetMessageType(protocol.Response) s.handleError(res, err) s.sendResponse(ctx, conn, err, req, res) } else { s.Plugins.DoPreWriteResponse(ctx, req, nil, err) } if s.HandleServiceError != nil { s.HandleServiceError(err) } // auth failed, closed the connection if closeConn { log.Infof("auth failed for conn %s: %v", conn.RemoteAddr().String(), err) return } continue } if s.pool != nil { s.pool.Submit(func() { s.processOneRequest(ctx, req, conn) }) } else { go s.processOneRequest(ctx, req, conn) } } } func (s *Server) processOneRequest(ctx *share.Context, req *protocol.Message, conn net.Conn) { defer func() { if r := recover(); r != nil { buf := make([]byte, 1024) buf = buf[:runtime.Stack(buf, true)] if s.HandleServiceError != nil { s.HandleServiceError(fmt.Errorf("%v", r)) } else { log.Errorf("[handler internal error]: servicepath: %s, servicemethod: %s, err: %v,stacks: %s", req.ServicePath, req.ServiceMethod, r, string(buf)) } sctx := NewContext(ctx, conn, req, s.AsyncWrite) sctx.WriteError(fmt.Errorf("%v", r)) } }() atomic.AddInt32(&s.handlerMsgNum, 1) defer atomic.AddInt32(&s.handlerMsgNum, -1) // 心跳请求,直接处理返回 if req.IsHeartbeat() { s.Plugins.DoHeartbeatRequest(ctx, req) req.SetMessageType(protocol.Response) data := req.EncodeSlicePointer() if s.writeTimeout != 0 { conn.SetWriteDeadline(time.Now().Add(s.writeTimeout)) } conn.Write(*data) protocol.PutData(data) return } cancelFunc := parseServerTimeout(ctx, req) if cancelFunc != nil { defer cancelFunc() } resMetadata := make(map[string]string) if req.Metadata == nil { req.Metadata = make(map[string]string) } ctx = share.WithLocalValue(share.WithLocalValue(ctx, share.ReqMetaDataKey, req.Metadata), share.ResMetaDataKey, resMetadata) s.Plugins.DoPreHandleRequest(ctx, req) if share.Trace { log.Debugf("server handle request %+v from conn: %v", req, conn.RemoteAddr().String()) } // use handlers first if handler, ok := s.router[req.ServicePath+"."+req.ServiceMethod]; ok { sctx := NewContext(ctx, conn, req, s.AsyncWrite) err := handler(sctx) if err != nil { if s.HandleServiceError != nil { s.HandleServiceError(err) } else { log.Errorf("[handler internal error]: servicepath: %s, servicemethod, err: %v", req.ServicePath, req.ServiceMethod, err) } sctx.WriteError(err) } return } res, err := s.handleRequest(ctx, req) if err != nil { if s.HandleServiceError != nil { s.HandleServiceError(err) } else { log.Warnf("rpcx: failed to handle request: %v", err) } } if !req.IsOneway() { if len(resMetadata) > 0 { // copy meta in context to responses meta := res.Metadata if meta == nil { res.Metadata = resMetadata } else { for k, v := range resMetadata { if meta[k] == "" { meta[k] = v } } } } s.sendResponse(ctx, conn, err, req, res) } if share.Trace { log.Debugf("server write response %+v for an request %+v from conn: %v", res, req, conn.RemoteAddr().String()) } } func parseServerTimeout(ctx *share.Context, req *protocol.Message) context.CancelFunc { if req == nil || req.Metadata == nil { return nil } st := req.Metadata[share.ServerTimeout] if st == "" { return nil } timeout, err := strconv.ParseInt(st, 10, 64) if err != nil { return nil } newCtx, cancel := context.WithTimeout(ctx.Context, time.Duration(timeout)*time.Millisecond) ctx.Context = newCtx return cancel } func (s *Server) isShutdown() bool { return atomic.LoadInt32(&s.inShutdown) == 1 } func (s *Server) closeConn(conn net.Conn) { s.mu.Lock() delete(s.activeConn, conn) s.mu.Unlock() conn.Close() s.Plugins.DoPostConnClose(conn) } func (s *Server) readRequest(ctx context.Context, r io.Reader) (req *protocol.Message, err error) { err = s.Plugins.DoPreReadRequest(ctx) if err != nil { return nil, err } // pool req? req = protocol.NewMessage() err = req.Decode(r) if err == io.EOF { return req, err } perr := s.Plugins.DoPostReadRequest(ctx, req, err) if err == nil { err = perr } return req, err } func (s *Server) auth(ctx context.Context, req *protocol.Message) error { if s.AuthFunc != nil { token := req.Metadata[share.AuthKey] return s.AuthFunc(ctx, req, token) } return nil } func (s *Server) handleRequest(ctx context.Context, req *protocol.Message) (res *protocol.Message, err error) { serviceName := req.ServicePath methodName := req.ServiceMethod res = req.Clone() res.SetMessageType(protocol.Response) s.serviceMapMu.RLock() service := s.serviceMap[serviceName] if share.Trace { log.Debugf("server get service %+v for an request %+v", service, req) } s.serviceMapMu.RUnlock() if service == nil { err = errors.New("rpcx: can't find service " + serviceName) return s.handleError(res, err) } mtype := service.method[methodName] if mtype == nil { if service.function[methodName] != nil { // check raw functions return s.handleRequestForFunction(ctx, req) } err = errors.New("rpcx: can't find method " + methodName) return s.handleError(res, err) } // get a argv object from object pool argv := reflectTypePools.Get(mtype.ArgType) codec := share.Codecs[req.SerializeType()] if codec == nil { err = fmt.Errorf("can not find codec for %d", req.SerializeType()) return s.handleError(res, err) } err = codec.Decode(req.Payload, argv) if err != nil { return s.handleError(res, err) } // and get a reply object from object pool replyv := reflectTypePools.Get(mtype.ReplyType) argv, err = s.Plugins.DoPreCall(ctx, serviceName, methodName, argv) if err != nil { // return reply to object pool reflectTypePools.Put(mtype.ReplyType, replyv) return s.handleError(res, err) } if mtype.ArgType.Kind() != reflect.Ptr { err = service.call(ctx, mtype, reflect.ValueOf(argv).Elem(), reflect.ValueOf(replyv)) } else { err = service.call(ctx, mtype, reflect.ValueOf(argv), reflect.ValueOf(replyv)) } replyv, err1 := s.Plugins.DoPostCall(ctx, serviceName, methodName, argv, replyv, err) if err == nil { err = err1 } // return argc to object pool reflectTypePools.Put(mtype.ArgType, argv) if err != nil { if replyv != nil { data, err := codec.Encode(replyv) // return reply to object pool reflectTypePools.Put(mtype.ReplyType, replyv) if err != nil { return s.handleError(res, err) } res.Payload = data } return s.handleError(res, err) } if !req.IsOneway() { data, err := codec.Encode(replyv) // return reply to object pool reflectTypePools.Put(mtype.ReplyType, replyv) if err != nil { return s.handleError(res, err) } res.Payload = data } else if replyv != nil { reflectTypePools.Put(mtype.ReplyType, replyv) } if share.Trace { log.Debugf("server called service %+v for an request %+v", service, req) } return res, nil } func (s *Server) handleRequestForFunction(ctx context.Context, req *protocol.Message) (res *protocol.Message, err error) { res = req.Clone() res.SetMessageType(protocol.Response) serviceName := req.ServicePath methodName := req.ServiceMethod s.serviceMapMu.RLock() service := s.serviceMap[serviceName] s.serviceMapMu.RUnlock() if service == nil { err = errors.New("rpcx: can't find service for func raw function") return s.handleError(res, err) } mtype := service.function[methodName] if mtype == nil { err = errors.New("rpcx: can't find method " + methodName) return s.handleError(res, err) } argv := reflectTypePools.Get(mtype.ArgType) codec := share.Codecs[req.SerializeType()] if codec == nil { err = fmt.Errorf("can not find codec for %d", req.SerializeType()) return s.handleError(res, err) } err = codec.Decode(req.Payload, argv) if err != nil { return s.handleError(res, err) } replyv := reflectTypePools.Get(mtype.ReplyType) argv, err = s.Plugins.DoPreCall(ctx, serviceName, methodName, argv) if err != nil { // return reply to object pool reflectTypePools.Put(mtype.ReplyType, replyv) return s.handleError(res, err) } if mtype.ArgType.Kind() != reflect.Ptr { err = service.callForFunction(ctx, mtype, reflect.ValueOf(argv).Elem(), reflect.ValueOf(replyv)) } else { err = service.callForFunction(ctx, mtype, reflect.ValueOf(argv), reflect.ValueOf(replyv)) } replyv, err1 := s.Plugins.DoPostCall(ctx, serviceName, methodName, argv, replyv, err) if err == nil { err = err1 } reflectTypePools.Put(mtype.ArgType, argv) if err != nil { reflectTypePools.Put(mtype.ReplyType, replyv) return s.handleError(res, err) } if !req.IsOneway() { data, err := codec.Encode(replyv) reflectTypePools.Put(mtype.ReplyType, replyv) if err != nil { return s.handleError(res, err) } res.Payload = data } else if replyv != nil { reflectTypePools.Put(mtype.ReplyType, replyv) } return res, nil } func (s *Server) handleError(res *protocol.Message, err error) (*protocol.Message, error) { res.SetMessageStatusType(protocol.Error) if res.Metadata == nil { res.Metadata = make(map[string]string) } if s.ServerErrorFunc != nil { res.Metadata[protocol.ServiceError] = s.ServerErrorFunc(res, err) } else { res.Metadata[protocol.ServiceError] = err.Error() } return res, err } // Can connect to RPC service using HTTP CONNECT to rpcPath. var connected = "200 Connected to rpcx" // ServeHTTP implements an http.Handler that answers RPC requests. func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodConnect { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusMethodNotAllowed) io.WriteString(w, "405 must CONNECT\n") return } conn, _, err := w.(http.Hijacker).Hijack() if err != nil { log.Info("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) return } io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n") s.mu.Lock() s.activeConn[conn] = struct{}{} s.mu.Unlock() s.serveConn(conn) } func (s *Server) ServeWS(conn *websocket.Conn) { s.mu.Lock() s.activeConn[conn] = struct{}{} s.mu.Unlock() conn.PayloadType = websocket.BinaryFrame s.serveConn(conn) } // Close immediately closes all active net.Listeners. func (s *Server) Close() error { s.mu.Lock() defer s.mu.Unlock() var err error if s.ln != nil { err = s.ln.Close() } for c := range s.activeConn { c.Close() delete(s.activeConn, c) s.Plugins.DoPostConnClose(c) } s.closeDoneChanLocked() if s.pool != nil { s.pool.StopAndWaitFor(10 * time.Second) } return err } // RegisterOnShutdown registers a function to call on Shutdown. // This can be used to gracefully shutdown connections. func (s *Server) RegisterOnShutdown(f func(s *Server)) { s.mu.Lock() s.onShutdown = append(s.onShutdown, f) s.mu.Unlock() } // RegisterOnRestart registers a function to call on Restart. func (s *Server) RegisterOnRestart(f func(s *Server)) { s.mu.Lock() s.onRestart = append(s.onRestart, f) s.mu.Unlock() } var shutdownPollInterval = 1000 * time.Millisecond // Shutdown gracefully shuts down the server without interrupting any // active connections. Shutdown works by first closing the // listener, then closing all idle connections, and then waiting // indefinitely for connections to return to idle and then shut down. // If the provided context expires before the shutdown is complete, // Shutdown returns the context's error, otherwise it returns any // error returned from closing the Server's underlying Listener. func (s *Server) Shutdown(ctx context.Context) error { var err error if atomic.CompareAndSwapInt32(&s.inShutdown, 0, 1) { log.Info("shutdown begin") s.mu.Lock() // 主动注销注册的服务 s.UnregisterAll() if s.ln != nil { s.ln.Close() } for conn := range s.activeConn { if tcpConn, ok := conn.(*net.TCPConn); ok { tcpConn.CloseRead() } } s.mu.Unlock() // wait all in-processing requests finish. ticker := time.NewTicker(shutdownPollInterval) defer ticker.Stop() outer: for { if s.checkProcessMsg() { break } select { case <-ctx.Done(): err = ctx.Err() break outer case <-ticker.C: } } s.jsonrpcHTTPServerLock.Lock() if s.gatewayHTTPServer != nil { if err := s.closeHTTP1APIGateway(ctx); err != nil { log.Warnf("failed to close gateway: %v", err) } else { log.Info("closed gateway") } } s.jsonrpcHTTPServerLock.Unlock() if s.jsonrpcHTTPServer != nil { if err := s.closeJSONRPC2(ctx); err != nil { log.Warnf("failed to close JSONRPC: %v", err) } else { log.Info("closed JSONRPC") } } s.mu.Lock() for conn := range s.activeConn { conn.Close() delete(s.activeConn, conn) s.Plugins.DoPostConnClose(conn) } s.closeDoneChanLocked() s.mu.Unlock() log.Info("shutdown end") } return err } // Restart restarts this server gracefully. // It starts a new rpcx server with the same port with SO_REUSEPORT socket option, // and shutdown this rpcx server gracefully. func (s *Server) Restart(ctx context.Context) error { pid, err := s.startProcess() if err != nil { return err } log.Infof("restart a new rpcx server: %d", pid) // TODO: is it necessary? time.Sleep(3 * time.Second) return s.Shutdown(ctx) } func (s *Server) startProcess() (int, error) { argv0, err := exec.LookPath(os.Args[0]) if err != nil { return 0, err } // Pass on the environment and replace the old count key with the new one. var env []string env = append(env, os.Environ()...) originalWD, _ := os.Getwd() allFiles := []*os.File{os.Stdin, os.Stdout, os.Stderr} process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{ Dir: originalWD, Env: env, Files: allFiles, }) if err != nil { return 0, err } return process.Pid, nil } func (s *Server) checkProcessMsg() bool { size := atomic.LoadInt32(&s.handlerMsgNum) log.Info("need handle in-processing msg size:", size) return size == 0 } func (s *Server) closeDoneChanLocked() { select { case <-s.doneChan: // Already closed. Don't close again. default: // Safe to close here. We're the only closer, guarded // by s.mu.RegisterName close(s.doneChan) } } var ip4Reg = regexp.MustCompile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) func validIP4(ipAddress string) bool { ipAddress = strings.Trim(ipAddress, " ") i := strings.LastIndex(ipAddress, ":") ipAddress = ipAddress[:i] // remove port return ip4Reg.MatchString(ipAddress) } func validIP6(ipAddress string) bool { ipAddress = strings.Trim(ipAddress, " ") i := strings.LastIndex(ipAddress, ":") ipAddress = ipAddress[:i] // remove port ipAddress = strings.TrimPrefix(ipAddress, "[") ipAddress = strings.TrimSuffix(ipAddress, "]") ip := net.ParseIP(ipAddress) if ip != nil && ip.To4() == nil { return true } else { return false } } ================================================ FILE: server/server_test.go ================================================ package server import ( "bytes" "context" "encoding/json" "io" "net" "testing" "time" testutils "github.com/smallnest/rpcx/_testutils" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/share" "github.com/stretchr/testify/assert" ) type Args struct { A int B int } type Reply struct { C int } type Arith int func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error { reply.C = args.A * args.B return nil } func (t *Arith) ThriftMul(ctx context.Context, args *testutils.ThriftArgs_, reply *testutils.ThriftReply) error { reply.C = args.A * args.B return nil } func (t *Arith) ConsumingOperation(ctx context.Context, args *testutils.ThriftArgs_, reply *testutils.ThriftReply) error { reply.C = args.A * args.B time.Sleep(10 * time.Second) return nil } func TestShutdownHook(t *testing.T) { s := NewServer() var cancel1 context.CancelFunc s.RegisterOnShutdown(func(s *Server) { var ctx context.Context ctx, cancel1 = context.WithTimeout(context.Background(), 155*time.Second) s.Shutdown(ctx) }) s.RegisterName("Arith2", new(Arith), "") s.Register(new(Arith), "") go s.Serve("tcp", ":0") time.Sleep(time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) s.Shutdown(ctx) cancel() if cancel1 != nil { cancel1() } } func TestHandleRequest(t *testing.T) { // use jsoncodec req := protocol.NewMessage() req.SetVersion(0) req.SetMessageType(protocol.Request) req.SetHeartbeat(false) req.SetOneway(false) req.SetCompressType(protocol.None) req.SetMessageStatusType(protocol.Normal) req.SetSerializeType(protocol.JSON) req.SetSeq(1234567890) req.ServicePath = "Arith" req.ServiceMethod = "Mul" argv := &Args{ A: 10, B: 20, } data, err := json.Marshal(argv) if err != nil { t.Fatal(err) } req.Payload = data server := NewServer() server.RegisterName("Arith", new(Arith), "") res, err := server.handleRequest(context.Background(), req) if err != nil { t.Fatalf("failed to hand request: %v", err) } if res.Payload == nil { t.Fatalf("expect reply but got %s", res.Payload) } reply := &Reply{} codec := share.Codecs[res.SerializeType()] if codec == nil { t.Fatalf("can not find codec %c", codec) } err = codec.Decode(res.Payload, reply) if err != nil { t.Fatalf("failed to decode response: %v", err) } if reply.C != 200 { t.Fatalf("expect 200 but got %d", reply.C) } } func TestHandler(t *testing.T) { // use jsoncodec req := protocol.NewMessage() req.SetVersion(0) req.SetMessageType(protocol.Request) req.SetHeartbeat(false) req.SetOneway(false) req.SetCompressType(protocol.None) req.SetMessageStatusType(protocol.Normal) req.SetSerializeType(protocol.JSON) req.SetSeq(1234567890) req.ServicePath = "Arith" req.ServiceMethod = "Mul" argv := &Args{ A: 10, B: 20, } data, err := json.Marshal(argv) if err != nil { t.Fatal(err) } req.Payload = data serverConn, clientConn := net.Pipe() handler := func(ctx *Context) error { req := &Args{} res := &Reply{} ctx.Bind(req) res.C = req.A * req.B return ctx.Write(res) } go func() { ctx := NewContext(share.NewContext(context.Background()), serverConn, req, false) err = handler(ctx) assert.NoError(t, err) serverConn.Close() }() data, err = io.ReadAll(clientConn) assert.NoError(t, err) resp, err := protocol.Read(bytes.NewReader(data)) assert.NoError(t, err) assert.Equal(t, "Arith", resp.ServicePath) assert.Equal(t, "Mul", resp.ServiceMethod) assert.Equal(t, req.Seq(), resp.Seq()) assert.Equal(t, "{\"C\":200}", string(resp.Payload)) } func Test_validIP6(t *testing.T) { type args struct { ipAddress string } tests := []struct { name string args args want bool }{ {"1", args{ipAddress: "[CDCD:910A:2222:5498:8475:1111:3900:2020]:8080"}, true}, {"2", args{ipAddress: "[1030::C9B4:FF12:48AA:1A2B]:8080"}, true}, {"3", args{ipAddress: "[2000:0:0:0:0:0:0:1]:8080"}, true}, {"4", args{ipAddress: "127.0.0.1:8080"}, false}, {"5", args{ipAddress: "localhost:8080"}, false}, {"6", args{ipAddress: "127.1:8080"}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, tt.want, validIP6(tt.args.ipAddress), "validIP6(%v)", tt.args.ipAddress) }) } } ================================================ FILE: server/service.go ================================================ package server import ( "context" "errors" "fmt" "reflect" "runtime" "strings" "sync" "unicode" "unicode/utf8" rerrors "github.com/smallnest/rpcx/errors" "github.com/smallnest/rpcx/log" ) // RpcServiceError represents an error that is case by service implementation. type RpcServiceInternalError struct { Err string Method string Argv interface{} stack string } // Error returns the error message. func (e RpcServiceInternalError) Error() string { return fmt.Sprintf("[service internal error]: %v, method: %s, argv: %+v, stack: %s", e.Err, e.Method, e.Argv, e.stack) } // String returns the error message. func (e RpcServiceInternalError) String() string { return e.Error() } // Precompute the reflect type for error. Can't use error directly // because Typeof takes an empty interface value. This is annoying. var typeOfError = reflect.TypeFor[error]() // Precompute the reflect type for context. var typeOfContext = reflect.TypeFor[context.Context]() type methodType struct { sync.Mutex // protects counters method reflect.Method ArgType reflect.Type ReplyType reflect.Type // numCalls uint } type functionType struct { sync.Mutex // protects counters fn reflect.Value ArgType reflect.Type ReplyType reflect.Type } type service struct { name string // name of service rcvr reflect.Value // receiver of methods for the service typ reflect.Type // type of the receiver method map[string]*methodType // registered methods function map[string]*functionType // registered functions } func isExported(name string) bool { rune, _ := utf8.DecodeRuneInString(name) return unicode.IsUpper(rune) } func isExportedOrBuiltinType(t reflect.Type) bool { for t.Kind() == reflect.Ptr { t = t.Elem() } // PkgPath will be non-empty even for an exported type, // so we need to check the type name as well. return isExported(t.Name()) || t.PkgPath() == "" } func (s *Server) ListServices() []string { s.serviceMapMu.RLock() defer s.serviceMapMu.RUnlock() var arr []string for name := range s.serviceMap { arr = append(arr, name) } return arr } // Register publishes in the server the set of methods of the // receiver value that satisfy the following conditions: // - exported method of exported type // - three arguments, the first is of context.Context, both of exported type for three arguments // - the third argument is a pointer // - one return value, of type error // // It returns an error if the receiver is not an exported type or has // no suitable methods. It also logs the error. // The client accesses each method using a string of the form "Type.Method", // where Type is the receiver's concrete type. func (s *Server) Register(rcvr interface{}, metadata string) error { sname, err := s.register(rcvr, "", false) if err != nil { return err } return s.Plugins.DoRegister(sname, rcvr, metadata) } // RegisterName is like Register but uses the provided name for the type // instead of the receiver's concrete type. func (s *Server) RegisterName(name string, rcvr interface{}, metadata string) error { _, err := s.register(rcvr, name, true) if err != nil { return err } if s.Plugins == nil { s.Plugins = &pluginContainer{} } return s.Plugins.DoRegister(name, rcvr, metadata) } // RegisterFunction publishes a function that satisfy the following conditions: // - three arguments, the first is of context.Context, both of exported type for three arguments // - the third argument is a pointer // - one return value, of type error // // The client accesses function using a string of the form "servicePath.Method". func (s *Server) RegisterFunction(servicePath string, fn interface{}, metadata string) error { fname, err := s.registerFunction(servicePath, fn, "", false) if err != nil { return err } return s.Plugins.DoRegisterFunction(servicePath, fname, fn, metadata) } // RegisterFunctionName is like RegisterFunction but uses the provided name for the function // instead of the function's concrete type. func (s *Server) RegisterFunctionName(servicePath string, name string, fn interface{}, metadata string) error { _, err := s.registerFunction(servicePath, fn, name, true) if err != nil { return err } return s.Plugins.DoRegisterFunction(servicePath, name, fn, metadata) } func (s *Server) register(rcvr interface{}, name string, useName bool) (string, error) { s.serviceMapMu.Lock() defer s.serviceMapMu.Unlock() service := new(service) service.typ = reflect.TypeOf(rcvr) service.rcvr = reflect.ValueOf(rcvr) sname := reflect.Indirect(service.rcvr).Type().Name() // Type if useName { sname = name } if sname == "" { errorStr := "rpcx.Register: no service name for type " + service.typ.String() log.Error(errorStr) return sname, errors.New(errorStr) } if !useName && !isExported(sname) { errorStr := "rpcx.Register: type " + sname + " is not exported" log.Error(errorStr) return sname, errors.New(errorStr) } service.name = sname // Install the methods service.method = suitableMethods(service.typ, true) if len(service.method) == 0 { var errorStr string // To help the user, see if a pointer receiver would work. method := suitableMethods(reflect.PtrTo(service.typ), false) if len(method) != 0 { errorStr = "rpcx.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)" } else { errorStr = "rpcx.Register: type " + sname + " has no exported methods of suitable type" } log.Error(errorStr) return sname, errors.New(errorStr) } s.serviceMap[service.name] = service return sname, nil } func (s *Server) registerFunction(servicePath string, fn interface{}, name string, useName bool) (string, error) { s.serviceMapMu.Lock() defer s.serviceMapMu.Unlock() ss := s.serviceMap[servicePath] if ss == nil { ss = new(service) ss.name = servicePath ss.function = make(map[string]*functionType) } f, ok := fn.(reflect.Value) if !ok { f = reflect.ValueOf(fn) } if f.Kind() != reflect.Func { return "", errors.New("function must be func or bound method") } fname := runtime.FuncForPC(reflect.Indirect(f).Pointer()).Name() if fname != "" { i := strings.LastIndex(fname, ".") if i >= 0 { fname = fname[i+1:] } } if useName { fname = name } if fname == "" { errorStr := "rpcx.registerFunction: no func name for type " + f.Type().String() log.Error(errorStr) return fname, errors.New(errorStr) } t := f.Type() if t.NumIn() != 3 { return fname, fmt.Errorf("rpcx.registerFunction: has wrong number of ins: %s", f.Type().String()) } if t.NumOut() != 1 { return fname, fmt.Errorf("rpcx.registerFunction: has wrong number of outs: %s", f.Type().String()) } // First arg must be context.Context ctxType := t.In(0) if !ctxType.Implements(typeOfContext) { return fname, fmt.Errorf("function %s must use context as the first parameter", f.Type().String()) } argType := t.In(1) if !isExportedOrBuiltinType(argType) { return fname, fmt.Errorf("function %s parameter type not exported: %v", f.Type().String(), argType) } replyType := t.In(2) if replyType.Kind() != reflect.Ptr { return fname, fmt.Errorf("function %s reply type not a pointer: %s", f.Type().String(), replyType) } if !isExportedOrBuiltinType(replyType) { return fname, fmt.Errorf("function %s reply type not exported: %v", f.Type().String(), replyType) } // The return type of the method must be error. if returnType := t.Out(0); returnType != typeOfError { return fname, fmt.Errorf("function %s returns %s, not error", f.Type().String(), returnType.String()) } // Install the methods ss.function[fname] = &functionType{fn: f, ArgType: argType, ReplyType: replyType} s.serviceMap[servicePath] = ss // init pool for reflect.Type of args and reply reflectTypePools.Init(argType) reflectTypePools.Init(replyType) return fname, nil } // suitableMethods returns suitable Rpc methods of typ, it will report // error using log if reportErr is true. func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType { methods := make(map[string]*methodType) for m := 0; m < typ.NumMethod(); m++ { method := typ.Method(m) mtype := method.Type mname := method.Name // Method must be exported. if method.PkgPath != "" { continue } // Method needs four ins: receiver, context.Context, *args, *reply. if mtype.NumIn() != 4 { if reportErr { log.Debug("method ", mname, " has wrong number of ins:", mtype.NumIn()) } continue } // First arg must be context.Context ctxType := mtype.In(1) if !ctxType.Implements(typeOfContext) { if reportErr { log.Debug("method ", mname, " must use context.Context as the first parameter") } continue } // Second arg need not be a pointer. argType := mtype.In(2) if !isExportedOrBuiltinType(argType) { if reportErr { log.Info(mname, " parameter type not exported: ", argType) } continue } // Third arg must be a pointer. replyType := mtype.In(3) if replyType.Kind() != reflect.Ptr { if reportErr { log.Info("method", mname, " reply type not a pointer:", replyType) } continue } // Reply type must be exported. if !isExportedOrBuiltinType(replyType) { if reportErr { log.Info("method", mname, " reply type not exported:", replyType) } continue } // Method needs one out. if mtype.NumOut() != 1 { if reportErr { log.Info("method", mname, " has wrong number of outs:", mtype.NumOut()) } continue } // The return type of the method must be error. if returnType := mtype.Out(0); returnType != typeOfError { if reportErr { log.Info("method", mname, " returns ", returnType.String(), " not error") } continue } methods[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType} // init pool for reflect.Type of args and reply reflectTypePools.Init(argType) reflectTypePools.Init(replyType) } return methods } // UnregisterAll unregisters all services. // You can call this method when you want to shutdown/upgrade this node. func (s *Server) UnregisterAll() error { var err error s.unregisterAllOnce.Do(func() { err = s.unregisterAll() }) return err } func (s *Server) unregisterAll() error { s.serviceMapMu.RLock() defer s.serviceMapMu.RUnlock() var es []error for k := range s.serviceMap { err := s.Plugins.DoUnregister(k) if err != nil { es = append(es, err) } } if len(es) > 0 { return rerrors.NewMultiError(es) } return nil } func (s *service) call(ctx context.Context, mtype *methodType, argv, replyv reflect.Value) (err error) { defer func() { if r := recover(); r != nil { buf := make([]byte, 4096) n := runtime.Stack(buf, false) buf = buf[:n] err = &RpcServiceInternalError{ Err: fmt.Sprintf("%v", r), Method: mtype.method.Name, Argv: argv.Interface(), stack: string(buf), } log.Error(err) } }() function := mtype.method.Func // Invoke the method, providing a new value for the reply. returnValues := function.Call([]reflect.Value{s.rcvr, reflect.ValueOf(ctx), argv, replyv}) // The return value for the method is an error. errInter := returnValues[0].Interface() if errInter != nil { return errInter.(error) } return nil } func (s *service) callForFunction(ctx context.Context, ft *functionType, argv, replyv reflect.Value) (err error) { defer func() { if r := recover(); r != nil { buf := make([]byte, 4096) n := runtime.Stack(buf, false) buf = buf[:n] err = &RpcServiceInternalError{ Err: fmt.Sprintf("%v", r), Method: fmt.Sprintf("%s", runtime.FuncForPC(ft.fn.Pointer())), Argv: argv.Interface(), stack: string(buf), } log.Error(err) } }() // Invoke the function, providing a new value for the reply. returnValues := ft.fn.Call([]reflect.Value{reflect.ValueOf(ctx), argv, replyv}) // The return value for the method is an error. errInter := returnValues[0].Interface() if errInter != nil { return errInter.(error) } return nil } ================================================ FILE: server/service_test.go ================================================ package server import ( "context" "reflect" "testing" "github.com/stretchr/testify/assert" ) func Test_isExported(t *testing.T) { assert.Equal(t, true, isExported("IsExported")) assert.Equal(t, false, isExported("isExported")) assert.Equal(t, false, isExported("_isExported")) assert.Equal(t, false, isExported("123_isExported")) assert.Equal(t, false, isExported("[]_isExported")) assert.Equal(t, false, isExported("&_isExported")) } func Mul(ctx context.Context, args *Args, reply *Reply) error { reply.C = args.A * args.B return nil } func Test_isExportedOrBuiltinType(t *testing.T) { typeOfMul := reflect.TypeOf(Mul) assert.Equal(t, true, isExportedOrBuiltinType(typeOfMul)) } ================================================ FILE: server/stream.go ================================================ package server import ( "context" "crypto/rand" "errors" "io" "net" "sync" "time" lru "github.com/hashicorp/golang-lru" "github.com/smallnest/rpcx/log" "github.com/smallnest/rpcx/share" ) var ErrNotAccept = errors.New("server refused the connection") // StreamHandler handles a streaming connection with client. type StreamHandler func(conn net.Conn, args *share.StreamServiceArgs) // StreamAcceptor accepts connection from clients or not. // You can use it to validate clients and determine if accept or drop the connection. type StreamAcceptor func(ctx context.Context, args *share.StreamServiceArgs) bool type streamTokenInfo struct { token []byte args *share.StreamServiceArgs } // StreamService support streaming between clients and server. // It registers a streaming service and listens on the given port. // Clients will invokes this service to get the token and send the token and begin to stream. type StreamService struct { Addr string AdvertiseAddr string handler StreamHandler acceptor StreamAcceptor cachedTokens *lru.Cache startOnce sync.Once ln net.Listener done chan struct{} } // NewStreamService creates a stream service. func NewStreamService(addr string, streamHandler StreamHandler, acceptor StreamAcceptor, waitNum int) *StreamService { cachedTokens, _ := lru.New(waitNum) fi := &StreamService{ Addr: addr, handler: streamHandler, cachedTokens: cachedTokens, acceptor: acceptor, } return fi } // EnableStreamService supports stream service in this server. func (s *Server) EnableStreamService(serviceName string, streamService *StreamService) { if serviceName == "" { serviceName = share.StreamServiceName } _ = streamService.Start() _ = s.RegisterName(serviceName, streamService, "") } func (s *StreamService) Stream(ctx context.Context, args *share.StreamServiceArgs, reply *share.StreamServiceReply) error { // clientConn := ctx.Value(server.RemoteConnContextKey).(net.Conn) if s.acceptor != nil && !s.acceptor(ctx, args) { return ErrNotAccept } token := make([]byte, 32) _, err := rand.Read(token) if err != nil { return err } *reply = share.StreamServiceReply{ Token: token, Addr: s.Addr, } if s.AdvertiseAddr != "" { reply.Addr = s.AdvertiseAddr } s.cachedTokens.Add(string(token), &streamTokenInfo{token, args}) return nil } func (s *StreamService) Start() error { s.startOnce.Do(func() { go s.start() }) return nil } func (s *StreamService) start() error { ln, err := net.Listen("tcp", s.Addr) if err != nil { return err } s.ln = ln var tempDelay time.Duration for { select { case <-s.done: return nil default: conn, e := ln.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Errorf("filetransfer: accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 if tc, ok := conn.(*net.TCPConn); ok { tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(3 * time.Minute) tc.SetLinger(10) } token := make([]byte, 32) _, err := io.ReadFull(conn, token) if err != nil { conn.Close() log.Errorf("failed to read token from %s", conn.RemoteAddr().String()) continue } tokenStr := string(token) info, ok := s.cachedTokens.Get(tokenStr) if !ok { conn.Close() log.Errorf("failed to read token from %s", conn.RemoteAddr().String()) continue } s.cachedTokens.Remove(tokenStr) switch ti := info.(type) { case *streamTokenInfo: if s.handler == nil { conn.Close() continue } go s.handler(conn, ti.args) default: conn.Close() } } } } func (s *StreamService) Stop() error { close(s.done) if s.ln != nil { s.ln.Close() } return nil } ================================================ FILE: serverplugin/alias.go ================================================ package serverplugin import ( "context" "github.com/smallnest/rpcx/protocol" ) var aliasAppliedKey = "__aliasAppliedKey" type aliasPair struct { servicePath, serviceMethod string } // AliasPlugin can be used to set aliases for services type AliasPlugin struct { Aliases map[string]*aliasPair ReseverseAliases map[string]*aliasPair } // Alias sets a alias for the serviceMethod. // For example Alias("anewpath&method", "Arith.mul") func (p *AliasPlugin) Alias(aliasServicePath, aliasServiceMethod string, servicePath, serviceMethod string) { p.Aliases[aliasServicePath+"."+aliasServiceMethod] = &aliasPair{ servicePath: servicePath, serviceMethod: serviceMethod, } p.ReseverseAliases[servicePath+"."+serviceMethod] = &aliasPair{ servicePath: aliasServicePath, serviceMethod: aliasServiceMethod, } } // NewAliasPlugin creates a new NewAliasPlugin func NewAliasPlugin() *AliasPlugin { return &AliasPlugin{ Aliases: make(map[string]*aliasPair), ReseverseAliases: make(map[string]*aliasPair), } } // PostReadRequest converts the alias of this service. func (p *AliasPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error { var sp = r.ServicePath var sm = r.ServiceMethod k := sp + "." + sm if p.Aliases != nil { if pm := p.Aliases[k]; pm != nil { r.ServicePath = pm.servicePath r.ServiceMethod = pm.serviceMethod if r.Metadata == nil { r.Metadata = make(map[string]string) } r.Metadata[aliasAppliedKey] = "true" } } return nil } // PreWriteResponse restore servicePath and serviceMethod. func (p *AliasPlugin) PreWriteResponse(ctx context.Context, r *protocol.Message, res *protocol.Message) error { if r.Metadata[aliasAppliedKey] != "true" { return nil } var sp = r.ServicePath var sm = r.ServiceMethod k := sp + "." + sm if p.ReseverseAliases != nil { if pm := p.ReseverseAliases[k]; pm != nil { r.ServicePath = pm.servicePath r.ServiceMethod = pm.serviceMethod delete(r.Metadata, aliasAppliedKey) if res != nil { res.ServicePath = pm.servicePath res.ServiceMethod = pm.serviceMethod } } } return nil } ================================================ FILE: serverplugin/blacklist.go ================================================ package serverplugin import "net" // BlacklistPlugin is a plugin that control only ip addresses in blacklist can **NOT** access services. type BlacklistPlugin struct { Blacklist map[string]bool BlacklistMask []*net.IPNet // net.ParseCIDR("172.17.0.0/16") to get *net.IPNet } // HandleConnAccept check ip. func (plugin *BlacklistPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) { ip, _, err := net.SplitHostPort(conn.RemoteAddr().String()) if err != nil { return conn, true } if plugin.Blacklist[ip] { return conn, false } remoteIP := net.ParseIP(ip) for _, mask := range plugin.BlacklistMask { if mask.Contains(remoteIP) { return conn, false } } return conn, true } ================================================ FILE: serverplugin/mdns.go ================================================ package serverplugin import ( "context" "encoding/json" "errors" "fmt" "net" "net/url" "os" "strconv" "strings" "time" "github.com/grandcat/zeroconf" metrics "github.com/rcrowley/go-metrics" ) type serviceMeta struct { Service string `json:"service,omitempty"` Meta string `json:"meta,omitempty"` ServiceAddress string `json:"service_address,omitempty"` } // MDNSRegisterPlugin implements mdns/dns-sd registry. type MDNSRegisterPlugin struct { // service address, for example, tcp@127.0.0.1:8972, quic@127.0.0.1:1234 ServiceAddress string port int Metrics metrics.Registry // Registered services Services []*serviceMeta UpdateInterval time.Duration server *zeroconf.Server domain string dying chan struct{} done chan struct{} } // NewMDNSRegisterPlugin return a new MDNSRegisterPlugin. // If domain is empty, use "local." in default. func NewMDNSRegisterPlugin(serviceAddress string, port int, m metrics.Registry, updateInterval time.Duration, domain string) *MDNSRegisterPlugin { if domain == "" { domain = "local." } return &MDNSRegisterPlugin{ ServiceAddress: serviceAddress, port: port, Metrics: m, UpdateInterval: updateInterval, domain: domain, dying: make(chan struct{}), done: make(chan struct{}), } } // Start starts the mdns loop. func (p *MDNSRegisterPlugin) Start() error { if p.server == nil && len(p.Services) != 0 { p.initMDNS() } if p.UpdateInterval > 0 { go func() { ticker := time.NewTicker(p.UpdateInterval) defer ticker.Stop() defer p.server.Shutdown() for { // refresh service TTL select { case <-p.dying: close(p.done) return case <-ticker.C: if p.server == nil && len(p.Services) == 0 { break } extra := make(map[string]string) if p.Metrics != nil { extra["calls"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("calls", p.Metrics).RateMean()) extra["connections"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("connections", p.Metrics).RateMean()) } // set this same metrics for all services at this server for _, sm := range p.Services { v, _ := url.ParseQuery(string(sm.Meta)) for key, value := range extra { v.Set(key, value) } sm.Meta = v.Encode() } ss, _ := json.Marshal(p.Services) s := url.QueryEscape(string(ss)) p.server.SetText([]string{s}) } } }() } return nil } // Stop unregister all services. func (p *MDNSRegisterPlugin) Stop() error { p.server.Shutdown() close(p.dying) <-p.done return nil } func (p *MDNSRegisterPlugin) initMDNS() { data, _ := json.Marshal(p.Services) s := url.QueryEscape(string(data)) host, _ := os.Hostname() addr := p.ServiceAddress i := strings.Index(addr, "@") if i > 0 { addr = addr[i+1:] } _, portStr, err := net.SplitHostPort(addr) if err != nil { panic(err) } p.port, err = strconv.Atoi(portStr) if err != nil { panic(err) } server, err := zeroconf.Register(host, "_rpcxservices", p.domain, p.port, []string{s}, nil) if err != nil { panic(err) } p.server = server } // HandleConnAccept handles connections from clients func (p *MDNSRegisterPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) { if p.Metrics != nil { metrics.GetOrRegisterMeter("connections", p.Metrics).Mark(1) } return conn, true } // PreCall handles rpc call from clients func (p *MDNSRegisterPlugin) PreCall(_ context.Context, _, _ string, args interface{}) (interface{}, error) { if p.Metrics != nil { metrics.GetOrRegisterMeter("calls", p.Metrics).Mark(1) } return args, nil } // Register handles registering event. // this service is registered at BASE/serviceName/thisIpAddress node func (p *MDNSRegisterPlugin) Register(name string, rcvr interface{}, metadata string) (err error) { if strings.TrimSpace(name) == "" { err = errors.New("Register service `name` can't be empty") return } sm := &serviceMeta{ Service: name, Meta: metadata, ServiceAddress: p.ServiceAddress, } p.Services = append(p.Services, sm) if p.server == nil { p.initMDNS() return } ss, _ := json.Marshal(p.Services) s := url.QueryEscape(string(ss)) p.server.SetText([]string{s}) return } func (p *MDNSRegisterPlugin) RegisterFunction(serviceName, fname string, fn interface{}, metadata string) error { return p.Register(serviceName, fn, metadata) } func (p *MDNSRegisterPlugin) Unregister(name string) (err error) { if len(p.Services) == 0 { return nil } if strings.TrimSpace(name) == "" { err = errors.New("Register service `name` can't be empty") return } var services = make([]*serviceMeta, 0, len(p.Services)-1) for _, meta := range p.Services { if meta.Service != name { services = append(services, meta) } } p.Services = services ss, _ := json.Marshal(p.Services) s := url.QueryEscape(string(ss)) p.server.SetText([]string{s}) // if p.server != nil { // p.server.Shutdown() // return // } return } ================================================ FILE: serverplugin/metrics.go ================================================ package serverplugin import ( "context" "net" "time" "github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics/exp" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/server" ) // MetricsPlugin has an issue. It changes seq of requests and it is wrong!!!! // we should use other methods to map requests and responses not but seq. // MetricsPlugin collects metrics of a rpc server. // You can report metrics to log, syslog, Graphite, InfluxDB or others to display them in Dashboard such as grafana, Graphite. type MetricsPlugin struct { Registry metrics.Registry Prefix string } // NewMetricsPlugin creates a new MetricsPlugin func NewMetricsPlugin(registry metrics.Registry) *MetricsPlugin { return &MetricsPlugin{Registry: registry} } func (p *MetricsPlugin) withPrefix(m string) string { return p.Prefix + m } // Register handles registering event. func (p *MetricsPlugin) Register(name string, rcvr interface{}, metadata string) error { serviceCounter := metrics.GetOrRegisterCounter(p.withPrefix("serviceCounter"), p.Registry) serviceCounter.Inc(1) return nil } // HandleConnAccept handles connections from clients func (p *MetricsPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) { clientMeter := metrics.GetOrRegisterMeter(p.withPrefix("clientMeter"), p.Registry) clientMeter.Mark(1) return conn, true } // PreReadRequest marks start time of calling service func (p *MetricsPlugin) PreReadRequest(ctx context.Context) error { return nil } // PostReadRequest counts read func (p *MetricsPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error { sp := r.ServicePath sm := r.ServiceMethod if sp == "" { return nil } m := metrics.GetOrRegisterMeter(p.withPrefix("service."+sp+"."+sm+".Read_Qps"), p.Registry) m.Mark(1) return nil } // PostWriteResponse count write func (p *MetricsPlugin) PostWriteResponse(ctx context.Context, req *protocol.Message, res *protocol.Message, e error) error { sp := res.ServicePath sm := res.ServiceMethod if sp == "" { return nil } m := metrics.GetOrRegisterMeter(p.withPrefix("service."+sp+"."+sm+".Write_Qps"), p.Registry) m.Mark(1) t := ctx.Value(server.StartRequestContextKey).(int64) if t > 0 { t = time.Now().UnixNano() - t if t < 30*time.Minute.Nanoseconds() { // it is impossible that calltime exceeds 30 minute // Historgram h := metrics.GetOrRegisterHistogram(p.withPrefix("service."+sp+"."+sm+".CallTime"), p.Registry, metrics.NewExpDecaySample(1028, 0.015)) h.Update(t) } } return nil } // Log reports metrics into logs. // // p.Log( 5 * time.Second, log.New(os.Stderr, "metrics: ", log.Lmicroseconds)) func (p *MetricsPlugin) Log(freq time.Duration, l metrics.Logger) { go metrics.Log(p.Registry, freq, l) } // Graphite reports metrics into graphite. // // addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:2003") // p.Graphite(10e9, "metrics", addr) func (p *MetricsPlugin) Graphite(freq time.Duration, prefix string, addr *net.TCPAddr) { go metrics.Graphite(p.Registry, freq, prefix, addr) } // // InfluxDB reports metrics into influxdb. // // // // p.InfluxDB(10e9, "http://127.0.0.1:8086","metrics", "test","test"}) // // // func (p *MetricsPlugin) InfluxDB(freq time.Duration, url, database, username, password string) { // go InfluxDB(p.Registry, freq, url, database, username, password) // } // // InfluxDBWithTags reports metrics into influxdb with tags. // // you can set node info into tags. // // // // p.InfluxDBWithTags(10e9, "http://127.0.0.1:8086","metrics", "test","test", map[string]string{"host":"127.0.0.1"}) // // // func (p *MetricsPlugin) InfluxDBWithTags(freq time.Duration, url, database, username, password string, tags map[string]string) { // go InfluxDBWithTags(p.Registry, freq, url, database, username, password, tags) // } // Exp uses the same mechanism as the official expvar but exposed under /debug/metrics, // which shows a json representation of all your usual expvars as well as all your go-metrics. func (p *MetricsPlugin) Exp() { exp.Exp(metrics.DefaultRegistry) } ================================================ FILE: serverplugin/rate_limiting.go ================================================ package serverplugin import ( "net" "time" "github.com/juju/ratelimit" ) // RateLimitingPlugin can limit connecting per unit time type RateLimitingPlugin struct { FillInterval time.Duration Capacity int64 bucket *ratelimit.Bucket } // NewRateLimitingPlugin creates a new RateLimitingPlugin func NewRateLimitingPlugin(fillInterval time.Duration, capacity int64) *RateLimitingPlugin { tb := ratelimit.NewBucket(fillInterval, capacity) return &RateLimitingPlugin{ FillInterval: fillInterval, Capacity: capacity, bucket: tb} } // HandleConnAccept can limit connecting rate func (plugin *RateLimitingPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) { return conn, plugin.bucket.TakeAvailable(1) > 0 } ================================================ FILE: serverplugin/redis.go ================================================ package serverplugin import ( "context" "errors" "fmt" "net" "net/url" "strings" "sync" "time" metrics "github.com/rcrowley/go-metrics" "github.com/rpcxio/libkv" "github.com/rpcxio/libkv/store" "github.com/rpcxio/libkv/store/redis" "github.com/smallnest/rpcx/log" ) func init() { redis.Register() } // RedisRegisterPlugin implements redis registry. type RedisRegisterPlugin struct { // service address, for example, tcp@127.0.0.1:8972, quic@127.0.0.1:1234 ServiceAddress string // redis addresses RedisServers []string // base path for rpcx server, for example com/example/rpcx BasePath string Metrics metrics.Registry // Registered services Services []string metasLock sync.RWMutex metas map[string]string UpdateInterval time.Duration Options *store.Config kv store.Store dying chan struct{} done chan struct{} } // Start starts to connect redis cluster func (p *RedisRegisterPlugin) Start() error { if p.done == nil { p.done = make(chan struct{}) } if p.dying == nil { p.dying = make(chan struct{}) } if p.kv == nil { kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options) if err != nil { log.Errorf("cannot create redis registry: %v", err) close(p.done) return err } p.kv = kv } err := p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true}) if err != nil && !strings.Contains(err.Error(), "Not a file") { log.Errorf("cannot create redis path %s: %v", p.BasePath, err) close(p.done) return err } if p.UpdateInterval > 0 { go func() { ticker := time.NewTicker(p.UpdateInterval) defer ticker.Stop() defer p.kv.Close() // refresh service TTL for { select { case <-p.dying: close(p.done) return case <-ticker.C: extra := make(map[string]string) if p.Metrics != nil { extra["calls"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("calls", p.Metrics).RateMean()) extra["connections"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("connections", p.Metrics).RateMean()) } // set this same metrics for all services at this server for _, name := range p.Services { nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress) kvPair, err := p.kv.Get(nodePath) if err != nil { log.Infof("can't get data of node: %s, because of %v", nodePath, err.Error()) p.metasLock.RLock() meta := p.metas[name] p.metasLock.RUnlock() err = p.kv.Put(nodePath, []byte(meta), &store.WriteOptions{TTL: p.UpdateInterval * 2}) if err != nil { log.Errorf("cannot re-create redis path %s: %v", nodePath, err) } } else { v, _ := url.ParseQuery(string(kvPair.Value)) for key, value := range extra { v.Set(key, value) } p.kv.Put(nodePath, []byte(v.Encode()), &store.WriteOptions{TTL: p.UpdateInterval * 2}) } } } } }() } return nil } // Stop unregister all services. func (p *RedisRegisterPlugin) Stop() error { if p.kv == nil { kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options) if err != nil { log.Errorf("cannot create redis registry: %v", err) return err } p.kv = kv } for _, name := range p.Services { nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress) exist, err := p.kv.Exists(nodePath) if err != nil { log.Errorf("cannot delete path %s: %v", nodePath, err) continue } if exist { p.kv.Delete(nodePath) log.Infof("delete path %s", nodePath, err) } } close(p.dying) <-p.done return nil } // HandleConnAccept handles connections from clients func (p *RedisRegisterPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) { if p.Metrics != nil { metrics.GetOrRegisterMeter("connections", p.Metrics).Mark(1) } return conn, true } // PreCall handles rpc call from clients func (p *RedisRegisterPlugin) PreCall(_ context.Context, _, _ string, args interface{}) (interface{}, error) { if p.Metrics != nil { metrics.GetOrRegisterMeter("calls", p.Metrics).Mark(1) } return args, nil } // Register handles registering event. // this service is registered at BASE/serviceName/thisIpAddress node func (p *RedisRegisterPlugin) Register(name string, rcvr interface{}, metadata string) (err error) { if strings.TrimSpace(name) == "" { err = errors.New("Register service `name` can't be empty") return } if p.kv == nil { redis.Register() kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options) if err != nil { log.Errorf("cannot create redis registry: %v", err) return err } p.kv = kv } err = p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true}) if err != nil && !strings.Contains(err.Error(), "Not a file") { log.Errorf("cannot create redis path %s: %v", p.BasePath, err) return err } nodePath := fmt.Sprintf("%s/%s", p.BasePath, name) err = p.kv.Put(nodePath, []byte(name), &store.WriteOptions{IsDir: true}) if err != nil && !strings.Contains(err.Error(), "Not a file") { log.Errorf("cannot create redis path %s: %v", nodePath, err) return err } nodePath = fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress) err = p.kv.Put(nodePath, []byte(metadata), &store.WriteOptions{TTL: p.UpdateInterval * 2}) if err != nil { log.Errorf("cannot create redis path %s: %v", nodePath, err) return err } p.Services = append(p.Services, name) p.metasLock.Lock() if p.metas == nil { p.metas = make(map[string]string) } p.metas[name] = metadata p.metasLock.Unlock() return } func (p *RedisRegisterPlugin) Unregister(name string) (err error) { if len(p.Services) == 0 { return nil } if strings.TrimSpace(name) == "" { err = errors.New("Register service `name` can't be empty") return } if p.kv == nil { redis.Register() kv, err := libkv.NewStore(store.REDIS, p.RedisServers, p.Options) if err != nil { log.Errorf("cannot create redis registry: %v", err) return err } p.kv = kv } err = p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true}) if err != nil && !strings.Contains(err.Error(), "Not a file") { log.Errorf("cannot create redis path %s: %v", p.BasePath, err) return err } nodePath := fmt.Sprintf("%s/%s", p.BasePath, name) err = p.kv.Put(nodePath, []byte(name), &store.WriteOptions{IsDir: true}) if err != nil && !strings.Contains(err.Error(), "Not a file") { log.Errorf("cannot create redis path %s: %v", nodePath, err) return err } nodePath = fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress) err = p.kv.Delete(nodePath) if err != nil { log.Errorf("cannot remove redis path %s: %v", nodePath, err) return err } var services = make([]string, 0, len(p.Services)-1) for _, s := range p.Services { if s != name { services = append(services, s) } } p.Services = services p.metasLock.Lock() if p.metas == nil { p.metas = make(map[string]string) } delete(p.metas, name) p.metasLock.Unlock() return } ================================================ FILE: serverplugin/registry_test.go ================================================ package serverplugin import "context" type Args struct { A int B int } type Reply struct { C int } type Arith int func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error { reply.C = args.A * args.B return nil } ================================================ FILE: serverplugin/req_rate_limiting.go ================================================ package serverplugin import ( "context" "time" "github.com/juju/ratelimit" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/server" ) // ReqRateLimitingPlugin can limit requests per unit time type ReqRateLimitingPlugin struct { FillInterval time.Duration Capacity int64 bucket *ratelimit.Bucket block bool // blocks or return error if reach the limit } // NewReqRateLimitingPlugin creates a new RateLimitingPlugin func NewReqRateLimitingPlugin(fillInterval time.Duration, capacity int64, block bool) *ReqRateLimitingPlugin { tb := ratelimit.NewBucket(fillInterval, capacity) return &ReqRateLimitingPlugin{ FillInterval: fillInterval, Capacity: capacity, bucket: tb, block: block, } } // PostReadRequest can limit request processing. func (plugin *ReqRateLimitingPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error { if plugin.block { plugin.bucket.Wait(1) return nil } count := plugin.bucket.TakeAvailable(1) if count == 1 { return nil } return server.ErrReqReachLimit } ================================================ FILE: serverplugin/req_rate_limiting_redis.go ================================================ package serverplugin import ( "context" "time" "github.com/go-redis/redis_rate/v10" "github.com/redis/go-redis/v9" "github.com/smallnest/rpcx/protocol" "github.com/smallnest/rpcx/server" ) var _ server.PostReadRequestPlugin = (*RedisRateLimitingPlugin)(nil) // RedisRateLimitingPlugin can limit requests per unit time type RedisRateLimitingPlugin struct { addrs []string limiter redis_rate.Limiter limit redis_rate.Limit } // NewRedisRateLimitingPlugin creates a new RateLimitingPlugin func NewRedisRateLimitingPlugin(addrs []string, rate int, burst int, period time.Duration) *RedisRateLimitingPlugin { limit := redis_rate.Limit{ Rate: rate, Burst: burst, Period: period, } rdb := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: addrs, }) limiter := redis_rate.NewLimiter(rdb) return &RedisRateLimitingPlugin{ addrs: addrs, limiter: *limiter, limit: limit, } } // PostReadRequest can limit request processing. func (plugin *RedisRateLimitingPlugin) PostReadRequest(ctx context.Context, r *protocol.Message, e error) error { res, err := plugin.limiter.Allow(ctx, r.ServicePath+"/"+r.ServiceMethod, plugin.limit) if err != nil { return err } if res.Allowed > 0 { return nil } return server.ErrReqReachLimit } ================================================ FILE: serverplugin/tee.go ================================================ package serverplugin import ( "io" "net" ) // TeeConnPlugin is a plugin that copy requests from clients and send to a io.Writer. type TeeConnPlugin struct { w io.Writer } func NewTeeConnPlugin(w io.Writer) *TeeConnPlugin { return &TeeConnPlugin{w: w} } // Update can start a stream copy by setting a non-nil w. // If you set a nil w, it doesn't copy stream. func (plugin *TeeConnPlugin) Update(w io.Writer) { plugin.w = w } // HandleConnAccept check ip. func (plugin *TeeConnPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) { tc := &teeConn{conn, plugin.w} return tc, true } type teeConn struct { net.Conn w io.Writer } func (t *teeConn) Read(p []byte) (n int, err error) { n, err = t.Conn.Read(p) if n > 0 && t.w != nil { t.w.Write(p[:n]) // if _, err := t.w.Write(p[:n]); err != nil { // return n, err //discard error // } } return } ================================================ FILE: serverplugin/whitelist.go ================================================ package serverplugin import "net" // WhitelistPlugin is a plugin that control only ip addresses in whitelist can access services. type WhitelistPlugin struct { Whitelist map[string]bool WhitelistMask []*net.IPNet // net.ParseCIDR("172.17.0.0/16") to get *net.IPNet } // HandleConnAccept check ip. func (plugin *WhitelistPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) { ip, _, err := net.SplitHostPort(conn.RemoteAddr().String()) if err != nil { return conn, false } if plugin.Whitelist[ip] { return conn, true } remoteIP := net.ParseIP(ip) for _, mask := range plugin.WhitelistMask { if mask.Contains(remoteIP) { return conn, true } } return conn, false } ================================================ FILE: share/context.go ================================================ package share import ( "context" "fmt" "reflect" "sync" ) // var _ context.Context = &Context{} // Context is a rpcx customized Context that can contains multiple values. type Context struct { tagsLock *sync.Mutex tags map[interface{}]interface{} context.Context } func NewContext(ctx context.Context) *Context { tagsLock := &sync.Mutex{} ctx = context.WithValue(ctx, ContextTagsLock, tagsLock) return &Context{ tagsLock: tagsLock, Context: ctx, tags: map[interface{}]interface{}{isShareContext: true}, } } func (c *Context) Lock() { c.tagsLock.Lock() } func (c *Context) Unlock() { c.tagsLock.Unlock() } func (c *Context) Value(key interface{}) interface{} { c.tagsLock.Lock() defer c.tagsLock.Unlock() if c.tags == nil { c.tags = make(map[interface{}]interface{}) } if v, ok := c.tags[key]; ok { return v } return c.Context.Value(key) } func (c *Context) SetValue(key, val interface{}) { c.tagsLock.Lock() defer c.tagsLock.Unlock() if c.tags == nil { c.tags = make(map[interface{}]interface{}) } c.tags[key] = val } // DeleteKey delete the kv pair by key. func (c *Context) DeleteKey(key interface{}) { c.tagsLock.Lock() defer c.tagsLock.Unlock() if c.tags == nil || key == nil { return } delete(c.tags, key) } func (c *Context) String() string { return fmt.Sprintf("%v.WithValue(%v)", c.Context, c.tags) } func WithValue(parent context.Context, key, val interface{}) *Context { if key == nil { panic("nil key") } if !reflect.TypeOf(key).Comparable() { panic("key is not comparable") } tags := make(map[interface{}]interface{}) tags[key] = val return &Context{Context: parent, tags: tags, tagsLock: &sync.Mutex{}} } func WithLocalValue(ctx *Context, key, val interface{}) *Context { if key == nil { panic("nil key") } if !reflect.TypeOf(key).Comparable() { panic("key is not comparable") } if ctx.tags == nil { ctx.tags = make(map[interface{}]interface{}) } ctx.tags[key] = val return ctx } // IsShareContext checks whether a context is share.Context. func IsShareContext(ctx context.Context) bool { ok := ctx.Value(isShareContext) return ok != nil } ================================================ FILE: share/context_test.go ================================================ package share import ( "context" "testing" "github.com/stretchr/testify/assert" ) var ( TheAnswer = "Answer to the Ultimate Question of Life, the Universe, and Everything" MagicNumber = 42 ) func TestContext(t *testing.T) { rpcxContext := NewContext(context.Background()) assert.NotNil(t, rpcxContext.Context) assert.NotNil(t, rpcxContext.tags) rpcxContext.SetValue("string", TheAnswer) rpcxContext.SetValue(42, MagicNumber) assert.Equal(t, MagicNumber, rpcxContext.Value(42)) assert.Equal(t, TheAnswer, rpcxContext.Value("string")) rpcxContext.SetValue("string", TheAnswer) t.Log(rpcxContext.String()) } func TestWithValue(t *testing.T) { ctx := WithValue(context.Background(), "key", "value") assert.NotNil(t, ctx.tags) } func TestWithLocalValue(t *testing.T) { var c = NewContext(context.Background()) c.SetValue("key", "value") ctx := WithLocalValue(c, "MagicNumber", "42") assert.Equal(t, "value", ctx.tags["key"]) assert.Equal(t, "42", ctx.tags["MagicNumber"]) } ================================================ FILE: share/share.go ================================================ package share import ( "github.com/smallnest/rpcx/codec" "github.com/smallnest/rpcx/protocol" ) const ( // DefaultRPCPath is used by ServeHTTP. DefaultRPCPath = "/_rpcx_" // AuthKey is used in metadata. AuthKey = "__AUTH" // ServerAddress is used to get address of the server by client ServerAddress = "__ServerAddress" // ServerTimeout timeout value passed from client to control timeout of server ServerTimeout = "__ServerTimeout" // SendFileServiceName is name of the file transfer service. SendFileServiceName = "_filetransfer" // StreamServiceName is name of the stream service. StreamServiceName = "_streamservice" // ContextTagsLock is name of the Context TagsLock. ContextTagsLock = "_tagsLock" // _isShareContext indicates this context is share.Contex. isShareContext = "_isShareContext" ) // Trace is a flag to write a trace log or not. // You should not enable this flag for product environment and enable it only for test. // It writes trace log with logger Debug level. var Trace bool // Codecs are codecs supported by rpcx. You can add customized codecs in Codecs. var Codecs = map[protocol.SerializeType]codec.Codec{ protocol.SerializeNone: &codec.ByteCodec{}, protocol.JSON: &codec.JSONCodec{}, protocol.ProtoBuffer: &codec.PBCodec{}, protocol.MsgPack: &codec.MsgpackCodec{}, protocol.Thrift: &codec.ThriftCodec{}, } // RegisterCodec register customized codec. func RegisterCodec(t protocol.SerializeType, c codec.Codec) { Codecs[t] = c } // ContextKey defines key type in context. type ContextKey string // ReqMetaDataKey is used to set metadata in context of requests. var ReqMetaDataKey = ContextKey("__req_metadata") // ResMetaDataKey is used to set metadata in context of responses. var ResMetaDataKey = ContextKey("__res_metadata") // FileTransferArgs args from clients. type FileTransferArgs struct { FileName string `json:"file_name,omitempty"` FileSize int64 `json:"file_size,omitempty"` Meta map[string]string `json:"meta,omitempty"` } // Clone clones this DownloadFileArgs. func (args FileTransferArgs) Clone() *FileTransferArgs { meta := make(map[string]string) for k, v := range args.Meta { meta[k] = v } return &FileTransferArgs{ FileName: args.FileName, FileSize: args.FileSize, Meta: meta, } } // FileTransferReply response to token and addr to clients. type FileTransferReply struct { Token []byte `json:"token,omitempty"` Addr string `json:"addr,omitempty"` } // DownloadFileArgs args from clients. type DownloadFileArgs struct { FileName string `json:"file_name,omitempty"` Meta map[string]string `json:"meta,omitempty"` } // Clone clones this DownloadFileArgs. func (args DownloadFileArgs) Clone() *DownloadFileArgs { meta := make(map[string]string) for k, v := range args.Meta { meta[k] = v } return &DownloadFileArgs{ FileName: args.FileName, Meta: meta, } } // StreamServiceArgs is the request type for stream service. type StreamServiceArgs struct { Meta map[string]string `json:"meta,omitempty"` } // StreamServiceReply is the reply type for stream service. type StreamServiceReply struct { Token []byte `json:"token,omitempty"` Addr string `json:"addr,omitempty"` } ================================================ FILE: share/share_test.go ================================================ package share import ( "testing" "github.com/smallnest/rpcx/protocol" "github.com/stretchr/testify/assert" ) type MockCodec struct {} func (codec MockCodec) Encode(i interface{}) ([]byte, error) { return nil, nil } func (codec MockCodec) Decode(data []byte, i interface{}) error { return nil } func TestShare(t *testing.T) { registeredCodecNum := len(Codecs) codec := MockCodec{} mockCodecType := 127 RegisterCodec(protocol.SerializeType(mockCodecType), codec) assert.Equal(t, registeredCodecNum + 1, len(Codecs)) } ================================================ FILE: tool/xgen/README.md ================================================ # xgen `xgen` isn a tool that can help you generate a server stub for rpcx services. It search structs in your specified files and add them as services. Currently it doesn't support registring functions. ## Usage ```sh # install go get -u github.com/smallnest/rpcx/tool/xgen/... # run xgen -o server.go .go ``` The above will generate server.go containing a rpcx which registers all exported struct types contained in `.go`. ## Options ``` -o string specify the filename of the output -pkg process the whole package instead of just the given file -r string registry type. support etcd, consul, zookeeper, mdns (default "etcd") -tags string build tags to add to generated file ``` You can run as: ```sh xgen [options] .go .go .go ``` for example, `xgen -o server.go a.go b.go /User/abc/go/src/github.com/abc/aaa/c.go` or ```sh xgen [options] ``` for example, `xgen -o server.go github.com/abc/aaa github.com/abc/bbb github.com/abc/ccc` ================================================ FILE: tool/xgen/parser/parser.go ================================================ // Package parser parses Go code and keeps track of all the types defined // and provides access to all the constants defined for an int type. package parser import ( "go/ast" "go/build" "go/parser" "go/token" "unicode" "unicode/utf8" ) type Parser struct { PkgPath string PkgName string PkgFullName string StructNames map[string]bool } type visitor struct { *Parser name string explicit bool } func isExported(name string) bool { rune, _ := utf8.DecodeRuneInString(name) return unicode.IsUpper(rune) } func (v *visitor) Visit(n ast.Node) (w ast.Visitor) { switch n := n.(type) { case *ast.Package: return v case *ast.File: v.PkgName = n.Name.String() return v case *ast.GenDecl: return v case *ast.TypeSpec: v.name = n.Name.String() // Allow to specify non-structs explicitly independent of '-all' flag. if v.explicit { v.StructNames[v.name] = true return nil } return v case *ast.StructType: return nil case *ast.FuncDecl: if isExported(v.name) { if n.Type.Params.NumFields() == 3 && n.Type.Results.NumFields() == 1 { params := n.Type.Params.List result := n.Type.Results.List[0] if result.Type.(*ast.Ident).Name != "error" { return nil } if p, ok := params[0].Type.(*ast.SelectorExpr); ok { x := p.X.(*ast.Ident) s := p.Sel if x.Name+"."+s.Name == "context.Context" { v.StructNames[v.name] = true } } } } return nil } return nil } func (p *Parser) Parse(fname string, isDir bool) error { p.PkgPath = build.Default.GOPATH fset := token.NewFileSet() if isDir { packages, err := parser.ParseDir(fset, fname, nil, parser.ParseComments) if err != nil { return err } for _, pckg := range packages { ast.Walk(&visitor{Parser: p}, pckg) } } else { f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments) if err != nil { return err } ast.Walk(&visitor{Parser: p}, f) } return nil } ================================================ FILE: tool/xgen/xgen.go ================================================ package main import ( "flag" "fmt" "go/build" "io" "log" "os" "path/filepath" "github.com/smallnest/rpcx/tool/xgen/parser" ) var ( processPkg = flag.Bool("pkg", false, "process the whole package instead of just the given file") specifiedName = flag.String("o", "", "specify the filename of the output") buildTags = flag.String("tags", "", "build tags to add to generated file") registry = flag.String("r", "", "registry type. support etcd, consul, zookeeper, mdns") ) func main() { flag.Parse() files := flag.Args() if *processPkg { if len(flag.Args()) == 0 { log.Fatalf("not set packages") } var pkgFiles []string for _, f := range files { pkgFiles = append(pkgFiles, filepath.Join(filepath.Join(build.Default.GOPATH, "src", f))) } files = pkgFiles } if len(files) == 0 { flag.Usage() os.Exit(1) } var parsers []*parser.Parser for _, fname := range files { fInfo, err := os.Stat(fname) if err != nil { panic(err) } if !filepath.IsAbs(fname) { fname, err = filepath.Abs(fname) if err != nil { panic(err) } } relDir, err := filepath.Rel(filepath.Join(build.Default.GOPATH, "src"), fname) if err != nil { fmt.Printf("provided directory not under GOPATH (%s): %v", build.Default.GOPATH, err) return } p := &parser.Parser{PkgFullName: relDir, StructNames: make(map[string]bool)} if err := p.Parse(fname, fInfo.IsDir()); err != nil { fmt.Printf("Error parsing %v: %v", fname, err) return } parsers = append(parsers, p) _ = generate(parsers) } } func generate(parsers []*parser.Parser) error { var w io.Writer if *specifiedName == "" { w = os.Stdout } else { f, err := os.Create(*specifiedName) if err != nil { return err } defer f.Close() w = f } if *buildTags != "" { fmt.Fprintln(w, "// +build ", *buildTags) fmt.Fprintln(w) } fmt.Fprintln(w, "// AUTOGENERATED FILE: rpcx server stub code") fmt.Fprintln(w, "// compilable during generation.") fmt.Fprintln(w) fmt.Fprintln(w, "package main") fmt.Fprintln(w) fmt.Fprintln(w, "import (") fmt.Fprintln(w, ` "flag"`) fmt.Fprintln(w, ` "time"`) fmt.Fprintln(w) fmt.Fprintln(w, ` metrics "github.com/rcrowley/go-metrics"`) fmt.Fprintln(w, ` "github.com/smallnest/rpcx/server"`) fmt.Fprintln(w, ` "github.com/smallnest/rpcx/serverplugin"`) var importedPackages = make(map[string]bool) for _, p := range parsers { if !importedPackages[p.PkgFullName] { fmt.Fprintln(w, ` "`+p.PkgFullName+`"`) importedPackages[p.PkgFullName] = true } } fmt.Fprintln(w, ")") fmt.Fprintln(w) var mainFn = ` var ( addr = flag.String("addr", "localhost:8972", "server address")` if *registry == "etcd" || *registry == "consul" || *registry == "zookeeper" { mainFn = mainFn + ` rAddr = flag.String("rAddr", "localhost:2379", "register address")` } mainFn = mainFn + ` basePath = flag.String("base", "/rpcx", "prefix path") ) func main() { flag.Parse() _ = time.Second _ = metrics.UseNilMetrics _ = serverplugin.GetFunctionName s := server.NewServer() addRegistryPlugin(s) registerServices(s) s.Serve("tcp", *addr) }` fmt.Fprintln(w, mainFn) fmt.Fprintln(w, `func registerServices(s *server.Server) {`) for _, p := range parsers { for n := range p.StructNames { fmt.Fprintln(w, ` s.Register(new(`+p.PkgName+"."+n+`), "")`) } } fmt.Fprintln(w, `}`) useRegistry := false fmt.Fprintln(w, `func addRegistryPlugin(s *server.Server) {`) switch *registry { case "etcd": fmt.Fprintln(w, ` // add registery r := &serverplugin.EtcdRegisterPlugin{ ServiceAddress: "tcp@" + *addr, EtcdServers: []string{*rAddr}, BasePath: *basePath, Metrics: metrics.NewRegistry(), UpdateInterval: time.Minute, }`) useRegistry = true case "consul": fmt.Fprintln(w, ` // add registery r := &serverplugin.ConsulRegisterPlugin{ ServiceAddress: "tcp@" + *addr, ConsulServers: []string{*rAddr}, BasePath: *basePath, Metrics: metrics.NewRegistry(), UpdateInterval: time.Minute, }`) useRegistry = true case "zookeeper": fmt.Fprintln(w, ` // add registery r := &serverplugin.ZooKeeperRegisterPlugin{ ServiceAddress: "tcp@" + *addr, ZooKeeperServers: []string{*rAddr}, BasePath: *basePath, Metrics: metrics.NewRegistry(), UpdateInterval: time.Minute, }`) useRegistry = true case "mdns": fmt.Fprintln(w, ` r := serverplugin.NewMDNSRegisterPlugin("tcp@"+*addr, 8972, metrics.NewRegistry(), time.Minute, "")`) useRegistry = true default: fmt.Fprintln(w, ` `) } if useRegistry { fmt.Fprintln(w, ` err := r.Start() if err != nil { //log.Fatal(err) } s.Plugins.Add(r)`) } fmt.Fprintln(w, `}`) return nil } ================================================ FILE: util/buffer_pool.go ================================================ package util import ( "math" "sync" ) type levelPool struct { size int pool sync.Pool } func newLevelPool(size int) *levelPool { return &levelPool{ size: size, pool: sync.Pool{ New: func() interface{} { data := make([]byte, size) return &data }, }, } } type LimitedPool struct { minSize int maxSize int pools []*levelPool } func NewLimitedPool(minSize, maxSize int) *LimitedPool { if maxSize < minSize { panic("maxSize can't be less than minSize") } const multiplier = 2 var pools []*levelPool curSize := minSize for curSize < maxSize { pools = append(pools, newLevelPool(curSize)) curSize *= multiplier } pools = append(pools, newLevelPool(maxSize)) return &LimitedPool{ minSize: minSize, maxSize: maxSize, pools: pools, } } func (p *LimitedPool) findPool(size int) *levelPool { if size > p.maxSize { return nil } idx := int(math.Ceil(math.Log2(float64(size) / float64(p.minSize)))) if idx < 0 { idx = 0 } if idx > len(p.pools)-1 { return nil } return p.pools[idx] } func (p *LimitedPool) findPutPool(size int) *levelPool { if size > p.maxSize { return nil } if size < p.minSize { return nil } idx := int(math.Floor(math.Log2(float64(size) / float64(p.minSize)))) if idx < 0 { idx = 0 } if idx > len(p.pools)-1 { return nil } return p.pools[idx] } func (p *LimitedPool) Get(size int) *[]byte { sp := p.findPool(size) if sp == nil { data := make([]byte, size) return &data } buf := sp.pool.Get().(*[]byte) *buf = (*buf)[:size] return buf } func (p *LimitedPool) Put(b *[]byte) { sp := p.findPutPool(cap(*b)) if sp == nil { return } *b = (*b)[:cap(*b)] sp.pool.Put(b) } ================================================ FILE: util/buffer_pool_test.go ================================================ package util import ( "fmt" "testing" ) func TestLimitedPool_findPool(t *testing.T) { pool := NewLimitedPool(512, 4096) tests := []struct { args int want int }{ {200, 512}, {512, 512}, {1000, 1024}, {2000, 2048}, {2048, 2048}, {4095, 4096}, {4096, 4096}, {4097, -1}, } for _, tt := range tests { t.Run(fmt.Sprintf("bytes-%d", tt.args), func(t *testing.T) { got := pool.findPool(tt.args) if got == nil { if tt.want > 0 { fmt.Printf("expect %d pool but got nil", tt.want) } return } if got.size != tt.want { fmt.Printf("expect %d pool but got %d pool", tt.want, got.size) } }) } } func TestLimitedPool_findPutPool(t *testing.T) { pool := NewLimitedPool(512, 4096) tests := []struct { args int want int }{ {200, -1}, //too small so we discard it {512, 512}, {1000, 512}, {2000, 1024}, {2048, 2048}, {4095, 2048}, {4096, 4096}, {4097, -1}, // too big so we discard it } for _, tt := range tests { t.Run(fmt.Sprintf("bytes-%d", tt.args), func(t *testing.T) { got := pool.findPutPool(tt.args) if got == nil { if tt.want > 0 { fmt.Printf("expect %d pool but got nil", tt.want) } return } if got.size != tt.want { fmt.Printf("expect %d pool but got %d pool", tt.want, got.size) } }) } } ================================================ FILE: util/compress.go ================================================ package util import ( "bytes" "compress/gzip" "io" "sync" ) var ( spWriter sync.Pool spReader sync.Pool spBuffer sync.Pool ) func init() { spWriter = sync.Pool{New: func() interface{} { return gzip.NewWriter(nil) }} spReader = sync.Pool{New: func() interface{} { return new(gzip.Reader) }} spBuffer = sync.Pool{New: func() interface{} { return bytes.NewBuffer(nil) }} } // Unzip unzips data. func Unzip(data []byte) ([]byte, error) { buf := bytes.NewBuffer(data) gr := spReader.Get().(*gzip.Reader) defer func() { spReader.Put(gr) }() err := gr.Reset(buf) if err != nil { return nil, err } defer gr.Close() data, err = io.ReadAll(gr) if err != nil { return nil, err } return data, err } // Zip zips data. func Zip(data []byte) ([]byte, error) { buf := spBuffer.Get().(*bytes.Buffer) w := spWriter.Get().(*gzip.Writer) w.Reset(buf) defer func() { buf.Reset() spBuffer.Put(buf) w.Close() spWriter.Put(w) }() _, err := w.Write(data) if err != nil { return nil, err } err = w.Flush() if err != nil { return nil, err } err = w.Close() if err != nil { return nil, err } dec := buf.Bytes() return dec, nil } ================================================ FILE: util/compress_test.go ================================================ package util import ( "bytes" "compress/gzip" "io" "testing" ) func TestZip(t *testing.T) { s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D" t.Logf("origin len: %d", len(s)) data, err := Zip([]byte(s)) if err != nil { t.Fatalf("failed to zip: %v", err) } t.Logf("zipped len: %d", len(data)) s2, err := Unzip(data) if err != nil { t.Fatalf("failed to unzip: %v", err) } if string(s2) != s { t.Fatalf("unzip data is wrong") } } func BenchmarkZip(b *testing.B) { s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D" b.RunParallel(func(pb *testing.PB) { for pb.Next() { data, err := Zip([]byte(s)) if err != nil { b.Errorf("failed to zip: %v", err) } _ = data } }) } func BenchmarkUnzip(b *testing.B) { s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D" data, err := Zip([]byte(s)) if err != nil { b.Fatalf("failed to zip: %v", err) } b.RunParallel(func(pb *testing.PB) { for pb.Next() { s2, err := Unzip(data) if err != nil { b.Errorf("failed to zip: %v", err) } _ = s2 } }) } func oldUnzip(data []byte) ([]byte, error) { buf := spBuffer.Get().(*bytes.Buffer) defer func() { buf.Reset() spBuffer.Put(buf) }() _, err := buf.Write(data) if err != nil { return nil, err } gr := spReader.Get().(*gzip.Reader) defer func() { spReader.Put(gr) }() err = gr.Reset(buf) if err != nil { return nil, err } defer gr.Close() data, err = io.ReadAll(gr) if err != nil { return nil, err } return data, err } func BenchmarkUnzip_Old(b *testing.B) { s := "%5B%7B%22service%22%3A%22AttrDict%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%2C%7B%22service%22%3A%22BrasInfo%22%2C%22service_address%22%3A%22udp%40127.0.0.1%3A5353%22%7D%5D" data, err := Zip([]byte(s)) if err != nil { b.Fatalf("failed to zip: %v", err) } b.RunParallel(func(pb *testing.PB) { for pb.Next() { s2, err := oldUnzip(data) if err != nil { b.Errorf("failed to zip: %v", err) } _ = s2 } }) } ================================================ FILE: util/converter.go ================================================ package util import ( "unsafe" ) func SliceByteToString(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } func StringToSliceByte(s string) []byte { x := (*[2]uintptr)(unsafe.Pointer(&s)) h := [3]uintptr{x[0], x[1], x[1]} return *(*[]byte)(unsafe.Pointer(&h)) } func CopyMeta(src, dst map[string]string) { if dst == nil { return } for k, v := range src { dst[k] = v } } ================================================ FILE: util/net.go ================================================ package util import ( "errors" "fmt" "net" "net/url" "sort" "strconv" "strings" ) // GetFreePort gets a free port. func GetFreePort() (port int, err error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return 0, err } defer listener.Close() return listener.Addr().(*net.TCPAddr).Port, nil } // ParseRpcxAddress parses rpcx address such as tcp@127.0.0.1:8972 quic@192.168.1.1:9981 func ParseRpcxAddress(addr string) (network string, ip string, port int, err error) { ati := strings.Index(addr, "@") if ati <= 0 { return "", "", 0, fmt.Errorf("invalid rpcx address: %s", addr) } network = addr[:ati] addr = addr[ati+1:] var portstr string ip, portstr, err = net.SplitHostPort(addr) if err != nil { return "", "", 0, err } port, err = strconv.Atoi(portstr) return network, ip, port, err } func ConvertMeta2Map(meta string) map[string]string { var rt = make(map[string]string) if meta == "" { return rt } v, err := url.ParseQuery(meta) if err != nil { return rt } for key := range v { rt[key] = v.Get(key) } return rt } func ConvertMap2String(meta map[string]string) string { var buf strings.Builder keys := make([]string, 0, len(meta)) for k := range meta { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { vs := meta[k] keyEscaped := url.QueryEscape(k) if buf.Len() > 0 { buf.WriteByte('&') } buf.WriteString(keyEscaped) buf.WriteByte('=') buf.WriteString(url.QueryEscape(vs)) } return buf.String() } // ExternalIPV4 gets external IPv4 address of this server. func ExternalIPV4() (string, error) { ifaces, err := net.Interfaces() if err != nil { return "", err } for _, iface := range ifaces { if iface.Flags&net.FlagUp == 0 { continue // interface down } if iface.Flags&net.FlagLoopback != 0 { continue // loopback interface } addrs, err := iface.Addrs() if err != nil { return "", err } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil || ip.IsLoopback() { continue } ip = ip.To4() if ip == nil { continue // not an ipv4 address } return ip.String(), nil } } return "", errors.New("are you connected to the network?") } // ExternalIPV6 gets external IPv6 address of this server. func ExternalIPV6() (string, error) { ifaces, err := net.Interfaces() if err != nil { return "", err } for _, iface := range ifaces { if iface.Flags&net.FlagUp == 0 { continue // interface down } if iface.Flags&net.FlagLoopback != 0 { continue // loopback interface } addrs, err := iface.Addrs() if err != nil { return "", err } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil || ip.IsLoopback() { continue } ip = ip.To16() if ip == nil { continue // not an ipv4 address } return ip.String(), nil } } return "", errors.New("are you connected to the network?") } ================================================ FILE: util/net_test.go ================================================ package util import ( "net" "strconv" "testing" ) func TestGetFreePort(t *testing.T) { for i := 0; i < 1000; i++ { port, err := GetFreePort() if err != nil { t.Error(err) } if port == 0 { t.Error("GetFreePort() return 0") } } } var oldGetFreePort = func() (port int, err error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return 0, err } defer listener.Close() addr := listener.Addr().String() _, portString, err := net.SplitHostPort(addr) if err != nil { return 0, err } return strconv.Atoi(portString) } func BenchmarkGetFreePort_Old(b *testing.B) { for i := 0; i < b.N; i++ { oldGetFreePort() } } func BenchmarkGetFreePort_New(b *testing.B) { for i := 0; i < b.N; i++ { GetFreePort() } } func TestExternalIPV4(t *testing.T) { ip, err := ExternalIPV4() if err != nil { t.Fatal(err) } if ip == "" { t.Fatal("expect an IP but got empty") } t.Log(ip) } func TestExternalIPV6(t *testing.T) { ip, err := ExternalIPV6() if err != nil { t.Fatal(err) } if ip == "" { t.Fatal("expect an IP but got empty") } t.Log(ip) }