SkillAgentSearch skills...

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/Easytcp
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

EasyTCP

gh-action Go Report codecov Go Reference Mentioned in Awesome Go

$ ./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
View on GitHub
GitHub Stars823
CategoryDevelopment
Updated1mo ago
Forks86

Languages

Go

Security Score

100/100

Audited on Feb 22, 2026

No findings