commit 16c6290f630b573b82f1a3e6b9d2c0199383dbe5 Author: crusader Date: Tue Apr 3 17:55:48 2018 +0900 ing diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3733e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Go template +# 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/ +*.iml + +vendor/ +glide.lock +.DS_Store +dist/ +debug diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2ca2b1d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${workspaceRoot}/main.go", + "env": {}, + "args": [], + "showLog": true + }, + { + "name": "File Debug", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${fileDirname}", + "env": {}, + "args": [], + "showLog": true + } + + ] +} \ No newline at end of file diff --git a/client.go b/client.go new file mode 100644 index 0000000..d21faa5 --- /dev/null +++ b/client.go @@ -0,0 +1,87 @@ +package server + +import ( + "crypto/tls" + "fmt" + "net" + "time" +) + +type Client struct { + Name string + + Network string + Address string + TLSConfig *tls.Config + HandshakeTimeout time.Duration + KeepAlive time.Duration + LocalAddress net.Addr + + MaxConnections int +} + +func (c *Client) Dial() (net.Conn, error) { + if err := c.Validate(); nil != err { + return nil, err + } + + var deadline time.Time + if 0 != c.HandshakeTimeout { + deadline = time.Now().Add(c.HandshakeTimeout) + } + + d := &net.Dialer{ + KeepAlive: c.KeepAlive, + Deadline: deadline, + LocalAddr: c.LocalAddress, + } + + conn, err := d.Dial(c.Network, c.Address) + if nil != err { + return nil, err + } + + if nil != c.TLSConfig { + cfg := c.TLSConfig.Clone() + tlsConn := tls.Client(conn, cfg) + if err := tlsConn.Handshake(); err != nil { + tlsConn.Close() + return nil, err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return nil, err + } + } + conn = tlsConn + } + + return conn, nil +} + +func (c *Client) Validate() error { + if "" == c.Name { + c.Name = "Client" + } + + if "" == c.Network { + return fmt.Errorf("Client: Network is not valid") + } + + if "" == c.Address { + return fmt.Errorf("Client: Address is not valid") + } + + if 0 >= c.MaxConnections { + c.MaxConnections = 1 + } + + if 0 >= c.KeepAlive { + c.KeepAlive = DefaultKeepAlive + } + if 0 >= c.HandshakeTimeout { + c.HandshakeTimeout = DefaultHandshakeTimeout + } + + return nil +} diff --git a/const.go b/const.go new file mode 100644 index 0000000..bf9ead9 --- /dev/null +++ b/const.go @@ -0,0 +1,7 @@ +package server + +const ( + DefaultHandshakeTimeout = 0 + + DefaultKeepAlive = 0 +) diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..01dc037 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,6 @@ +package: git.loafle.net/commons/server-go +import: +- package: git.loafle.net/commons/logging-go +- package: git.loafle.net/commons/util-go + subpackages: + - context diff --git a/server-context.go b/server-context.go new file mode 100644 index 0000000..a405214 --- /dev/null +++ b/server-context.go @@ -0,0 +1,19 @@ +package server + +import ( + cuc "git.loafle.net/commons/util-go/context" +) + +type ServerContext interface { + cuc.Context +} + +func NewServerContext(parent cuc.Context) ServerContext { + return &serverContext{ + ServerContext: cuc.NewContext(parent), + } +} + +type serverContext struct { + ServerContext +} diff --git a/server-handler.go b/server-handler.go new file mode 100644 index 0000000..87c762f --- /dev/null +++ b/server-handler.go @@ -0,0 +1,72 @@ +package server + +import ( + "fmt" + "net" +) + +type ServerHandler interface { + GetName() string + GetMaxConnections() int + + ServerContext() ServerContext + + Init(serverCTX ServerContext) error + OnStart(serverCTX ServerContext) error + OnError(serverCTX ServerContext, conn net.Conn, status int, reason error) + OnStop(serverCTX ServerContext) + Destroy(serverCTX ServerContext) + + Listen(serverCTX ServerContext) (net.Listener, error) + Servlet() Servlet + + Validate() +} + +type ServerHandlers struct { + ServerHandler + + Name string + MaxConnections int +} + +func (sh *ServerHandlers) ServerContext() ServerContext { + return nil +} + +func (sh *ServerHandlers) Init(serverCTX ServerContext) error { + return nil +} + +func (sh *ServerHandlers) OnStart(serverCTX ServerContext) error { + return nil +} + +func (sh *ServerHandlers) OnError(serverCTX ServerContext, conn net.Conn, status int, reason error) { +} + +func (sh *ServerHandlers) OnStop(serverCTX ServerContext) { + +} + +func (sh *ServerHandlers) Destroy(serverCTX ServerContext) { + +} + +func (sh *ServerHandlers) Listen(serverCTX ServerContext) (net.Listener, error) { + return nil, fmt.Errorf("Server: Method[ServerHandler.Listen] is not implemented") +} + +func (sh *ServerHandlers) Servlet() Servlet { + return nil +} + +func (sh *ServerHandlers) Validate() { + if "" == sh.Name { + sh.Name = "Server" + } + + if 0 >= sh.MaxConnections { + sh.MaxConnections = 0 + } +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..fae6f5e --- /dev/null +++ b/server.go @@ -0,0 +1,166 @@ +package server + +import ( + "context" + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + "git.loafle.net/commons/logging-go" +) + +type Server struct { + ServerHandler ServerHandler + + ctx ServerContext + servlets sync.Map + stopChan chan struct{} + stopWg sync.WaitGroup +} + +func (s *Server) ListenAndServe() error { + if s.stopChan != nil { + return fmt.Errorf("Server: server is already running. Stop it before starting it again") + } + + var ( + err error + listener net.Listener + ) + if nil == s.ServerHandler { + panic("Server: server handler must be specified.") + } + s.ServerHandler.Validate() + + s.ctx = s.ServerHandler.ServerContext() + if nil == s.ctx { + return fmt.Errorf("Server: ServerContext is nil") + } + + if err = s.ServerHandler.Init(s.ctx); nil != err { + return fmt.Errorf("Server: Initialization of server has been failed %v", err) + } + + if listener, err = s.ServerHandler.Listen(s.ctx); nil != err { + return err + } + + s.stopChan = make(chan struct{}) + s.stopWg.Add(1) + return s.handleLoop(listener) +} + +func (s *Server) Shutdown(ctx context.Context) error { + if s.stopChan == nil { + return fmt.Errorf("Server: server must be started before stopping it") + } + close(s.stopChan) + s.stopWg.Wait() + + s.ServerHandler.Destroy(s.ctx) + + s.stopChan = nil + + return nil +} + +func (s *Server) ConnectionSize() int { + var sz int + s.servlets.Range(func(k, v interface{}) bool { + sz++ + return true + }) + return sz +} + +func (s *Server) handleLoop(listener net.Listener) error { + var ( + stopping atomic.Value + conn net.Conn + err error + ) + + defer func() { + s.ServerHandler.OnStop(s.ctx) + + s.stopWg.Done() + }() + + if err = s.ServerHandler.OnStart(s.ctx); nil != err { + return err + } + + for { + acceptChan := make(chan struct{}) + + go func() { + if conn, err = listener.Accept(); err != nil { + if nil == stopping.Load() { + logging.Logger().Errorf("Server: Cannot accept new connection: [%s]", err) + } + } + close(acceptChan) + }() + + select { + case <-s.stopChan: + stopping.Store(true) + listener.Close() + <-acceptChan + return nil + case <-acceptChan: + } + + if nil != err { + select { + case <-s.stopChan: + return nil + case <-time.After(time.Second): + } + continue + } + + if 0 < s.ServerHandler.GetMaxConnections() { + sz := s.ConnectionSize() + if sz >= s.ServerHandler.GetMaxConnections() { + logging.Logger().Warnf("max connections size %d, refuse\n", sz) + conn.Close() + continue + } + } + + s.stopWg.Add(1) + go s.handleConnection(conn) + } +} + +func (s *Server) handleConnection(conn net.Conn) { + servlet := s.ServerHandler.Servlet() + + defer func() { + s.servlets.Delete(servlet) + s.stopWg.Done() + }() + + if nil == servlet { + logging.Logger().Errorf("Server: Servlet is nil") + } + s.servlets.Store(servlet, true) + + servletStopChan := make(chan struct{}) + doneChan := make(chan struct{}) + + go servlet.Handle(s.ctx, conn, doneChan, servletStopChan) + + select { + case <-doneChan: + close(servletStopChan) + conn.Close() + case <-s.stopChan: + close(servletStopChan) + conn.Close() + <-doneChan + } +} diff --git a/servlet.go b/servlet.go new file mode 100644 index 0000000..7853f72 --- /dev/null +++ b/servlet.go @@ -0,0 +1,7 @@ +package server + +import "net" + +type Servlet interface { + Handle(serverCTX ServerContext, conn net.Conn, doneChan chan<- struct{}, stopChan <-chan struct{}) +}