easytcp

package module
v0.0.9 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2021 License: MIT Imports: 17 Imported by: 15

README

EasyTCP

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

$ ./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.15 ~ go1.17 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 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) 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(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

  • goversion: 1.15.15
  • goos: darwin
  • goarch: amd64
Benchmark name (1) (2) (3) (4) remark
NoRoute-8 250000 4584 ns/op 79 B/op 2 allocs/op
NotFoundHandler-8 250000 5471 ns/op 565 B/op 6 allocs/op
OneHandler-8 250000 6250 ns/op 352 B/op 7 allocs/op
ManyHandlers-8 250000 6405 ns/op 464 B/op 11 allocs/op
OneRouteCtxGetSet-8 250000 6940 ns/op 697 B/op 11 allocs/op
OneRouteMessageGetSet-8 250000 5406 ns/op 623 B/op 9 allocs/op
OneRouteJsonCodec-8 250000 7582 ns/op 1383 B/op 21 allocs/op built with encoding/json
OneRouteJsonCodec-8 250000 8430 ns/op 1410 B/op 17 allocs/op built with json-jsoniter/go
OneRouteProtobufCodec-8 250000 6628 ns/op 444 B/op 8 allocs/op
OneRouteMsgpackCodec-8 250000 7491 ns/op 661 B/op 11 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) 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) error {
        // do things before...
        err := next(c)
        // do things after...
        return 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. (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(entry *message.Entry) ([]byte, error) {
    size := len(entry.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], entry.ID.(uint16))
    copy(buffer[4:], entry.Data)
    return buffer, nil
}

func (p *CustomPacker) Unpack(reader io.Reader) (*message.Entry, 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)
    }

    entry := &message.Entry{
        // since entry.ID is type of uint16, we need to use uint16 as well when adding routes.
        // eg: server.AddRoute(uint16(123), ...)
        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 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{}, // 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) error {
    var reqData map[string]interface{}
    if err := c.Bind(&reqData); err != nil { // here we decode message data and bind to reqData
        // handle error...
    }
    // or c.MustBind(&reqData)
    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

Stargazers over time

Stargazers over time

Documentation

Index

Constants

View Source
const (
	DefaultReqQueueSize      = 1024
	DefaultRespQueueSize     = 1024
	DefaultWriteAttemptTimes = 1
)

Variables

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

ErrServerStopped is returned 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) Copy added in v0.0.9

func (c *Context) Copy() *Context

Copy returns a copy of the current context. This should be used when one wants to change the context after pushed to a channel.

func (*Context) Deadline

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

Deadline implements the context.Context Deadline method.

func (*Context) DecodeTo added in v0.0.6

func (c *Context) DecodeTo(data []byte, v interface{}) error

DecodeTo decodes data to v via codec.

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) (value interface{}, exists bool)

Get returns the value from c.storage by key.

func (*Context) GetResponse added in v0.0.6

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

GetResponse returns response entry of context.

func (*Context) Message

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

Message returns the request message entry.

func (*Context) MustBind added in v0.0.6

func (c *Context) MustBind(v interface{})

MustBind binds the request message's raw data to v. Panics if any error occurred.

func (*Context) MustDecodeTo added in v0.0.6

func (c *Context) MustDecodeTo(data []byte, v interface{})

MustDecodeTo decodes data to v via codec. Panics if any error occurred.

func (*Context) MustGet added in v0.0.6

func (c *Context) MustGet(key string) interface{}

MustGet returns the value from c.storage by key. Panics if key does not exist.

func (*Context) Remove added in v0.0.7

func (c *Context) Remove(key string)

Remove deletes the key from storage.

func (*Context) Response

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

Response creates and sets the response message to the context.

func (*Context) Send added in v0.0.6

func (c *Context) Send(id, data interface{}) error

Send sends response message to current session.

func (*Context) SendTo added in v0.0.6

func (c *Context) SendTo(sess *Session, id, data interface{}) error

SendTo sends response message to the specified session.

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) SetResponse added in v0.0.6

func (c *Context) SetResponse(id interface{}, data []byte)

SetResponse sets response entry with id and data.

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 {
	// MaxDataSize represents the max size of `data`
	MaxDataSize int
}

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

dataSize(4)|id(4)|data(n)

| segment | type | size | remark | | ---------- | ------ | ------- | ----------------------- | | `dataSize` | uint32 | 4 | the size of `data` only | | `id` | uint32 | 4 | | | `data` | []byte | dynamic | | .

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. Unpack returns the entry whose ID is type of int. So we need use int id to register routes.

type HandlerFunc

type HandlerFunc func(ctx *Context) 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 mf MiddlewareFunc = func(next HandlerFunc) HandlerFunc {
	return func(ctx *Context) 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(entry *message.Entry) ([]byte, error)

	// Unpack unpacks the message packet from reader,
	// returns the message.Entry, 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 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 keeps accepting TCP connection in a loop. The loop breaks 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, listener and the router.

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.
	ReqQueueSize          int           // sets the request channel size of router, DefaultReqQueueSize will be used if < 0.
	RespQueueSize         int           // sets the response channel size of session, DefaultRespQueueSize will be used if < 0.
	DoNotPrintRoutes      bool          // whether to print registered route handlers to the console.

	// WriteAttemptTimes sets the max attempt times for packet writing in each session.
	// The DefaultWriteAttemptTimes will be used if <= 0.
	WriteAttemptTimes int
}

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, but doesn't close the connection. The connection will be closed in the server once the session's closed.

func (*Session) ID

func (s *Session) ID() string

ID returns the session's ID.

func (*Session) SendResp

func (s *Session) SendResp(ctx *Context) (err error)

SendResp pushes response message entry to respQueue. Returns error if session is closed.

type SessionManager

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

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
	// contains filtered or unexported fields
}

SessionOption is the extra options for Session.

Jump to

Keyboard shortcuts

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