srpc

package module
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 8, 2025 License: Apache-2.0 Imports: 35 Imported by: 0

README

sRPC

English | 中文

sRPC is a lightweight RPC framework that implements the Connect RPC protocol with gRPC-compatible interfaces. It seamlessly connects browsers and backend services using unified Protocol Buffer definitions.

Go Reference Go Report Card License

Overview

sRPC combines the simplicity and browser compatibility of Connect with the robust interface design of gRPC. With a single API definition, you can build services that support both browser clients and native gRPC clients.

Why sRPC?

Connect is an excellent choice for new projects due to its simplicity and browser compatibility. sRPC, however, focuses on compatibility, offering a seamless way for existing gRPC projects to support HTTP and web clients without significant code changes. With sRPC, you can:

  • Retain gRPC Compatibility: Continue using your existing gRPC services and interfaces.
  • Expand Client Support: Enable HTTP and browser-based clients to interact with your services.
  • Minimize Migration Effort: Avoid rewriting or heavily modifying your existing gRPC code.

Features

  • Protocol Compatibility: Implements the Connect RPC protocol, supporting browser and gRPC-compatible HTTP APIs.
  • gRPC-Compatible Interface: Provides the same API experience as gRPC for seamless transitions.
  • Standard Library Compatibility: Works with both Go's standard library and gRPC code.
  • Lightweight Design: Focuses on core functionality without unnecessary complexity.
  • Full Streaming Support: Supports unary calls, server streaming, client streaming, and bidirectional streaming.
  • Interceptors and Middleware: Offers a flexible request/response processing pipeline.
  • Error Handling: Structured error types compatible with Go's standard errors and gRPC status codes.
  • Transport Agnostic: Supports HTTP/1.1 and HTTP/2.

Quick Start

Installation
go get github.com/opensraph/srpc
Define Services

Define your services using standard Protocol Buffers:

syntax = "proto3";

package srpc.examples.echo;

// EchoRequest represents the echo request.
message EchoRequest {
  string message = 1;
}

// EchoResponse represents the echo response.
message EchoResponse {
  string message = 1;
}

// Echo defines the echo service.
service Echo {
  // UnaryEcho is a unary echo.
  rpc UnaryEcho(EchoRequest) returns (EchoResponse) {}
  // ServerStreamingEcho is server-side streaming.
  rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {}
  // ClientStreamingEcho is client-side streaming.
  rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {}
  // BidirectionalStreamingEcho is bidirectional streaming.
  rpc BidirectionalStreamingEcho(stream EchoRequest) returns (stream EchoResponse) {}
}
Server Implementation

sRPC's server API is fully compatible with gRPC, allowing seamless migration of existing gRPC services. Below is an example of a server implementation with authentication and logging interceptors:

package main

import (
    "context"
    "flag"
    "fmt"
    "io"
    "log"
    "net"
    "time"

    "github.com/opensraph/srpc"
    "github.com/opensraph/srpc/errors"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/examples/data"
    "google.golang.org/grpc/metadata"

    pb "google.golang.org/grpc/examples/features/proto/echo"
)

var (
    port = flag.Int("port", 50051, "the port to serve on")

    errMissingMetadata = errors.Newf("missing metadata").WithCode(errors.InvalidArgument)
    errInvalidToken    = errors.Newf("invalid token").WithCode(errors.Unauthenticated)
)

type server struct {
    pb.UnimplementedEchoServer
}

func (s *server) UnaryEcho(_ context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
    fmt.Printf("unary echoing message %q\n", in.Message)
    return &pb.EchoResponse{Message: in.Message}, nil
}

func (s *server) BidirectionalStreamingEcho(stream pb.Echo_BidirectionalStreamingEchoServer) error {
    for {
        in, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                return nil
            }
            fmt.Printf("server: error receiving from stream: %v\n", err)
            return err
        }
        fmt.Printf("bidi echoing message %q\n", in.Message)
        stream.Send(&pb.EchoResponse{Message: in.Message})
    }
}

func main() {
    flag.Parse()

    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    creds, err := credentials.NewServerTLSFromFile(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem"))
    if err != nil {
        log.Fatalf("failed to create credentials: %v", err)
    }

    s := srpc.NewServer(
        srpc.Creds(creds),
        srpc.UnaryInterceptor(unaryInterceptor),
        srpc.StreamInterceptor(streamInterceptor),
    )

    pb.RegisterEchoServer(s, &server{})

    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
Client Usage

The client API is also gRPC-compatible while supporting standard library-style calling patterns. Below is an example of a client implementation with logging and token injection interceptors:

package main

import (
    "context"
    "flag"
    "fmt"
    "io"
    "log"
    "time"

    "github.com/opensraph/srpc"
    "golang.org/x/oauth2"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/credentials/oauth"
    "google.golang.org/grpc/examples/data"
    ecpb "google.golang.org/grpc/examples/features/proto/echo"
)

var addr = flag.String("addr", "localhost:50051", "the address to connect to")

const fallbackToken = "some-secret-token"

func callUnaryEcho(client ecpb.EchoClient, message string) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    resp, err := client.UnaryEcho(ctx, &ecpb.EchoRequest{Message: message})
    if err != nil {
        log.Fatalf("client.UnaryEcho(_) = _, %v: ", err)
    }
    fmt.Println("UnaryEcho: ", resp.Message)
}

func callBidiStreamingEcho(client ecpb.EchoClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    c, err := client.BidirectionalStreamingEcho(ctx)
    if err != nil {
        return
    }
    for i := 0; i < 5; i++ {
        if err := c.Send(&ecpb.EchoRequest{Message: fmt.Sprintf("Request %d", i+1)}); err != nil {
            log.Fatalf("failed to send request due to error: %v", err)
        }
    }
    c.CloseSend()
    for {
        resp, err := c.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("failed to receive response due to error: %v", err)
        }
        fmt.Println("BidiStreaming Echo: ", resp.Message)
    }
}

func main() {
    flag.Parse()

    creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca_cert.pem"), "x.test.example.com")
    if err != nil {
        log.Fatalf("failed to load credentials: %v", err)
    }

    conn, err := srpc.NewClient(*addr,
        srpc.WithGRPCOptions(
            grpc.WithTransportCredentials(creds),
        ),
        srpc.WithUnaryInterceptor(unaryInterceptor),
        srpc.WithStreamInterceptor(streamInterceptor),
    )
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    client := ecpb.NewEchoClient(conn)

    callUnaryEcho(client, "hello world")
    callBidiStreamingEcho(client)
}
Testing with cURL

You can test the UnaryEcho method of the sRPC server using cURL. Below is an example:

curl \
    --insecure \
    --header "Content-Type: application/json" \
    --data '{"message": "Hello sRPC"}' \
    https://localhost:50051/grpc.examples.echo.Echo/UnaryEcho

Compatibility Design

sRPC is carefully designed to be compatible with both Go's standard library and gRPC:

Standard Library Compatibility
  • Error handling uses the standard error interface
package main

import (
    "github.com/opensraph/srpc/errors"
    "google.golang.org/protobuf/types/known/anypb"
)

func main() {
    var err *errors.Error
    err = errors.New("an error occurred").WithCode(errors.InvalidArgument)
    err.WithDetails(&anypb.Any{
        TypeUrl: "type.googleapis.com/google.protobuf.StringValue",
        Value:   []byte("value"),
    })
    err.WithDetailFromMap(map[string]any{
        "key": "value",
    })
}
gRPC Compatibility
  • Provides the same registration interfaces as grpc.ServiceRegistrar
  • Uses the same service descriptor structures

Examples

The repository contains working examples that demonstrate various use cases:

  • Basic Server: A simple sRPC server implementation with authentication and logging interceptors. Demonstrates unary and bidirectional streaming RPCs, token validation, and TLS-based credentials.
  • Basic Client: A corresponding client implementation showcasing unary and bidirectional streaming RPC calls, logging and token injection interceptors, and TLS-based secure connections.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrServerStopped = errors.New("srpc: the server has been stopped")
)

Functions

func NewClient

func NewClient(target string, opt ...ClientOption) (*client, error)

func NewServer

func NewServer(opt ...ServerOption) *server

Types

type CallOption

type CallOption = grpc.CallOption

type Client

type Client interface {
	grpc.ClientConnInterface
}

type ClientConn

type ClientConn = grpc.ClientConn

type ClientOption

type ClientOption func(o *clientOptions)

func WithChainStreamInterceptor

func WithChainStreamInterceptor(Interceptors ...StreamClientInterceptor) ClientOption

func WithChainUnaryInterceptor

func WithChainUnaryInterceptor(Interceptors ...UnaryClientInterceptor) ClientOption

func WithGRPCOptions

func WithGRPCOptions(opts ...DialOption) ClientOption

func WithStreamInterceptor

func WithStreamInterceptor(interceptor StreamClientInterceptor) ClientOption

func WithUnaryInterceptor

func WithUnaryInterceptor(interceptor UnaryClientInterceptor) ClientOption

type ClientStream

type ClientStream = grpc.ClientStream

type DialOption

type DialOption = grpc.DialOption

type GRPCStreamDesc

type GRPCStreamDesc = grpc.StreamDesc

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler is an HTTP handler that serves RPC requests.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request)

ServeHTTP implements http.Handler.

type Implementation

type Implementation func(ctx context.Context, stream protocol.StreamingHandlerConn) error

Implementation is a function type that handles an RPC call with the given context and stream.

type Interceptor

type Interceptor struct {
	// contains filtered or unexported fields
}

Interceptor manages the RPC interceptor chains for both server and client-side interceptors, supporting both unary and streaming modes. It provides chainable methods to add multiple interceptors that will be executed in sequence.

Interceptors follow an "onion model" execution pattern: - For servers: The first added interceptor processes the request first and the response last - For clients: The first added interceptor processes outgoing messages first and incoming messages last

Request flow visualization:

 client.Send()       client.Receive()
       |                   ^
       v                   |
    A ---                 --- A
    B ---                 --- B
    : ...                 ... :
    Y ---                 --- Y
    Z ---                 --- Z
       |                   ^
       v                   |
  = = = = = = = = = = = = = = = =
               network
  = = = = = = = = = = = = = = = =
       |                   ^
       v                   |
    A ---                 --- A
    B ---                 --- B
    : ...                 ... :
    Y ---                 --- Y
    Z ---                 --- Z
       |                   ^
       v                   |
handler.Receive()   handler.Send()
       |                   ^
       |                   |
       '-> handler logic >-'

Note: In clients, Send handles request messages and Receive handles response messages. For handlers, it's the reverse. Depending on your interceptor's logic, you may need to wrap different methods in clients versus servers.

Interceptors are commonly used for implementing cross-cutting concerns like logging, authentication, error handling, metrics collection, and tracing.

func (*Interceptor) ChainStreamInterceptor

func (i *Interceptor) ChainStreamInterceptor(ints ...StreamServerInterceptor) *Interceptor

ChainStreamInterceptor adds stream server interceptors to the chain. See grpc.ChainStreamInterceptor for more details.

func (*Interceptor) ChainUnaryInterceptor

func (i *Interceptor) ChainUnaryInterceptor(ints ...UnaryServerInterceptor) *Interceptor

ChainUnaryInterceptor adds unary server interceptors to the chain. See grpc.ChainUnaryInterceptor for more details.

func (*Interceptor) StreamClientInterceptor

func (i *Interceptor) StreamClientInterceptor() StreamClientInterceptor

func (*Interceptor) StreamInterceptor

func (i *Interceptor) StreamInterceptor() StreamServerInterceptor

func (*Interceptor) UnaryClientInterceptor

func (i *Interceptor) UnaryClientInterceptor() UnaryClientInterceptor

func (*Interceptor) UnaryInterceptor

func (i *Interceptor) UnaryInterceptor() UnaryServerInterceptor

func (*Interceptor) WithChainStreamInterceptor

func (i *Interceptor) WithChainStreamInterceptor(ints ...StreamClientInterceptor) *Interceptor

WithChainStreamInterceptor adds stream client interceptors to the chain. See grpc.WithChainStreamInterceptor for more details.

func (*Interceptor) WithChainUnaryInterceptor

func (i *Interceptor) WithChainUnaryInterceptor(ints ...UnaryClientInterceptor) *Interceptor

WithChainUnaryInterceptor adds unary client interceptors to the chain. See WithChainUnaryInterceptor for more details.

type Server

type Server interface {
	grpc.ServiceRegistrar
	reflection.GRPCServer
	Serve(l net.Listener) error
	Stop()
	GracefulStop()
	Handle(pattern string, handler http.Handler)
}

type ServerOption

type ServerOption func(o *serverOptions)

func ChainStreamInterceptor

func ChainStreamInterceptor(ints ...StreamServerInterceptor) ServerOption

ChainStreamInterceptor returns a ServerOption that specifies the chained interceptor for stream RPCs. The first interceptor will be the outer most, while the last interceptor will be the inner most wrapper around the real call. All stream interceptors added by this method will be chained.

func ChainUnaryInterceptor

func ChainUnaryInterceptor(ints ...UnaryServerInterceptor) ServerOption

ChainUnaryInterceptor returns a ServerOption that specifies the chained interceptor for unary RPCs. The first interceptor will be the outer most, while the last interceptor will be the inner most wrapper around the real call. All unary interceptors added by this method will be chained.

func Codec

func Codec(codecs ...encoding.Codec) ServerOption

Codec returns a ServerOption that sets the codecs for the server.

func Compression

func Compression(name string, decompressor compress.Decompressor, compressor compress.Compressor) ServerOption

Compression returns a ServerOption that sets the compression algorithm

func CompressionMinBytes

func CompressionMinBytes(n int) ServerOption

CompressionMinBytes returns a ServerOption that sets the minimum number of bytes for compression to be applied. This is useful for tuning the performance of the server. If not set, the default value is 0, which means no minimum size. Compression will be applied to all messages.

func Creds

Creds returns a ServerOption that sets credentials for server connections.

func EnableTracing

func EnableTracing() ServerOption

EnableTracing returns a ServerOption that enables tracing for the server. This is useful for debugging and monitoring the server's performance. If not set, tracing is disabled by default.

func GlobalHandler added in v0.0.3

func GlobalHandler(h func(next http.Handler) http.Handler) ServerOption

GlobalHandler returns a ServerOption that sets the global handler for the server. This handler will be used for all requests that do not match any registered service or method.

func IdleTimeout

func IdleTimeout(d time.Duration) ServerOption

IdleTimeout returns a ServerOption that sets the idle timeout for the server.

func MaxConcurrentStreams

func MaxConcurrentStreams(n uint32) ServerOption

MaxConcurrentStreams returns a ServerOption that will apply a limit on the number of concurrent streams to each ServerTransport.

func MaxRecvMsgSize

func MaxRecvMsgSize(n int) ServerOption

MaxRecvMsgSize returns a ServerOption to set the max message size in bytes the server can receive. If this is not set, gRPC uses the default 4MB.

func MaxSendMsgSize

func MaxSendMsgSize(n int) ServerOption

MaxSendMsgSize returns a ServerOption to set the max message size in bytes the server can send. If this is not set, gRPC uses the default `math.MaxInt32`.

func ReadTimeout

func ReadTimeout(d time.Duration) ServerOption

ReadTimeout returns a ServerOption that sets the read timeout for the server.

func StreamInterceptor

func StreamInterceptor(i StreamServerInterceptor) ServerOption

StreamInterceptor returns a ServerOption that sets the StreamServerInterceptor for the server.

func UnaryInterceptor

func UnaryInterceptor(i UnaryServerInterceptor) ServerOption

UnaryInterceptor returns a ServerOption that sets the UnaryServerInterceptor for the server.

func UnknownHandler

func UnknownHandler(h http.Handler) ServerOption

UnknownHandler returns a ServerOption that sets the handler for unknown requests. This is useful for handling requests that do not match any registered service or method. The handler should be a http.Handler that can handle the request and return a response. If not set, the server will return a 404 Not Found response for unknown requests.

func WriteTimeout

func WriteTimeout(d time.Duration) ServerOption

WriteTimeout returns a ServerOption that sets the write timeout for the server.

type ServerStream

type ServerStream = grpc.ServerStream

type StreamClientInterceptor

type StreamClientInterceptor = grpc.StreamClientInterceptor

type StreamHandler

type StreamHandler = grpc.StreamHandler

type StreamServerInfo

type StreamServerInfo = grpc.StreamServerInfo

type StreamServerInterceptor

type StreamServerInterceptor = grpc.StreamServerInterceptor

type Streamer

type Streamer = grpc.Streamer

type UnaryClientInterceptor

type UnaryClientInterceptor = grpc.UnaryClientInterceptor

type UnaryHandler

type UnaryHandler = grpc.UnaryHandler

type UnaryInvoker

type UnaryInvoker = grpc.UnaryInvoker

type UnaryServerInfo

type UnaryServerInfo = grpc.UnaryServerInfo

type UnaryServerInterceptor

type UnaryServerInterceptor = grpc.UnaryServerInterceptor

Directories

Path Synopsis
examples
proto module
internal
srpcsync
Package srpcsync implements additional synchronization primitives built upon the sync package.
Package srpcsync implements additional synchronization primitives built upon the sync package.
mem
Package mem provides utilities that facilitate memory reuse in byte slices that are used as buffers.
Package mem provides utilities that facilitate memory reuse in byte slices that are used as buffers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL