EasyTCP

repository·master·Indexed 21 days ago

https://github.com/darthpestilane/easytcp

A lightweight, non-invasive TCP server framework for Go built on the standard net package. EasyTCP provides structured routing based on message IDs, middleware support, and customizable data packing and coding. It includes built-in codecs for JSON, Protobuf, and Msgpack, as well as a default packer for message framing. The framework supports global and per-route middlewares, lifecycle hooks for session management, and a thread-safe metadata storage system within its Message type.

Tokens
10.3K
Snippets
41
Records
43
Agent score
73%

What's inside easytcp

  1. How EasyTCP architecture works

    master

    EasyTCP follows a structured pipeline for handling TCP connections:

    1. Connection Acceptance: The TCP server accepts a connection and creates a Session.
    2. Session Lifecycle: Inside a session, the server reads the connection, uses a Packer to unpack the payload, and then passes the request through the Router (which executes middlewares and the specific route handler).
    3. Route Handling: If a Codec is configured, it decodes the request data before the user logic runs. After the user logic, the Codec encodes the response data before the Packer packs it for transmission back through the connection.
  2. How routing and middleware work in EasyTCP

    master

    EasyTCP uses a message ID to route incoming messages to specific handlers. The request flow follows a pipeline: Request -> Middlewares -> Handler -> Middlewares -> Response.

    There are two levels of middleware:

    1. Global Middlewares: Registered via s.Use(...). These are invoked first for every request.
    2. Per-Route Middlewares: Registered via s.AddRoute(...). These are specific to a particular message ID.

    A middleware is defined as an easytcp.MiddlewareFunc that wraps an easytcp.HandlerFunc.

    // Register global middlewares
    s.Use(recoverMiddleware, logMiddleware)
    
    // Register a route with specific middlewares
    s.AddRoute(reqID, handler, middleware1, middleware2)
    
    // Middleware definition pattern
    var exampleMiddleware easytcp.MiddlewareFunc = func(next easytcp.HandlerFunc) easytcp.HandlerFunc {
        return func(c easytcp.Context) {
            // logic before handler
            next(c)
            // logic after handler
        }
    }
  3. Quick start with EasyTCP

    master

    To create a basic TCP server, use easytcp.NewServer with a ServerOption. You can register routes using AddRoute with a specific message ID. In the handler, use c.Request() to access the incoming data and c.SetResponseMessage() to send a response. You can also add global middlewares using s.Use() and set lifecycle hooks like OnSessionCreate and OnSessionClose.

    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())
        }
    }
  4. Use the Message type to manage data and metadata

    master

    The Message struct represents both inbound and outbound messages. It consists of a fixed id and data payload, along with a thread-safe storage map for attaching arbitrary metadata (key-value pairs) during the message lifecycle.

    msg := easytcp.NewMessage(1, []byte("payload"))
    fmt.Println(msg.ID())   // 1
    fmt.Println(msg.Data()) // [112 97 121 108 111 97 100]
  5. How middleware and handlers are executed

    master

    The Router executes a request through a layered stack. When a message arrives, the execution order is:

    1. Global Middlewares: All middlewares registered via registerMiddleware (in the order they were registered).
    2. Route-specific Middlewares: Middlewares registered specifically for the message's ID via register.
    3. Handler: The HandlerFunc mapped to the message ID.

    If no handler is found for an ID, the notFoundHandler (if set) is used. If no notFoundHandler is set, a nilHandler (no-op) is used.

    Execution Stack Visualization: GlobalMW1 -> GlobalMW2 -> RouteMW1 -> RouteMW2 -> Handler

  6. Use a Codec for automatic data binding

    master

    If you provide a Codec (e.g., &easytcp.JsonCodec{}) in the ServerOption, you can use c.Bind() to automatically decode request data into a variable and c.SetResponse() to encode and send response data. This simplifies working with structured data like JSON or Protobuf.

    // 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.
    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
        }
    })
  7. Register a route with a handler

    master

    Use s.AddRoute(reqID, handler) to associate a specific message ID with a handler function. Inside the handler, you can access the request via c.Request() and send a response using c.SetResponseMessage(...) or c.SetResponse(...) (if a Codec is used).

    s.AddRoute(reqID, func(c easytcp.Context) {
        // acquire request
        req := c.Request()
    
        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")))
    })
  8. Implement and configure a custom Packer

    master

    A Packer is responsible for packing and unpacking the packet's payload. You can provide a custom implementation to the easytcp.ServerOption when creating a server.

    By default, EasyTCP uses DefaultPacker, which uses the format Size(4)|ID(4)|Data(n), where Size represents the length of Data only.

    To implement a custom Packer, you must satisfy the Packer interface, including bytesOrder(), Pack(*easytcp.Message), and Unpack(io.Reader) *easytcp.Message.

    // Configure the server with a custom packer
    s := easytcp.NewServer(&easytcp.ServerOption{
        Packer: new(MyPacker),
    })
    
    // Example implementation of a custom 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)
        return msg, nil
    }
  9. Use a Codec to encode and decode message data

    master

    A Codec handles the encoding and decoding of message data. If no Codec is set, EasyTCP will not attempt to encode or decode the payload.

    When a Codec is used:

    1. Decoding: In the route handler, use c.Bind(&target) to decode the request data into a variable.
    2. Encoding: Use c.SetResponse(respID, data) to automatically encode the response data before it is passed to the Packer.

    Built-in Codecs include:

    • JsonCodec (uses encoding/json)
    • ProtobufCodec (uses google.golang.org/protobuf)
    • MsgpackCodec (uses github.com/vmihailenco/msgpack)
    // Configure the server with a codec
    s := easytcp.NewServer(&easytcp.ServerOption{
        Codec: &easytcp.JsonCodec{},
    })
    
    // Use the codec in a route handler
    s.AddRoute(reqID, func(c easytcp.Context) {
        var reqData map[string]interface{}
        if err := c.Bind(&reqData); err != nil {
            // handle error
        }
    
        respData := map[string]string{"key": "value"}
        if err := c.SetResponse(respID, respData); err != nil {
            // handle error
        }
    })
  10. Send messages using Context

    master

    Once you have prepared a response within the Context, you must trigger the transmission:

    • Send() bool: Sends the current context (and its response message) to the session associated with the context.
    • SendTo(session Session) bool: Sends the current context to a specific Session instance.
    ctx.SetResponse("ok", "done")
    ctx.Send()