easytcp

package module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2021 License: MIT Imports: 18 Imported by: 15

README

EasyTCP

Run Actions Go Report codecov

$ ./start

[EASYTCP 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, so far, has been tested with

  • go1.14.x
  • go1.15.x
  • go1.16.x
  • go1.17.x

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"
    "github.com/DarthPestilane/easytcp/message"
)

func main() {
    // Create a new server with default options.
    s := easytcp.NewServer(&easytcp.ServerOption{})

    // Register a route with message's ID.
    // The `DefaultPacker` treats id as uint32,
    // so when we add routes or return response, we should use uint32 or *uint32.
    s.AddRoute(uint32(1001), func(c *easytcp.Context) (*message.Entry, error) {
        fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", c.Message().ID, len(c.Message().Data), c.Message().Data)
        return c.Response(uint32(1002), []byte("copy that"))
    })

    // Set custom logger (optional).
    easytcp.SetLogger(lg)

    // Add global middlewares (optional).
    s.Use(recoverMiddleware)

    // Set hooks (optional).
    s.OnSessionCreate = func(sess *easytcp.Session) {}
    s.OnSessionClose = func(sess *easytcp.Session) {}

    // Set not-found route handler (optional).
    s.NotFoundHandler(handler)

    // Listen and serve.
    if err := s.Serve(":5896"); err != nil && err != server.ErrServerStopped {
        fmt.Println("serve error: ", err.Error())
    }
}

Above is the server side example. There are client and more detailed examples including:

in examples/tcp.

Benchmark

  • goos: darwin
  • goarch: amd64
Benchmark name (1) (2) (3) (4) remark
Benchmark_NoRoute-8 197164 8799 ns/op 159 B/op 5 allocs/op
Benchmark_NotFoundHandler-8 300993 7285 ns/op 811 B/op 13 allocs/op
Benchmark_OneHandler-8 201592 8907 ns/op 670 B/op 20 allocs/op
Benchmark_ManyHandlers-8 276344 8057 ns/op 796 B/op 15 allocs/op
Benchmark_OneRouteSet-8 248247 8245 ns/op 1002 B/op 19 allocs/op
Benchmark_OneRouteJsonCodec-8 176893 6413 ns/op 1618 B/op 32 allocs/op build with encoding/json
Benchmark_OneRouteJsonCodec-8 189723 5985 ns/op 1347 B/op 27 allocs/op build with json-jsoniter/go
Benchmark_OneRouteProtobufCodec-8 210532 6346 ns/op 708 B/op 19 allocs/op
Benchmark_OneRouteMsgpackCodec-8 181495 6196 ns/op 868 B/op 22 allocs/op

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) (*message.Entry, error) {
    // handle the request via ctx
    fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", c.Message().ID, len(c.Message().Data), c.Message().Data)

    // do things...

    // return response
    return c.Response(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) (*message.Entry, error) {
        // do things before...
        resp, err := next(c)
        // do things after...
        return resp, err
    }
}
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.

This may not covery some particular cases, but fortunately, we can create our own Packer.

// Packer16bit is a custom packer, implements Packer interafce.
// THe Packet format is `size[2]id[2]data`
type Packer16bit struct{}

func (p *Packer16bit) bytesOrder() binary.ByteOrder {
    return binary.BigEndian
}

func (p *Packer16bit) Pack(entry *message.Entry) ([]byte, error) {
    size := len(entry.Data) // without id
    buff := bytes.NewBuffer(make([]byte, 0, size+2+2))
    if err := binary.Write(buff, p.bytesOrder(), uint16(size)); err != nil {
        return nil, fmt.Errorf("write size err: %s", err)
    }
    if err := binary.Write(buff, p.bytesOrder(), entry.ID.(uint16)); err != nil {
        return nil, fmt.Errorf("write id err: %s", err)
    }
    if err := binary.Write(buff, p.bytesOrder(), entry.Data); err != nil {
        return nil, fmt.Errorf("write data err: %s", err)
    }
    return buff.Bytes(), nil
}

func (p *Packer16bit) Unpack(reader io.Reader) (*message.Entry, error) {
    sizeBuff := make([]byte, 2)
    if _, err := io.ReadFull(reader, sizeBuff); err != nil {
        return nil, fmt.Errorf("read size err: %s", err)
    }
    size := p.bytesOrder().Uint16(sizeBuff)

    idBuff := make([]byte, 2)
    if _, err := io.ReadFull(reader, idBuff); err != nil {
        return nil, fmt.Errorf("read id err: %s", err)
    }
    id := p.bytesOrder().Uint16(idBuff)
    // since id here is the type of uint16, we need to use a uint16 when adding routes.
    // eg: server.AddRoute(uint16(123), ...)

    data := make([]byte, size)
    if _, err := io.ReadFull(reader, data); err != nil {
        return nil, fmt.Errorf("read data err: %s", err)
    }

    entry := &message.Entry{ID: id, Data: data}
    entry.Set("theWholeLength", 2+2+size) // we can set our custom kv data here.
    // c.Message().Get("theWholeLength")  // and get them in route handler.
    return entry, nil
}

And see the custom packer here.

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{}, // this is optional. The JsonCodec is a built-in codec
})

Since we set the codec, we may want to decode the request data in route handler.

s.AddRoute(reqID, func(c *easytcp.Context) (*message.Entry, error) {
    var reqData map[string]interface{}
    if err := c.Bind(&reqData); err != nil { // here we decode message data and bind to reqData
        // handle error...
    }
    fmt.Printf("[server] request received | id: %d; size: %d; data-decoded: %+v\n", c.Message().ID, len(c.Message().Data), reqData)
    respData := map[string]string{"key": "value"}
    return c.Response(respID, respData)
})

Codec's encoding will be invoked before message packed, and decoding should be invoked in the route handler which is after message unpacked.

NOTE:

If the Codec is not set (or is nil), EasyTCP will try to convert the respData (the second parameter of c.Response) into a []byte. So the type of respData should be one of string, []byte or fmt.Stringer.

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

Documentation

Index

Constants

View Source
const RespKey = "easytcp.router.context.response"

RespKey is the preset key to the response data before encoding.

Variables

View Source
var ErrServerStopped = fmt.Errorf("server stopped")

ErrServerStopped is used when server stopped.

Functions

func SetLogger

func SetLogger(lg Logger)

SetLogger sets the package logger.

Types

type Codec

type Codec interface {
	// Encode encodes data into []byte.
	// Returns error when error occurred.
	Encode(v interface{}) ([]byte, error)

	// Decode decodes data into v.
	// Returns error when error occurred.
	Decode(data []byte, v interface{}) error
}

Codec is a generic codec for encoding and decoding data.

type Context

type Context struct {
	// contains filtered or unexported fields
}

Context is a generic context in a message routing. It allows us to pass variables between handler and middlewares. Context implements the context.Context interface.

func (*Context) Bind

func (c *Context) Bind(v interface{}) error

Bind binds the request message's raw data to v.

func (*Context) Deadline

func (c *Context) Deadline() (deadline time.Time, ok bool)

Deadline implements the context.Context Deadline method.

func (*Context) Done

func (c *Context) Done() <-chan struct{}

Done implements the context.Context Done method.

func (*Context) Err

func (c *Context) Err() error

Err implements the context.Context Err method.

func (*Context) Get

func (c *Context) Get(key string) (interface{}, bool)

Get returns the value from c.storage by key.

func (*Context) Message

func (c *Context) Message() *message.Entry

Message returns the request message entry.

func (*Context) Response

func (c *Context) Response(id interface{}, data interface{}) (*message.Entry, error)

Response creates a response message.

func (*Context) Session

func (c *Context) Session() *Session

Session returns current session.

func (*Context) Set

func (c *Context) Set(key string, value interface{})

Set sets the value in c.storage.

func (*Context) Value

func (c *Context) Value(key interface{}) interface{}

Value implements the context.Context Value method.

type DefaultLogger

type DefaultLogger struct {
	// contains filtered or unexported fields
}

DefaultLogger is the default logger instance for this package. DefaultLogger uses the built-in log.Logger.

func (*DefaultLogger) Errorf

func (d *DefaultLogger) Errorf(format string, args ...interface{})

Errorf implements Logger Errorf method.

func (*DefaultLogger) Tracef

func (d *DefaultLogger) Tracef(format string, args ...interface{})

Tracef implements Logger Tracef method.

type DefaultPacker

type DefaultPacker struct {
	MaxSize int
}

DefaultPacker is the default Packer used in session. DefaultPacker treats the packet with the format:

(size)(id)(data):
	size: uint32 | took 4 bytes, only the size of `data`
	id:   uint32 | took 4 bytes
	data: []byte | took `size` bytes

func NewDefaultPacker

func NewDefaultPacker() *DefaultPacker

NewDefaultPacker create a *DefaultPacker with initial field value.

func (*DefaultPacker) Pack

func (d *DefaultPacker) Pack(entry *message.Entry) ([]byte, error)

Pack implements the Packer Pack method.

func (*DefaultPacker) Unpack

func (d *DefaultPacker) Unpack(reader io.Reader) (*message.Entry, error)

Unpack implements the Packer Unpack method.

type Error

type Error interface {
	error
	Fatal() bool // should return true if the error is fatal, otherwise false.
}

Error is a generic interface for error handling.

type HandlerFunc

type HandlerFunc func(ctx *Context) (*message.Entry, error)

HandlerFunc is the function type for handlers.

type JsonCodec

type JsonCodec struct{}

JsonCodec implements the Codec interface. JsonCodec encodes and decodes data in json way.

func (*JsonCodec) Decode

func (c *JsonCodec) Decode(data []byte, v interface{}) error

Decode implements the Codec Decode method.

func (*JsonCodec) Encode

func (c *JsonCodec) Encode(v interface{}) ([]byte, error)

Encode implements the Codec Encode method.

type Logger

type Logger interface {
	Errorf(format string, args ...interface{})
	Tracef(format string, args ...interface{})
}

Logger is the generic interface for log recording.

var Log Logger = newLogger()

Log is the instance of Logger interface.

type MiddlewareFunc

type MiddlewareFunc func(next HandlerFunc) HandlerFunc

MiddlewareFunc is the function type for middlewares. A common pattern is like:

var md MiddlewareFunc = func(next HandlerFunc) HandlerFunc {
	return func(ctx *Context) (packet.Message, error) {
		return next(ctx)
	}
}

type MsgpackCodec added in v0.0.2

type MsgpackCodec struct{}

MsgpackCodec implements the Codec interface.

func (*MsgpackCodec) Decode added in v0.0.2

func (m *MsgpackCodec) Decode(data []byte, v interface{}) error

Decode implements the Codec Decode method.

func (*MsgpackCodec) Encode added in v0.0.2

func (m *MsgpackCodec) Encode(v interface{}) ([]byte, error)

Encode implements the Codec Encode method.

type Packer

type Packer interface {
	// Pack packs Message into the packet to be written.
	// Pack(msg Message) ([]byte, error)
	Pack(entry *message.Entry) ([]byte, error)

	// Unpack unpacks the message packet from reader,
	// returns the Message interface, and error if error occurred.
	Unpack(reader io.Reader) (*message.Entry, error)
}

Packer is a generic interface to pack and unpack message packet.

type ProtobufCodec

type ProtobufCodec struct{}

ProtobufCodec implements the Codec interface.

func (*ProtobufCodec) Decode

func (p *ProtobufCodec) Decode(data []byte, v interface{}) error

Decode implements the Codec Decode method.

func (*ProtobufCodec) Encode

func (p *ProtobufCodec) Encode(v interface{}) ([]byte, error)

Encode implements the Codec Encode method.

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router is a router for incoming message. Router routes the message to its handler and middlewares.

type Server

type Server struct {
	Listener net.Listener

	// Packer is the message packer, will be passed to session.
	Packer Packer

	// Codec is the message codec, will be passed to session.
	Codec Codec

	// OnSessionCreate is an event hook, will be invoked when session's created.
	OnSessionCreate func(sess *Session)

	// OnSessionClose is an event hook, will be invoked when session's closed.
	OnSessionClose func(sess *Session)
	// contains filtered or unexported fields
}

Server is a server for TCP connections.

func NewServer

func NewServer(opt *ServerOption) *Server

NewServer creates a Server pointer according to opt.

func (*Server) AddRoute

func (s *Server) AddRoute(msgID interface{}, handler HandlerFunc, middlewares ...MiddlewareFunc)

AddRoute registers message handler and middlewares to the router.

func (*Server) NotFoundHandler

func (s *Server) NotFoundHandler(handler HandlerFunc)

NotFoundHandler sets the not-found handler for router.

func (*Server) Serve

func (s *Server) Serve(addr string) error

Serve starts to listen TCP and keep accepting TCP connection in a loop. Accepting loop will break when error occurred, and the error will be returned.

func (*Server) Stop

func (s *Server) Stop() error

Stop stops server by closing all the TCP sessions and the listener.

func (*Server) Use

func (s *Server) Use(middlewares ...MiddlewareFunc)

Use registers global middlewares to the router.

type ServerOption

type ServerOption struct {
	SocketReadBufferSize  int           // sets the socket read buffer size.
	SocketWriteBufferSize int           // sets the socket write buffer size.
	SocketSendDelay       bool          // sets the socket delay or not.
	ReadTimeout           time.Duration // sets the timeout for connection read.
	WriteTimeout          time.Duration // sets the timeout for connection write.
	Packer                Packer        // packs and unpacks packet payload, default packer is the packet.DefaultPacker.
	Codec                 Codec         // encodes and decodes the message data, can be nil
	WriteBufferSize       int           // sets the writing channel buffer size, 1024 will be used if < 0.
	ReadBufferSize        int           // sets the reading channel buffer size, 1024 will be used if < 0.
	DoNotPrintRoutes      bool          // whether to print registered route handlers to the console.
}

ServerOption is the option for Server.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session represents a TCP session.

func (*Session) Close

func (s *Session) Close()

Close closes the session by closing all the channels.

func (*Session) ID

func (s *Session) ID() string

ID returns the session's ID.

func (*Session) SendResp

func (s *Session) SendResp(respMsg *message.Entry) error

SendResp pushes response message entry to respQueue. If respQueue is closed, returns error.

type SessionManager

type SessionManager struct {
	// Sessions keeps all sessions.
	// Key is session's ID, value is *Session
	Sessions sync.Map
}

SessionManager manages all the sessions in application runtime.

func Sessions

func Sessions() *SessionManager

Sessions returns a SessionManager pointer in a singleton way.

func (*SessionManager) Add

func (m *SessionManager) Add(s *Session)

Add adds a session to Sessions. If the ID of s already existed in Sessions, it replaces the value with the s.

func (*SessionManager) Get

func (m *SessionManager) Get(id string) *Session

Get returns a Session when found by the id, returns nil otherwise.

func (*SessionManager) Range

func (m *SessionManager) Range(fn func(id string, sess *Session) (next bool))

Range calls fn sequentially for each id and sess present in the Sessions. If fn returns false, range stops the iteration.

func (*SessionManager) Remove

func (m *SessionManager) Remove(id string)

Remove removes a session from Sessions. Parameter id should be the session's id.

type SessionOption

type SessionOption struct {
	Packer          Packer
	Codec           Codec
	ReadBufferSize  int
	WriteBufferSize int
}

SessionOption is the extra options for Session.

type UnpackError

type UnpackError struct {
	Err error
}

UnpackError is the error returned in packer.Unpack.

func (*UnpackError) Error

func (pe *UnpackError) Error() string

Error implements error interface.

func (*UnpackError) Fatal

func (pe *UnpackError) Fatal() bool

Fatal implements Error interface.

Directories

Path Synopsis
examples
Package mock is a generated GoMock package.
Package mock is a generated GoMock package.
test_data
pb

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL