Mini RPC is now a small TCP-based RPC runtime that can also work with Go code generated by the separate mini-protoc project. The runtime owns the network, framing, request/response handling, method registry, and JSON serialization. mini-protoc owns schema parsing and generated service glue.
The older README content is intentionally kept below this updated guide for background and learning notes.
Mini RPC currently provides:
- A public
rpcpackage withServer,Client,Registry, andHandlerFunc. - A TCP transport layer in
internal/transport. - A length-prefixed protocol layer in
internal/protocol. - A JSON codec implementation in
internal/codec. - A generated-code demo in
demo/user. - Server-side integration with
mini-protocgenerated registration functions.
Generated client types from mini-protoc still contain placeholder methods that call panic("not implemented"). The working client path today uses rpc.Client.Call(...) directly.
flowchart TD
A["demo/user/user.proto"] --> B["mini-protoc\nexternal project"]
B --> C["demo/user/user.pb.go"]
C --> D["UserRequest / UserResponse structs"]
C --> E["UserService interface"]
C --> F["RegisterUserService(server, service)"]
G["UserServiceImpl\ndemo/user/server/service.go"] --> F
H["rpc.Server"] --> F
F --> I["Registry methods\nUserService/GetUser\nUserService/CreateUser"]
J["rpc.Client.Call"] --> K["TCP frame"]
K --> L["rpc.Server.Handle"]
L --> I
I --> M["Decode request\ncall service\nencode response"]
mini-rpc/
├── rpc/ Public runtime API
│ ├── client.go RPC client request/response flow
│ ├── registry.go Method registry
│ └── server.go RPC server dispatch and codec helpers
├── internal/
│ ├── codec/ Codec interface and JSON implementation
│ ├── protocol/ Request/response messages and TCP framing
│ └── transport/ TCP client and server
├── demo/user/
│ ├── user.proto Schema used by mini-protoc
│ ├── user.pb.go Generated code from mini-protoc
│ ├── server/ Demo server implementation
│ └── client/ Demo client using rpc.Client.Call
├── go.mod
└── README.md
flowchart LR
A["demo/user/client"] --> B["rpc.Client"]
B --> C["JSON codec"]
C --> D["protocol.Request"]
D --> E["TCPClient\nWriteFrame"]
E --> F["TCPServer\nReadFrame"]
F --> G["rpc.Server.Handle"]
G --> H["Registry lookup"]
H --> I["Generated handler wrapper"]
I --> J["UserServiceImpl"]
J --> K["protocol.Response"]
K --> L["TCP response frame"]
L --> B
Start the server:
go run ./demo/user/serverIn another terminal, run the client:
go run ./demo/user/clientThe current demo client sends a UserRequest to UserService/GetUser and prints the decoded UserResponse.
mini-protoc lives in a separate project. From this repository, regenerate the demo code with:
go run github.com/akansha204/mini-protoc/cmd/mini-protoc@latest ./demo/user/user.protoDuring local development, if mini-protoc is checked out beside this repo, you can also run it from that checkout:
cd ../mini-prtoc
go run ./cmd/mini-protoc ../mini-rpc/demo/user/user.protoThe generated file is written next to the proto file:
demo/user/user.proto -> demo/user/user.pb.go
The generated demo/user/user.pb.go contains:
UserRequestandUserResponsestructs.UserServiceinterface.UserServiceClientandNewUserServiceClient().- Placeholder generated client methods that still panic.
RegisterUserService(server *rpc.Server, service UserService).
The registration function is the important runtime bridge. It registers method names like UserService/GetUser, decodes the request payload using server.Decode, calls the concrete UserService implementation, and encodes the response using server.Encode.
The demo server creates the runtime server, registers the generated service glue, and starts TCP transport:
jsonCodec := &codec.JSONCodec{}
server := rpc.NewServer(
jsonCodec,
rpc.NewRegistry(),
)
user.RegisterUserService(
server,
&UserServiceImpl{},
)
tcpServer := transport.NewTCPServer(
":8080",
server.Handle,
)
if err := tcpServer.Start(); err != nil {
log.Fatal(err)
}Your concrete service only needs to implement the generated interface:
type UserServiceImpl struct{}
func (s *UserServiceImpl) GetUser(req user.UserRequest) (user.UserResponse, error) {
return user.UserResponse{
Name: req.Name,
Age: req.Age,
}, nil
}Generated mini-protoc clients are not wired to the runtime yet. Use the runtime client directly:
tcpClient := transport.NewTCPClient(":8080")
if err := tcpClient.Connect(); err != nil {
log.Fatal(err)
}
defer tcpClient.Close()
client := rpc.NewClient(
&codec.JSONCodec{},
tcpClient,
)
req := user.UserRequest{
Name: "Akansha",
Age: 21,
}
var resp user.UserResponse
err := client.Call(
"UserService/GetUser",
req,
&resp,
)
if err != nil {
log.Fatal(err)
}- The client encodes the typed request struct into bytes.
rpc.Clientwraps those bytes in aprotocol.Requestwith an ID and method name.transport.TCPClientwrites the request as a length-prefixed TCP frame.transport.TCPServerreads the frame and passes it torpc.Server.Handle.rpc.Serverdecodes the request and looks up the method inRegistry.- The generated registration wrapper decodes the payload into the generated request type.
- Your service implementation runs.
- The wrapper encodes the typed response.
rpc.Serverwraps it in aprotocol.Response.- The client receives the response and decodes the payload into the result pointer.
- Generated mini-protoc client methods are placeholders and do not call the network yet.
- The runtime currently uses JSON serialization.
- Transport is raw TCP with length-prefixed frames.
- There is no timeout, retry, TLS, streaming, service discovery, or connection pooling yet.
internal/packages are intended for use inside this module; external users should prefer the publicrpcpackage unless they are working inside this repo.
A lightweight RPC (Remote Procedure Call) built from scratch in Go. This project teaches you how RPC systems work under the hood by building a complete, working implementation using raw TCP communication.
Mini RPC lets you call functions on a remote computer as if they were running locally. Imagine you have a server with a math calculator - Mini RPC allows a client program to ask the server to add two numbers, multiply them, or perform any other registered function, and get the answer back.
Think of it like this:
- Without RPC: Your program can only call functions that exist in the same program
- With RPC: Your program can call functions on a completely different computer over the network
This project shows exactly how frameworks like gRPC work internally by demonstrating all the moving parts.
Understanding RPC systems helps you understand:
- How network communication between programs works
- How data is serialized for transmission over the network
- How message protocols ensure reliable communication
- How services handle thousands of function calls from different clients simultaneously
- The foundation behind popular frameworks like gRPC, JSON-RPC, and others
This is perfect for learning because it strips away all the complexity and shows you the core concepts in their simplest form.
Here's the simplest explanation of how Mini RPC works:
- Client Side: You create a request that says "call the Add function with parameters 5 and 3"
- Sending: This request is converted to JSON, wrapped with size information, and sent over a TCP connection
- Server Side: The server reads your request, looks up the Add function, runs it with your parameters
- Response: The server sends back the result (8) as JSON over the same TCP connection
- Client Receives: Your program gets the result and uses it
What makes this work reliably is that we properly structure the messages (so the server knows where one message ends and the next begins), we keep the connection open (so we don't reconnect for every call), and everything is serialized in a standard format (JSON).
Mini RPC is built in four layers, each handling a specific responsibility:
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
│ (What you write with Mini RPC) │
├─────────────────────────────────────────────────────────────────┤
│ RPC Layer │
│ Handles request/response logic and routing │
│ - Client: Sends requests, receives responses │
│ - Server: Routes requests to handlers, sends responses │
│ - Registry: Keeps track of which function handles which call │
├─────────────────────────────────────────────────────────────────┤
│ Protocol Layer │
│ Structures messages with clear boundaries (length-prefixing) │
│ - Encoding/Decoding messages │
│ - Serialization (JSON, Protobuf, etc.) │
├─────────────────────────────────────────────────────────────────┤
│ Transport Layer │
│ Raw TCP socket communication │
│ (The actual bytes going over network) │
└─────────────────────────────────────────────────────────────────┘
Each layer does one thing well and builds on the layer below it. This keeps the code clean and easy to understand.
mini-rpc/
├── cmd/ # Command-line applications
│ ├── client/ # RPC client example
│ │ └── main.go # Client usage demonstration
│ └── server/ # RPC server example
│ └── main.go # Server setup and handler registration
│
├── internal/ # Internal packages (not exported)
│ ├── codec/ # Serialization/deserialization
│ │ ├── codec.go # Codec interface definition
│ │ └── json_codec.go # JSON-based implementation
│ │
│ ├── protocol/ # RPC protocol definitions
│ │ ├── framing.go # TCP frame serialization
│ │ └── messaging.go # Request/Response message structures
│ │
│ ├── rpc/ # RPC core logic
│ │ ├── client.go # RPC client implementation
│ │ ├── server.go # RPC server implementation
│ │ ├── registry.go # Method registry for dispatching
│ │ └── handler.go # Example method handlers (Calculator)
│ │
│ └── transport/ # Network communication
│ ├── tcp_client.go # TCP client connection handler
│ └── tcp_server.go # TCP server listener and handler
│
├── go.mod # Go module definition
└── README.md
The transport layer manages all network communication using TCP sockets.
TCPServer (tcp_server.go):
- Listens on a specified address (e.g.,
:8080) - Accepts incoming client connections
- Handles each connection concurrently using goroutines
- Forwards incoming data to a handler function
TCPClient (tcp_client.go):
- Connects to a remote RPC server
- Sends serialized messages to the server
- Uses the protocol layer for frame-based communication
Defines how RPC messages are structured and transmitted over the network.
Messaging (messaging.go):
- Request: Contains method name, parameters, and request ID
type Request struct { ID uint64 // Unique request identifier Method string // Method name to invoke Params []interface{} // Method parameters }
- Response: Contains result, error, and corresponding request ID
type Response struct { ID uint64 // Matches request ID Result interface{} // Method result Error string // Error message if any }
Framing (framing.go):
- Implements TCP frame serialization using a length-prefixed protocol
- Ensures reliable message boundaries over TCP stream
- Header: 4-byte big-endian integer representing payload length
- Max frame size: 4MB (prevents unbounded memory allocation)
- Functions:
WriteFrame()sends data,ReadFrame()receives data
Handles serialization and deserialization of data for network transmission.
Codec Interface (codec.go):
type Codec interface {
Encode(v interface{}) ([]byte, error) // Serialize to bytes
Decode(data []byte, v interface{}) error // Deserialize from bytes
}JSONCodec (json_codec.go):
- Default implementation using JSON serialization
- Easy to debug (human-readable format)
- Can be extended to support other formats (Protobuf, MessagePack, etc.)
Implements the actual RPC client and server logic.
Registry (registry.go):
- Maintains a map of method names to handler functions
- Allows registration of custom methods:
registry.Register("MethodName", handlerFunc) - Provides lookup:
registry.Get("MethodName")returns the handler
Server (server.go):
- Core RPC server logic that processes incoming requests
- Receives serialized request data
- Decodes request, looks up method in registry
- Executes the handler with provided parameters
- Encodes and returns response
- Handles errors gracefully with error messages in response
Client (client.go):
- Core RPC client logic for making remote calls
- Maintains connection to server via TCPClient
Call(method string, params []interface{}, result interface{})executes remote method- Encodes request, sends it, waits for and decodes response
- Returns result or error
Handlers (handler.go):
- Example implementation: Calculator with Add, Subtract, Mul, Div methods
- Shows how to implement handler functions matching
HandlerFuncsignature - Error handling inside handler returns to client
- Go 1.25.3 or higher
- TCP port 8080 available (configurable in examples)
Clone the repository:
git clone https://github.com/akansha204/mini-rpc.git
cd mini-rpcBuild the project:
go build ./cmd/server
go build ./cmd/clientTerminal 1 - Start the server:
go run ./cmd/serverExpected output:
mini-rpc Server Starting...
Server listening on :8080
Terminal 2 - Run the client:
go run ./cmd/clientExpected output:
Result 8 # Add(5, 3) = 8
Result 2 # Subtract(5, 3) = 2
Result 15 # Mul(5, 3) = 15
Result 1 # Div(5, 5) = 1
Create handler functions that match the HandlerFunc signature:
type HandlerFunc func(params []interface{}) (interface{}, error)Example: Create a greeter service
type Greeter struct{}
func (g *Greeter) Hello(params []interface{}) (interface{}, error) {
if len(params) != 1 {
return nil, fmt.Errorf("Hello expects 1 parameter")
}
name := params[0].(string)
return "Hello, " + name + "!", nil
}Instantiate registry and register handlers:
registry := rpc.NewRegistry()
greeter := &Greeter{}
registry.Register("Hello", greeter.Hello)jsonCodec := &codec.JSONCodec{}
rpcServer := rpc.NewServer(jsonCodec, registry)
tcpServer := transport.NewTCPServer(":8080", rpcServer.Handle)
if err := tcpServer.Start(); err != nil {
log.Fatal(err)
}// Connect to server
client := transport.NewTCPClient(":8080")
if err := client.Connect(); err != nil {
log.Fatal(err)
}
defer client.Close()
// Create RPC client
jsonCodec := &codec.JSONCodec{}
rpcClient := rpc.NewClient(jsonCodec, client)
// Call remote method
var result string
err := rpcClient.Call("Hello", []interface{}{"World"}, &result)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // Output: Hello, World!- Client creates
Requestwith unique ID, method name, and parameters - Client encodes request using codec to bytes
- Client frames the serialized bytes (4-byte length prefix)
- Client sends frame over TCP
- Server reads frame, extracting length and payload
- Server decodes payload to
Requestobject - Server looks up method in registry
- Server executes handler function with parameters
- Server creates
Responsewith result or error - Server encodes and frames response
- Server sends response back over TCP
- Client reads and parses response
- Client returns result or error to caller
TCP Frame Format:
┌─────────────┬──────────────┐
│ Header │ Payload │
│ (4 bytes) │ (Variable) │
├─────────────┼──────────────┤
│ Length │ JSON Data │
│ (uint32) │ │
└─────────────┴──────────────┘
Request JSON:
{
"id": 1,
"method": "Add",
"params": [5, 3]
}Response JSON:
{
"id": 1,
"result": 8,
"error": ""
}Codecinterface allows pluggable serialization- Easy to add new codecs without changing core logic
- Methods registered dynamically at runtime
- Flexible service composition
- Each client connection handled in separate goroutine
- Scales to multiple simultaneous clients
- Errors captured in Response message
- Client receives error details from server
- No exception-based error propagation
The project uses only Go standard library modules:
encoding/json- JSON codecencoding/binary- Frame header serializationnet- TCP networkingfmt,log,io- Utilities
No external dependencies required, keeping the project lightweight and dependency-free.
- Sequential Request-Response: Current implementation sends request and waits for response. For high throughput, implement request pipelining.
- Connection Pooling: Extend
TCPClientto support connection reuse and pooling. - Message Batching: Combine multiple RPC calls in one request for reduced latency.
- Codec Performance: JSON is convenient but slower than binary formats like Protobuf or MessagePack.
- Asynchronous request handling with callbacks
- Request timeout and retry logic
- Service discovery and load balancing
- HTTP/2 or gRPC integration
- TLS support for secure communication
- Streaming RPC support
- Metrics and monitoring
MIT
This is an educational project. Feel free to fork and experiment with enhancements!