EasyTCP

$ ./start
[EASYTCP] Message-Route Table:
+------------+-----------------------+
| Message ID | Route Handler |
+------------+-----------------------+
| 1000 | path/to/handler.Func1 |
+------------+-----------------------+
| 1002 | path/to/handler.Func2 |
+------------+-----------------------+
[EASYTCP] Serving at: tcp://[::]:10001
Introduction
EasyTCP
is a light-weight and less painful TCP server framework written in Go (Golang) based on the standard net
package.
✨ Features:
- Non-invasive design
- Pipelined middlewares for route handler
- Customizable message packer and codec, and logger
- Handy functions to handle request data and send response
- Common hooks
EasyTCP
helps you build a TCP server easily and fast.
This package has been tested in go1.16 ~ go1.18 on the latest Linux, Macos and Windows.
Install
Use the below Go command to install EasyTCP.
$ go get -u github.com/DarthPestilane/easytcp
Note: EasyTCP uses Go Modules to manage dependencies.
Quick start
package main
import (
"fmt"
"github.com/DarthPestilane/easytcp"
)
func main() {
s := easytcp.NewServer(&easytcp.ServerOption{
Packer: easytcp.NewDefaultPacker(),
Codec: nil,
})
s.AddRoute(1001, func(c easytcp.Context) {
req := c.Request()
fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", req.ID(), len(req.Data()), req.Data())
c.SetResponseMessage(easytcp.NewMessage(1002, []byte("copy that")))
})
easytcp.SetLogger(lg)
s.Use(recoverMiddleware)
s.OnSessionCreate = func(session easytcp.Session) {...}
s.OnSessionClose = func(session easytcp.Session) {...}
s.NotFoundHandler(handler)
if err := s.Run(":5896"); err != nil && err != server.ErrServerStopped {
fmt.Println("serve error: ", err.Error())
}
}
If we setup with the codec
s := easytcp.NewServer(&easytcp.ServerOption{
Packer: easytcp.NewDefaultPacker(),
Codec: &easytcp.JsonCodec{},
})
s.AddRoute(1001, func(c easytcp.Context) {
var reqData map[string]interface{}
if err := c.Bind(&reqData); err != nil {
}
respId := 1002
respData := map[string]interface{}{
"success": true,
"feeling": "Great!",
}
if err := c.SetResponse(respId, respData); err != nil {
}
})
Above is the server side example. There are client and more detailed examples including:
in examples/tcp.
Benchmark
go test -bench=. -run=none -benchmem -benchtime=250000x
goos: darwin
goarch: amd64
pkg: github.com/DarthPestilane/easytcp
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
Benchmark_NoHandler-8 250000 4277 ns/op 83 B/op 2 allocs/op
Benchmark_OneHandler-8 250000 4033 ns/op 81 B/op 2 allocs/op
Benchmark_DefaultPacker_Pack-8 250000 38.00 ns/op 16 B/op 1 allocs/op
Benchmark_DefaultPacker_Unpack-8 250000 105.8 ns/op 96 B/op 3 allocs/op
since easytcp is built on the top of golang net
library, the benchmark of networks does not make much sense.
Architecture
accepting connection:
+------------+ +-------------------+ +----------------+
| | | | | |
| | | | | |
| tcp server |--->| accept connection |--->| create session |
| | | | | |
| | | | | |
+------------+ +-------------------+ +----------------+
in session:
+------------------+ +-----------------------+ +----------------------------------+
| read connection |--->| unpack packet payload |--->| |
+------------------+ +-----------------------+ | |
| router (middlewares and handler) |
+------------------+ +-----------------------+ | |
| write connection |<---| pack packet payload |<---| |
+------------------+ +-----------------------+ +----------------------------------+
in route handler:
+----------------------------+ +------------+
| codec decode request data |--->| |
+----------------------------+ | |
| user logic |
+----------------------------+ | |
| codec encode response data |<---| |
+----------------------------+ +------------+
Conception
Routing
EasyTCP considers every message has a ID
segment to distinguish one another.
A message will be routed, according to it's id, to the handler through middlewares.
request flow:
+----------+ +--------------+ +--------------+ +---------+
| request |--->| |--->| |--->| |
+----------+ | | | | | |
| middleware 1 | | middleware 2 | | handler |
+----------+ | | | | | |
| response |<---| |<---| |<---| |
+----------+ +--------------+ +--------------+ +---------+
Register a route
s.AddRoute(reqID, func(c easytcp.Context) {
req := c.Request()
fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", req.ID(), len(req.Data()), req.Data())
c.SetResponseMessage(easytcp.NewMessage(respID, []byte("copy that")))
})
Using middleware
s.Use(recoverMiddleware, logMiddleware, ...)
s.AddRoute(reqID, handler, middleware1, middleware2)
var exampleMiddleware easytcp.MiddlewareFunc = func(next easytcp.HandlerFunc) easytcp.HandlerFunc {
return func(c easytcp.Context) {
next(c)
}
}
Packer
A packer is to pack and unpack packets' payload. We can set the Packer when creating the server.
s := easytcp.NewServer(&easytcp.ServerOption{
Packer: new(MyPacker),
})
We can set our own Packer or EasyTCP uses DefaultPacker
.
The DefaultPacker
considers packet's payload as a Size(4)|ID(4)|Data(n)
format. Size
only represents the length of Data
instead of the whole payload length
This may not covery some particular cases, but fortunately, we can create our own Packer.
type CustomPacker struct{}
func (p *CustomPacker) bytesOrder() binary.ByteOrder {
return binary.BigEndian
}
func (p *CustomPacker) Pack(msg *easytcp.Message) ([]byte, error) {
size := len(msg.Data())
buffer := make([]byte, 2+2+size)
p.bytesOrder().PutUint16(buffer[:2], uint16(size))
p.bytesOrder().PutUint16(buffer[2:4], msg.ID().(uint16))
copy(buffer[4:], msg.Data())
return buffer, nil
}
func (p *CustomPacker) Unpack(reader io.Reader) (*easytcp.Message, error) {
headerBuffer := make([]byte, 2+2)
if _, err := io.ReadFull(reader, headerBuffer); err != nil {
return nil, fmt.Errorf("read size and id err: %s", err)
}
size := p.bytesOrder().Uint16(headerBuffer[:2])
id := p.bytesOrder().Uint16(headerBuffer[2:])
data := make([]byte, size)
if _, err := io.ReadFull(reader, data); err != nil {
return nil, fmt.Errorf("read data err: %s", err)
}
msg := easytcp.NewMessage(id, data)
msg.Set("theWholeLength", 2+2+size)
return msg, nil
}
And see more custom packers:
Codec
A Codec is to encode and decode message data. The Codec is optional, EasyTCP won't encode or decode message data if the Codec is not set.
We can set Codec when creating the server.
s := easytcp.NewServer(&easytcp.ServerOption{
Codec: &easytcp.JsonCodec{},
})
Since we set the codec, we may want to decode the request data in route handler.
s.AddRoute(reqID, func(c easytcp.Context) {
var reqData map[string]interface{}
if err := c.Bind(&reqData); err != nil {
}
req := c.Request()
fmt.Printf("[server] request received | id: %d; size: %d; data-decoded: %+v\n", req.ID(), len(req.Data()), reqData())
respData := map[string]string{"key": "value"}
if err := c.SetResponse(respID, respData); err != nil {
}
})
Codec's encoding will be invoked before message packed,
and decoding should be invoked in the route handler which is after message unpacked.
JSON Codec
JsonCodec
is an EasyTCP's built-in codec, which uses encoding/json
as the default implementation.
Can be changed by build from other tags.
jsoniter :
go build -tags=jsoniter .
Protobuf Codec
ProtobufCodec
is an EasyTCP's built-in codec, which uses google.golang.org/protobuf
as the implementation.
Msgpack Codec
MsgpackCodec
is an EasyTCP's built-in codec, which uses github.com/vmihailenco/msgpack
as the implementation.
Contribute
Check out a new branch for the job, and make sure github action passed.
Use issues for everything
- For a small change, just send a PR.
- For bigger changes open an issue for discussion before sending a PR.
- PR should have:
- Test case
- Documentation
- Example (If it makes sense)
- You can also contribute by:
- Reporting issues
- Suggesting new features or enhancements
- Improve/fix documentation
Stargazers over time
