Easytcp
:sparkles: :rocket: EasyTCP is a light-weight TCP framework written in Go (Golang), built with message router. EasyTCP helps you build a TCP server easily fast and less painful.
Install / Use
/learn @DarthPestilane/EasytcpREADME
EasyTCP
$ ./start
[EASYTCP] Message-Route Table:
+------------+-----------------------+---------------------------------
| Message ID | Route Handler | Middlewares |
+------------+-----------------------+---------------------------------
| 1000 | path/to/handler.Func1 | /path/to/middleware.Func1(g) |
| | | /path/to/middleware.Func2 |
+------------+-----------------------+---------------------------------
| 1002 | path/to/handler.Func2 | /path/to/middleware.Func1(g) |
| | | /path/to/middleware.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 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() {
// Create a new server with options.
s := easytcp.NewServer(&easytcp.ServerOption{
Packer: easytcp.NewDefaultPacker(), // use default packer
Codec: nil, // don't use codec
})
// Register a route with message's ID.
// The `DefaultPacker` treats id as int,
// so when we add routes or return response, we should use int.
s.AddRoute(1001, func(c easytcp.Context) {
// acquire request
req := c.Request()
// do things...
fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", req.ID(), len(req.Data()), req.Data())
// set response
c.SetResponseMessage(easytcp.NewMessage(1002, []byte("copy that")))
})
// Set custom logger (optional).
easytcp.SetLogger(lg)
// Add global middlewares (optional).
s.Use(recoverMiddleware)
// Set hooks (optional).
s.OnSessionCreate = func(session easytcp.Session) {...}
s.OnSessionClose = func(session easytcp.Session) {...}
// Set not-found route handler (optional).
s.NotFoundHandler(handler)
// Listen and serve.
if err := s.Run(":5896"); err != nil && err != server.ErrServerStopped {
fmt.Println("serve error: ", err.Error())
}
}
If we setup with the codec
// Create a new server with options.
s := easytcp.NewServer(&easytcp.ServerOption{
Packer: easytcp.NewDefaultPacker(), // use default packer
Codec: &easytcp.JsonCodec{}, // use JsonCodec
})
// Register a route with message's ID.
// The `DefaultPacker` treats id as int,
// so when we add routes or return response, we should use int.
s.AddRoute(1001, func(c easytcp.Context) {
// decode request data and bind to `reqData`
var reqData map[string]interface{}
if err := c.Bind(&reqData); err != nil {
// handle err
}
// do things...
respId := 1002
respData := map[string]interface{}{
"success": true,
"feeling": "Great!",
}
// encode response data and set to `c`
if err := c.SetResponse(respId, respData); err != nil {
// handle err
}
})
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 its 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) {
// acquire request
req := c.Request()
// do things...
fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", req.ID(), len(req.Data()), req.Data())
// set response
c.SetResponseMessage(easytcp.NewMessage(respID, []byte("copy that")))
})
Using middleware
// register global middlewares.
// global middlewares are prior than per-route middlewares, they will be invoked first
s.Use(recoverMiddleware, logMiddleware, ...)
// register middlewares for one route
s.AddRoute(reqID, handler, middleware1, middleware2)
// a middleware looks like:
var exampleMiddleware easytcp.MiddlewareFunc = func(next easytcp.HandlerFunc) easytcp.HandlerFunc {
return func(c easytcp.Context) {
// do things before...
next(c)
// do things after...
}
}
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), // this is optional, the default one is DefaultPacker
})
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.
// CustomPacker is a custom packer, implements Packer interafce.
// Treats Packet format as `size(2)id(2)data(n)`
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()) // only the size of 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.byte
