From 9d7de95373253cebdb7c4c9458a0335eedc92809 Mon Sep 17 00:00:00 2001 From: NorthLan <6995syu@163.com> Date: Tue, 19 Apr 2022 00:17:06 +0800 Subject: [PATCH] first commit --- .gitignore | 17 +++++ component/base.go | 14 ++++ component/component.go | 16 +++++ component/hub.go | 20 ++++++ component/method.go | 55 ++++++++++++++++ component/options.go | 34 ++++++++++ component/service.go | 107 ++++++++++++++++++++++++++++++ go.mod | 3 + internal/codec/codec.go | 124 +++++++++++++++++++++++++++++++++++ internal/codec/codec_test.go | 27 ++++++++ internal/log/logger.go | 33 ++++++++++ internal/packet/packet.go | 47 +++++++++++++ 12 files changed, 497 insertions(+) create mode 100644 .gitignore create mode 100644 component/base.go create mode 100644 component/component.go create mode 100644 component/hub.go create mode 100644 component/method.go create mode 100644 component/options.go create mode 100644 component/service.go create mode 100644 go.mod create mode 100644 internal/codec/codec.go create mode 100644 internal/codec/codec_test.go create mode 100644 internal/log/logger.go create mode 100644 internal/packet/packet.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94a98d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +.idea/ +.vscode/ \ No newline at end of file diff --git a/component/base.go b/component/base.go new file mode 100644 index 0000000..3153734 --- /dev/null +++ b/component/base.go @@ -0,0 +1,14 @@ +package component + +var _ Component = (*Base)(nil) + +// Base 空组件 +type Base struct{} + +func (*Base) Init() {} + +func (*Base) AfterInit() {} + +func (*Base) BeforeShutdown() {} + +func (*Base) Shutdown() {} diff --git a/component/component.go b/component/component.go new file mode 100644 index 0000000..7e5afd0 --- /dev/null +++ b/component/component.go @@ -0,0 +1,16 @@ +package component + +// Component 具体组件的接口. +type Component interface { + // Init 初始化组件时调用. + Init() + + // AfterInit 初始化组件后调用. + AfterInit() + + // BeforeShutdown 组件被停止前调用. + BeforeShutdown() + + // Shutdown 停止组件时调用. + Shutdown() +} diff --git a/component/hub.go b/component/hub.go new file mode 100644 index 0000000..39fab8f --- /dev/null +++ b/component/hub.go @@ -0,0 +1,20 @@ +package component + +type CompWithOptions struct { + Comp Component + Opts []Option +} + +type Components struct { + comps []CompWithOptions +} + +// Register registers a component to hub with options +func (cs *Components) Register(c Component, options ...Option) { + cs.comps = append(cs.comps, CompWithOptions{c, options}) +} + +// List returns all components with it's options +func (cs *Components) List() []CompWithOptions { + return cs.comps +} diff --git a/component/method.go b/component/method.go new file mode 100644 index 0000000..420a633 --- /dev/null +++ b/component/method.go @@ -0,0 +1,55 @@ +package component + +import ( + "reflect" + "unicode" + "unicode/utf8" +) + +var ( + typeOfError = reflect.TypeOf((*error)(nil)).Elem() + typeOfBytes = reflect.TypeOf(([]byte)(nil)) + //typeOfSession = reflect.TypeOf(session.New(nil)) +) + +func isExported(name string) bool { + w, _ := utf8.DecodeRuneInString(name) + return unicode.IsUpper(w) +} + +func isExportedOrBuiltinType(t reflect.Type) bool { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + // PkgPath will be non-empty even for an exported type, + // so we need to check the type name as well. + return isExported(t.Name()) || t.PkgPath() == "" +} + +// isHandlerMethod decide a method is suitable handler method +func isHandlerMethod(method reflect.Method) bool { + mt := method.Type + // Method must be exported. + if method.PkgPath != "" { + return false + } + + // Method needs three ins: receiver, *Session, []byte or pointer. + if mt.NumIn() != 3 { + return false + } + + // Method needs one outs: error + if mt.NumOut() != 1 { + return false + } + + if t1 := mt.In(1); t1.Kind() != reflect.Ptr || t1 != typeOfSession { + return false + } + + if (mt.In(2).Kind() != reflect.Ptr && mt.In(2) != typeOfBytes) || mt.Out(0) != typeOfError { + return false + } + return true +} diff --git a/component/options.go b/component/options.go new file mode 100644 index 0000000..bd26332 --- /dev/null +++ b/component/options.go @@ -0,0 +1,34 @@ +package component + +type ( + options struct { + name string // component name + nameFunc func(string) string // rename handler name + schedulerName string // schedulerName name + } + + // Option used to customize handler + Option func(options *options) +) + +// WithName used to rename component name +func WithName(name string) Option { + return func(opt *options) { + opt.name = name + } +} + +// WithNameFunc override handler name by specific function +// such as: strings.ToUpper/strings.ToLower +func WithNameFunc(fn func(string) string) Option { + return func(opt *options) { + opt.nameFunc = fn + } +} + +// WithSchedulerName set the name of the service scheduler +func WithSchedulerName(name string) Option { + return func(opt *options) { + opt.schedulerName = name + } +} diff --git a/component/service.go b/component/service.go new file mode 100644 index 0000000..81d87b2 --- /dev/null +++ b/component/service.go @@ -0,0 +1,107 @@ +package component + +import ( + "errors" + "reflect" +) + +type ( + // Handler represents a message.Message's handler's meta information. + Handler struct { + Receiver reflect.Value // receiver of method + Method reflect.Method // method stub + Type reflect.Type // arg type of method + IsRawArg bool // whether the data need to unserialize + } + + // Service implements a specific service, some of it's methods will be + // called when the correspond events is occurred. + Service struct { + Name string // name of service + Type reflect.Type // type of the receiver + Receiver reflect.Value // receiver of methods for the service + Handlers map[string]*Handler // registered methods + SchedulerName string // name of scheduler variable in session data + Options options // options + } +) + +func NewService(comp Component, opts []Option) *Service { + s := &Service{ + Type: reflect.TypeOf(comp), + Receiver: reflect.ValueOf(comp), + } + + // apply options + for i := range opts { + opt := opts[i] + opt(&s.Options) + } + if name := s.Options.name; name != "" { + s.Name = name + } else { + s.Name = reflect.Indirect(s.Receiver).Type().Name() + } + s.SchedulerName = s.Options.schedulerName + + return s +} + +// suitableMethods returns suitable methods of typ +func (s *Service) suitableHandlerMethods(typ reflect.Type) map[string]*Handler { + methods := make(map[string]*Handler) + for m := 0; m < typ.NumMethod(); m++ { + method := typ.Method(m) + mt := method.Type + mn := method.Name + if isHandlerMethod(method) { + raw := false + if mt.In(2) == typeOfBytes { + raw = true + } + // rewrite handler name + if s.Options.nameFunc != nil { + mn = s.Options.nameFunc(mn) + } + methods[mn] = &Handler{Method: method, Type: mt.In(2), IsRawArg: raw} + } + } + return methods +} + +// ExtractHandler extract the set of methods from the +// receiver value which satisfy the following conditions: +// - exported method of exported type +// - two arguments, both of exported type +// - the first argument is *session.Session +// - the second argument is []byte or a pointer +func (s *Service) ExtractHandler() error { + typeName := reflect.Indirect(s.Receiver).Type().Name() + if typeName == "" { + return errors.New("no service name for type " + s.Type.String()) + } + if !isExported(typeName) { + return errors.New("type " + typeName + " is not exported") + } + + // Install the methods + s.Handlers = s.suitableHandlerMethods(s.Type) + + if len(s.Handlers) == 0 { + str := "" + // To help the user, see if a pointer receiver would work. + method := s.suitableHandlerMethods(reflect.PtrTo(s.Type)) + if len(method) != 0 { + str = "type " + s.Name + " has no exported methods of suitable type (hint: pass a pointer to value of that type)" + } else { + str = "type " + s.Name + " has no exported methods of suitable type" + } + return errors.New(str) + } + + for i := range s.Handlers { + s.Handlers[i].Receiver = s.Receiver + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..031f9c9 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module ng + +go 1.18 diff --git a/internal/codec/codec.go b/internal/codec/codec.go new file mode 100644 index 0000000..8325aa8 --- /dev/null +++ b/internal/codec/codec.go @@ -0,0 +1,124 @@ +package codec + +import ( + "bytes" + "errors" + "ng/internal/packet" +) + +// Codec constants. +const ( + HeadLength = 4 + MaxPacketSize = 64 * 1024 +) + +// ErrPacketSizeExceed is the error used for encode/decode. +var ErrPacketSizeExceed = errors.New("codec: packet size exceed") + +// A Decoder reads and decodes network data slice +type Decoder struct { + buf *bytes.Buffer + size int // last packet length + typ byte // last packet type +} + +// NewDecoder returns a new decoder that used for decode network bytes slice. +func NewDecoder() *Decoder { + return &Decoder{ + buf: bytes.NewBuffer(nil), + size: -1, + } +} + +func (c *Decoder) forward() error { + header := c.buf.Next(HeadLength) + c.typ = header[0] + if c.typ < packet.Handshake || c.typ > packet.Kick { + return packet.ErrWrongPacketType + } + c.size = bytesToInt(header[1:]) + + // packet length limitation + if c.size > MaxPacketSize { + return ErrPacketSizeExceed + } + return nil +} + +// Decode decode the network bytes slice to packet.Packet(s) +// TODO(Warning): shared slice +func (c *Decoder) Decode(data []byte) ([]*packet.Packet, error) { + c.buf.Write(data) + + var ( + packets []*packet.Packet + err error + ) + // check length + if c.buf.Len() < HeadLength { + return nil, err + } + + // first time + if c.size < 0 { + if err = c.forward(); err != nil { + return nil, err + } + } + + for c.size <= c.buf.Len() { + p := &packet.Packet{Type: packet.Type(c.typ), Length: c.size, Data: c.buf.Next(c.size)} + packets = append(packets, p) + + // more packet + if c.buf.Len() < HeadLength { + c.size = -1 + break + } + + if err = c.forward(); err != nil { + return packets, err + } + } + + return packets, nil +} + +// Encode create a packet.Packet from the raw bytes slice and then encode to network bytes slice +// Protocol refs: https://github.com/NetEase/pomelo/wiki/Communication-Protocol +// +// --|----------------|-- +// --------|------------------------|-------- +// 1 byte packet type, 3 bytes packet data length(big end), and data segment +func Encode(typ packet.Type, data []byte) ([]byte, error) { + if typ < packet.Handshake || typ > packet.Kick { + return nil, packet.ErrWrongPacketType + } + + p := &packet.Packet{Type: typ, Length: len(data)} + buf := make([]byte, p.Length+HeadLength) + buf[0] = byte(p.Type) + + copy(buf[1:HeadLength], intToBytes(p.Length)) + copy(buf[HeadLength:], data) + + return buf, nil +} + +// Decode packet data length byte to int(Big end) +func bytesToInt(b []byte) int { + result := 0 + for _, v := range b { + result = result<<8 + int(v) + } + return result +} + +// Encode packet data length to bytes(Big end) +func intToBytes(n int) []byte { + buf := make([]byte, 3) + buf[0] = byte((n >> 16) & 0xFF) + buf[1] = byte((n >> 8) & 0xFF) + buf[2] = byte(n & 0xFF) + return buf +} diff --git a/internal/codec/codec_test.go b/internal/codec/codec_test.go new file mode 100644 index 0000000..31a99a9 --- /dev/null +++ b/internal/codec/codec_test.go @@ -0,0 +1,27 @@ +package codec + +import ( + . "ng/internal/packet" + "testing" +) + +func BenchmarkDecoder_Decode(b *testing.B) { + data := []byte("hello world") + pp1, err := Encode(Handshake, data) + if err != nil { + b.Error(err.Error()) + } + + d1 := NewDecoder() + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + packets, err := d1.Decode(pp1) + if err != nil { + b.Fatal(err) + } + if len(packets) != 1 { + b.Fatal("decode error") + } + } +} diff --git a/internal/log/logger.go b/internal/log/logger.go new file mode 100644 index 0000000..a868daa --- /dev/null +++ b/internal/log/logger.go @@ -0,0 +1,33 @@ +package log + +import ( + "log" + "os" +) + +// Logger represents the log interface +type Logger interface { + Println(v ...interface{}) + Fatal(v ...interface{}) + Fatalf(format string, v ...interface{}) +} + +func init() { + SetLogger(log.New(os.Stderr, "[NGServer]", log.LstdFlags|log.Lshortfile)) +} + +var ( + Println func(v ...interface{}) + Fatal func(v ...interface{}) + Fatalf func(format string, v ...interface{}) +) + +// SetLogger rewrites the default logger +func SetLogger(logger Logger) { + if logger == nil { + return + } + Println = logger.Println + Fatal = logger.Fatal + Fatalf = logger.Fatalf +} diff --git a/internal/packet/packet.go b/internal/packet/packet.go new file mode 100644 index 0000000..268516d --- /dev/null +++ b/internal/packet/packet.go @@ -0,0 +1,47 @@ +package packet + +import ( + "errors" + "fmt" +) + +// Type represents the network packet's type such as: handshake and so on. +type Type byte + +const ( + _ Type = iota + // Handshake represents a handshake: request(client) <====> handshake response(server) + Handshake = 0x01 + + // HandshakeAck represents a handshake ack from client to server + HandshakeAck = 0x02 + + // Heartbeat represents a heartbeat + Heartbeat = 0x03 + + // Data represents a common data packet + Data = 0x04 + + // Kick represents a kick off packet + Kick = 0x05 // disconnect message from server +) + +// ErrWrongPacketType represents a wrong packet type. +var ErrWrongPacketType = errors.New("wrong packet type") + +// Packet represents a network packet. +type Packet struct { + Type Type + Length int + Data []byte +} + +// New create a Packet instance. +func New() *Packet { + return &Packet{} +} + +// String represents the Packet's in text mode. +func (p *Packet) String() string { + return fmt.Sprintf("Type: %d, Length: %d, Data: %s", p.Type, p.Length, string(p.Data)) +}