easytcp

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Feb 3, 2024 License: MIT Imports: 21 Imported by: 0

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/rxjh-emu/easytcp

Note: EasyTCP uses Go Modules to manage dependencies.

Quick start

package main

import (
    "fmt"
    "github.com/rxjh-emu/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/rxjh-emu/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.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)
    }

    // since msg.ID is type of uint16, we need to use uint16 as well when adding routes.
    // eg: server.AddRoute(uint16(123), ...)
    msg := easytcp.NewMessage(id, data)
    msg.Set("theWholeLength", 2+2+size) // we can set our custom kv data here.
    // c.Request().Get("theWholeLength")  // and get them in route handler.
    return msg, 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) {
    var reqData map[string]interface{}
    if err := c.Bind(&reqData); err != nil { // here we decode message data and bind to reqData
        // handle error...
    }
    req := c.Request()
    fmt.Printf("[server] request received | id: %d; size: %d; data-decoded: %+v\n", req.ID(), len(req.Data()), reqData())
    respData := map[string]string{"key": "value"}
    if err := c.SetResponse(respID, respData); err != nil {
        // handle error...
    }
})

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

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

Overview

Package easytcp is a generated GoMock package.

Index

Constants

View Source
const DefaultRespQueueSize = 1024

Variables

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

ErrServerStopped is returned when server stopped.

Functions

func NewSession

func NewSession(conn net.Conn, opt *SessionOption) *session

newSession creates a new session. Parameter conn is the TCP connection, opt includes packer, codec, and channel size. Returns a session pointer.

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 interface {
	context.Context

	// WithContext sets the underline context.
	// It's very useful to control the workflow when send to response channel.
	WithContext(ctx context.Context) Context

	// Session returns the current session.
	Session() Session

	// SetSession sets session.
	SetSession(sess Session) Context

	// Request returns request message.
	Request() *Message

	// SetRequest encodes data with session's codec and sets request message.
	SetRequest(id, data interface{}) error

	// MustSetRequest encodes data with session's codec and sets request message.
	// panics on error.
	MustSetRequest(id, data interface{}) Context

	// SetRequestMessage sets request message directly.
	SetRequestMessage(msg *Message) Context

	// Bind decodes request message to v.
	Bind(v interface{}) error

	// Response returns the response message.
	Response() *Message

	// SetResponse encodes data with session's codec and sets response message.
	SetResponse(id, data interface{}) error

	// MustSetResponse encodes data with session's codec and sets response message.
	// panics on error.
	MustSetResponse(id, data interface{}) Context

	// SetResponseMessage sets response message directly.
	SetResponseMessage(msg *Message) Context

	// Send sends itself to current session.
	Send() bool

	// SendTo sends itself to session.
	SendTo(session Session) bool

	// Get returns key value from storage.
	Get(key string) (value interface{}, exists bool)

	// Set store key value into storage.
	Set(key string, value interface{})

	// Remove deletes the key from storage.
	Remove(key string)

	// Copy returns a copy of Context.
	Copy() Context
}

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

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(msg *Message) ([]byte, error)

Pack implements the Packer Pack method.

func (*DefaultPacker) Unpack

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

Unpack implements the Packer Unpack method. Unpack returns the message whose ID is type of int. So we need use int id to register routes.

type HandlerFunc

type HandlerFunc func(ctx Context)

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.

func Log

func Log() Logger

Log returns the package logger.

type Message

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

Message is the abstract of inbound and outbound message.

func NewMessage

func NewMessage(id interface{}, data []byte) *Message

NewMessage creates a Message pointer.

func (*Message) Data

func (m *Message) Data() []byte

Data returns the data part of current message.

func (*Message) Get

func (m *Message) Get(key string) (value interface{}, exists bool)

Get retrieves the value according to the key.

func (*Message) ID

func (m *Message) ID() interface{}

ID returns the id of current message.

func (*Message) MustGet

func (m *Message) MustGet(key string) interface{}

MustGet retrieves the value according to the key. Panics if key does not exist.

func (*Message) Remove

func (m *Message) Remove(key string)

Remove deletes the key from storage.

func (*Message) Set

func (m *Message) Set(key string, value interface{})

Set stores kv pair.

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) {
		next(ctx)
	}
}

type MockPacker

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

MockPacker is a mock of Packer interface.

func NewMockPacker

func NewMockPacker(ctrl *gomock.Controller) *MockPacker

NewMockPacker creates a new mock instance.

func (*MockPacker) EXPECT

func (m *MockPacker) EXPECT() *MockPackerMockRecorder

EXPECT returns an object that allows the caller to indicate expected use.

func (*MockPacker) Pack

func (m *MockPacker) Pack(arg0 *Message) ([]byte, error)

Pack mocks base method.

func (*MockPacker) Unpack

func (m *MockPacker) Unpack(arg0 io.Reader) (*Message, error)

Unpack mocks base method.

type MockPackerMockRecorder

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

MockPackerMockRecorder is the mock recorder for MockPacker.

func (*MockPackerMockRecorder) Pack

func (mr *MockPackerMockRecorder) Pack(arg0 interface{}) *gomock.Call

Pack indicates an expected call of Pack.

func (*MockPackerMockRecorder) Unpack

func (mr *MockPackerMockRecorder) Unpack(arg0 interface{}) *gomock.Call

Unpack indicates an expected call of Unpack.

type MsgpackCodec

type MsgpackCodec struct{}

MsgpackCodec implements the Codec interface.

func (*MsgpackCodec) Decode

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

Decode implements the Codec Decode method.

func (*MsgpackCodec) Encode

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)

	// Unpack unpacks the message packet from reader,
	// returns the message, and error if error occurred.
	Unpack(reader io.Reader) (*Message, 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) Run

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

Run 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) RunTLS

func (s *Server) RunTLS(addr string, config *tls.Config) error

RunTLS starts serve TCP with TLS.

func (*Server) Serve

func (s *Server) Serve(lis net.Listener) error

Serve starts to serve the lis.

func (*Server) Stop

func (s *Server) Stop() error

Stop stops server. Closing Listener and all connections.

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 DefaultPacker.
	Codec                 Codec         // encodes and decodes the message data, can be nil.
	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.

	// AsyncRouter represents whether to execute a route HandlerFunc of each session in a goroutine.
	// true means execute in a goroutine.
	AsyncRouter bool
}

ServerOption is the option for Server.

type Session

type Session interface {
	// ID returns current session's id.
	ID() interface{}

	// SetID sets current session's id.
	SetID(id interface{})

	// Send sends the ctx to the respStream.
	Send(ctx Context) bool

	// Codec returns the codec, can be nil.
	Codec() Codec

	// Close closes current session.
	Close()

	// AllocateContext gets a Context ships with current session.
	AllocateContext() Context

	// Conn returns the underlined connection.
	Conn() net.Conn

	// AfterCreateHook blocks until session's on-create hook triggered.
	AfterCreateHook() <-chan struct{}

	// AfterCloseHook blocks until session's on-close hook triggered.
	AfterCloseHook() <-chan struct{}
}

Session represents a TCP session.

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