Giter VIP home page Giter VIP logo

easytcp's Introduction

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.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

easytcp's People

Contributors

anacrolix avatar chenjiayao avatar darthpestilane avatar dependabot[bot] avatar juneezee avatar wlbwlbwlb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

easytcp's Issues

Help with install

i got an error running my main.go
after i run the command nothing changed

error:

main.go:6:5: no required module provides package github.com/DarthPestilane/easytcp/message; to add it:
go get github.com/DarthPestilane/easytcp/message

Why delay if accept return not timeout error

in server acceptLoop

if ne, ok := err.(net.Error); ok && !ne.Timeout() {
				Log.Errorf("accept err: %s; retrying in %s", err, tempErrDelay)
				time.Sleep(tempErrDelay)
				continue
			}

why here if not timeout will accept delay in doc Most "temporary" errors are timeouts

// An Error represents a network error.
type Error interface {
	error
	Timeout() bool // Is the error a timeout?

	// Deprecated: Temporary errors are not well-defined.
	// Most "temporary" errors are timeouts, and the few exceptions are surprising.
	// Do not use this method.
	Temporary() bool
}

easytcp middleware

Is easytcp possible to add db context like Gorm to easytcp middelware ?

New Session as client ?

Is it possible to use Session as a client? I want to use Session as a client to enable server communication with another server within the internal network. Are there any examples or use cases beyond the existing examples?

why don't close respQueue after the Session closed?

i found the respQueue chan was created in session struct

type session struct {
	id          interface{}   // session's ID.
	conn        net.Conn      // tcp connection
	closed      chan struct{} // to close()
	closeOnce   sync.Once     // ensure one session only close once
	respQueue   chan Context  // response queue channel, pushed in Send() and popped in writeOutbound()
	packer      Packer        // to pack and unpack message
	codec       Codec         // encode/decode message data
	ctxPool     sync.Pool     // router context pool
	asyncRouter bool          // calls router HandlerFunc in a goroutine if false
}

but it doesn't close when calling the session close function

// Close closes the session, but doesn't close the connection.
// The connection will be closed in the server once the session's closed.
func (s *session) Close() {
	s.closeOnce.Do(func() { close(s.closed) })
}

Could you tell me your design about it, many thanks.

Q: sesdion context usage guide or example

Dear contributors,
Could you help me how to use session context or any other way to reference its starting initial request or response mesage until the end of coversation ?
Exsmple code will be big help to me and others.
Appreviate!!!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.