ソースを参照

First available version.

ZRY 8 ヶ月 前
コミット
a75bc7aa89
16 ファイル変更683 行追加1 行削除
  1. 1 0
      .gitignore
  2. 8 0
      .idea/.gitignore
  3. 7 0
      .idea/misc.xml
  4. 8 0
      .idea/modules.xml
  5. 11 0
      .idea/simple-serial-port-server.iml
  6. 6 0
      .idea/vcs.xml
  7. 43 1
      README.md
  8. 53 0
      cfg.go
  9. 24 0
      go.mod
  10. 46 0
      go.sum
  11. 25 0
      justfile
  12. 76 0
      main.go
  13. 144 0
      serial.go
  14. 173 0
      tcp_server.go
  15. 29 0
      udp_server.go
  16. 29 0
      ws_server.go

+ 1 - 0
.gitignore

@@ -24,3 +24,4 @@ _testmain.go
 *.test
 *.prof
 
+dist

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 7 - 0
.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="XMakeProjectSettings">
+    <option name="currentArchitecture" value="x86" />
+    <option name="workingDirectory" value="$PROJECT_DIR$" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/simple-serial-port-server.iml" filepath="$PROJECT_DIR$/.idea/simple-serial-port-server.iml" />
+    </modules>
+  </component>
+</project>

+ 11 - 0
.idea/simple-serial-port-server.iml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="Go" enabled="true" />
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/dist" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 43 - 1
README.md

@@ -1,3 +1,45 @@
 # simple-serial-port-server
 
-A simple serial port server.
+A simple serial port server.
+
+# Config
+
+Example
+
+```toml
+
+[serial]
+port = "COM1"
+baud = 9600
+# default is 8.
+data_bits = 8
+# default is 1.
+#    for one stop bit, use 1
+#    for one and a half stop bit, use 1.5
+#    for two stop bit, use 2
+#    other value will cause error
+stop_bits = 1
+# defualt is "N"
+#    "N" - None Parity
+#    "O" - Odd Parity
+#    "E" - Even Parity
+#    "M" - Parity bit is always 1
+#    "S" - Parity bit is always 0
+#    other value will cause error
+parity = "N"
+
+[server]
+# currently only support "tcp"
+# "udp" and "ws" may be supported in the future
+mode = "tcp"
+
+[server.tcp]
+# default is ‘localhost:9600’
+bind = ":9600"
+
+[perf]
+# buffer size for server read from client, default is 2048
+tx_buf_size = 2048
+# buffer size for read from serial port, default is 2048
+rx_buf_size = 2048
+```

+ 53 - 0
cfg.go

@@ -0,0 +1,53 @@
+package main
+
+import "errors"
+
+var ErrCfgVerifyFailed = errors.New("config verify failed")
+
+type ConfigDefClass struct {
+	Serial struct {
+		ComPort  string  `toml:"port"`
+		Baudrate int     `toml:"baud"`
+		DataBits int     `toml:"data_bits"`
+		StopBits float32 `toml:"stop_bits"`
+		Parity   string  `toml:"parity"`
+	} `toml:"serial"`
+	Performance struct {
+		TxBufferSize int `toml:"tx_buf_size"`
+		RxBufferSize int `toml:"rx_buf_size"`
+	} `toml:"perf"`
+	Server struct {
+		Mode      string `toml:"mode"`
+		TCPConfig struct {
+			Bind string `toml:"bind"`
+		} `toml:"tcp"`
+	} `toml:"server"`
+}
+
+func ConfigVerifyAndFillDefault() error {
+	// TODO: process default config
+	if Config.Serial.ComPort == "" {
+		App.Error("no serial port specified.")
+		return ErrCfgVerifyFailed
+	}
+	if Config.Serial.Baudrate <= 0 {
+		App.Error("baudrate should more than 0.")
+		return ErrCfgVerifyFailed
+	}
+	if Config.Serial.DataBits == 0 {
+		Config.Serial.DataBits = 8
+	}
+	if Config.Serial.StopBits == 0 {
+		Config.Serial.StopBits = 1
+	}
+	if Config.Serial.Parity == "" {
+		Config.Serial.Parity = "N"
+	}
+	if Config.Performance.TxBufferSize <= 0 {
+		Config.Performance.TxBufferSize = 2048
+	}
+	if Config.Performance.RxBufferSize <= 0 {
+		Config.Performance.RxBufferSize = 2048
+	}
+	return nil
+}

+ 24 - 0
go.mod

@@ -0,0 +1,24 @@
+module simple-serial-port-server
+
+go 1.20
+
+require (
+	git.swzry.com/zry/zry-go-program-framework/core v0.0.0-20230909163811-a6d54dcab998
+	git.swzry.com/zry/zry-go-program-framework/easy_toml_config v0.0.0-20230909163811-a6d54dcab998
+	git.swzry.com/zry/zry-go-program-framework/svcfw v0.0.0-20230909163811-a6d54dcab998
+	github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f
+	github.com/puzpuzpuz/xsync v1.5.2
+	github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
+)
+
+require (
+	git.swzry.com/zry/GoHiedaLogger/hiedabke_console v0.0.0-20230814164330-c2545e8bfba1 // indirect
+	git.swzry.com/zry/GoHiedaLogger/hiedalog v0.0.0-20230814164330-c2545e8bfba1 // indirect
+	github.com/edofic/go-ordmap/v2 v2.0.0 // indirect
+	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
+	github.com/oklog/run v1.1.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+	github.com/s0rg/trie v1.2.0 // indirect
+	github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 // indirect
+	golang.org/x/sys v0.12.0 // indirect
+)

+ 46 - 0
go.sum

@@ -0,0 +1,46 @@
+git.swzry.com/zry/GoHiedaLogger/hiedabke_console v0.0.0-20230814164330-c2545e8bfba1 h1:68DRimhaFe4Tvx4kMIDNts/VpOnt4LxkRVXGmpOeHmc=
+git.swzry.com/zry/GoHiedaLogger/hiedabke_console v0.0.0-20230814164330-c2545e8bfba1/go.mod h1:QKGs1+W8+y6/RMTSaAv6LwJnY9/7zaGs38IbwmYlFbo=
+git.swzry.com/zry/GoHiedaLogger/hiedalog v0.0.0-20230814164330-c2545e8bfba1 h1:5lcgeoBT7tvU/XH9jKlzQguiYjLbdh7SY4UsUf/Ek00=
+git.swzry.com/zry/GoHiedaLogger/hiedalog v0.0.0-20230814164330-c2545e8bfba1/go.mod h1:NMU7558kNXCUuK0qKYQMtYK/kn2lhwelnij295H3pdU=
+git.swzry.com/zry/zry-go-program-framework/core v0.0.0-20230909163811-a6d54dcab998 h1:Qd6qjkvm6HWKlMtannlAm8RQkyYPMFyWXuMeZWUDdQM=
+git.swzry.com/zry/zry-go-program-framework/core v0.0.0-20230909163811-a6d54dcab998/go.mod h1:3qpblCnuLmLKgdagaxqvx7HZyY+cOxuTHOCXupkIrt0=
+git.swzry.com/zry/zry-go-program-framework/easy_toml_config v0.0.0-20230909163811-a6d54dcab998 h1:aHFJHGyKNHAEr0IuLzfk9YQECNCEaYirMaW2xo9NcJU=
+git.swzry.com/zry/zry-go-program-framework/easy_toml_config v0.0.0-20230909163811-a6d54dcab998/go.mod h1:a8xLJHASVL6WGsL/RsMZFpqUA3RTEWlXQD2IgdrAPPM=
+git.swzry.com/zry/zry-go-program-framework/svcfw v0.0.0-20230909163811-a6d54dcab998 h1:9+nlO/e7a3PT8haqlMJdGZ7AdC1gleOls0ZoRM0rw4s=
+git.swzry.com/zry/zry-go-program-framework/svcfw v0.0.0-20230909163811-a6d54dcab998/go.mod h1:t7jiT8T3LWA0Zv6fgX+z8yb6PX8yFpZ+/bh0wvyjTCo=
+github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0=
+github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/edofic/go-ordmap/v2 v2.0.0 h1:pWiYELTJdFAQ00TQSfA4PoyYsAfKxHNUff9x88lWZlY=
+github.com/edofic/go-ordmap/v2 v2.0.0/go.mod h1:BF4fX5pcyMRO/VidWMgFfDleHEdg+wPpCfQbORyOSCY=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
+github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY=
+github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg=
+github.com/s0rg/trie v1.2.0 h1:sN0EvZuqIxleXZECZ5GZqd+jOYRMR/yZeSPfWSrZr48=
+github.com/s0rg/trie v1.2.0/go.mod h1:P+hJUWvPu/imKrsdzOrVswr8Mme6GgFtZfBKojYYkfk=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
+github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4=
+github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
+golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 25 - 0
justfile

@@ -0,0 +1,25 @@
+#!/usr/bin/env just --justfile
+
+jfdir := replace(justfile_directory(), "\\", "/")
+dist := jfdir / "dist"
+
+goos := `go env GOOS`
+
+executable_suffix := if goos == "windows" { ".exe" } else { "" }
+
+export GOOS := goos
+
+default:
+  just --list
+
+tidy:
+	go mod tidy -e
+
+update:
+	go get -u
+
+build:
+	go build -o {{dist / "SimpleSerialPortServer" + executable_suffix}}
+
+run:
+	cd {{dist}} && {{"./SimpleSerialPortServer" + executable_suffix}}

+ 76 - 0
main.go

@@ -0,0 +1,76 @@
+package main
+
+import (
+	"flag"
+	"git.swzry.com/zry/zry-go-program-framework/core"
+	"git.swzry.com/zry/zry-go-program-framework/easy_toml_config"
+	"git.swzry.com/zry/zry-go-program-framework/svcfw"
+	"os"
+)
+
+var ArgsHelp bool
+var ArgsCli bool
+var ArgsCfg string
+
+var App *svcfw.AppFramework
+var Config ConfigDefClass
+
+var SubSvcSerial *SerialSubSvc
+var SubSvcServer core.ISubService
+
+func main() {
+	flag.BoolVar(&ArgsHelp, "h", false, "print help")
+	flag.BoolVar(&ArgsCli, "cli", false, "specify config by cli args")
+	flag.StringVar(&ArgsCfg, "cfg", "config.toml", "specify cfg file, default is 'config.toml'")
+	// TODO: Add more args
+	flag.Parse()
+	if ArgsHelp {
+		flag.PrintDefaults()
+		return
+	}
+	App = svcfw.NewAppFramework(true, "main")
+	App.InitConsoleLogBackend(os.Stdout, "")
+	if ArgsCli {
+		App.Panic("load config from cli not supported yet.")
+		// TODO: Config from CLI Args
+		//App.Info("load config from cli args...")
+	} else {
+		App.InfoF("load config from '%s'...", ArgsCfg)
+		App.MustPrepare("config", func() error {
+			return easy_toml_config.LoadConfigFromFile(ArgsCfg, &Config)
+		})
+		App.MustPrepare("config", ConfigVerifyAndFillDefault)
+	}
+	switch Config.Server.Mode {
+	case "tcp":
+		SubSvcServer = NewTCPServerSubSvc()
+		break
+	case "udp":
+		SubSvcServer = NewUDPServerSubSvc()
+		break
+	case "ws":
+		SubSvcServer = NewWSServerSubSvc()
+		break
+	default:
+		App.PanicF("invalid server mode '%s'", Config.Server.Mode)
+		return
+	}
+	SubSvcSerial = NewSerialSubSvc()
+	App.AddSubSvc("serial", SubSvcSerial)
+	App.AddSubSvc("server", SubSvcServer)
+	App.AddSubSvc("sig-quit", svcfw.NewWatchSignalExitSubServiceWithDefault())
+
+	App.Info("preparing sub services...")
+	err := App.Prepare()
+	if err != nil {
+		App.Panic("error in preparing sub services: ", err)
+	}
+
+	App.Info("start running sub services...")
+	err = App.Run()
+
+	App.Info("all sub services end.")
+	if err != nil {
+		App.Panic("error in running sub services: ", err)
+	}
+}

+ 144 - 0
serial.go

@@ -0,0 +1,144 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	zgpf_core "git.swzry.com/zry/zry-go-program-framework/core"
+	"github.com/tarm/serial"
+	"sync"
+)
+
+var _ zgpf_core.ISubService = (*SerialSubSvc)(nil)
+
+type SerialSubSvc struct {
+	serialCfg     *serial.Config
+	serialPort    *serial.Port
+	subSvcContext context.Context
+	subSvcCancel  context.CancelFunc
+	TxChan        chan []byte
+	RxChan        chan []byte
+}
+
+func NewSerialSubSvc() *SerialSubSvc {
+	s := &SerialSubSvc{
+		TxChan: make(chan []byte),
+		RxChan: make(chan []byte),
+	}
+	return s
+}
+
+func (s *SerialSubSvc) Prepare(ctx *zgpf_core.SubServiceContext) error {
+	sd, ss, sp, err := GetDnSnP(Config.Serial.DataBits, Config.Serial.StopBits, Config.Serial.Parity)
+	if err != nil {
+		ctx.Error("invalid serial port config: ", err)
+		return errors.New("invalid serial port config")
+	}
+	s.serialCfg = &serial.Config{
+		Name:     Config.Serial.ComPort,
+		Baud:     Config.Serial.Baudrate,
+		Size:     sd,
+		Parity:   sp,
+		StopBits: ss,
+	}
+	return nil
+}
+
+func (s *SerialSubSvc) Run(ctx *zgpf_core.SubServiceContext) error {
+	s.subSvcContext, s.subSvcCancel = context.WithCancel(ctx.GetParentContext())
+	sport, err := serial.OpenPort(s.serialCfg)
+	if err != nil {
+		ctx.ErrorF("failed open serial port '%s': %v", s.serialCfg.Name, err)
+		return errors.New("failed open serial port")
+	}
+	s.serialPort = sport
+	ctx.InfoF("serial port '%s' opened", s.serialCfg.Name)
+	defer s.serialPort.Close()
+	wg := sync.WaitGroup{}
+	wg.Add(2)
+	go func() {
+		defer wg.Done()
+		readbuf := make([]byte, Config.Performance.RxBufferSize)
+		for {
+			n, xerr := s.serialPort.Read(readbuf)
+			if xerr != nil {
+				ctx.Error("error in read serial port: ", xerr)
+				return
+			}
+			s.RxChan <- readbuf[:n]
+		}
+	}()
+	go func() {
+		defer wg.Done()
+		for {
+			select {
+			case <-s.subSvcContext.Done():
+				return
+			case txdata := <-s.TxChan:
+				_, xerr := s.serialPort.Write(txdata)
+				if xerr != nil {
+					ctx.Error("error in write serial port: ", xerr)
+					return
+				}
+			}
+		}
+	}()
+	wg.Wait()
+	ctx.Info("serial sub service end.")
+	return nil
+}
+
+func (s *SerialSubSvc) Stop(ctx *zgpf_core.SubServiceContext) {
+	if s.subSvcCancel != nil {
+		s.subSvcCancel()
+	}
+	if s.serialPort != nil {
+		err := s.serialPort.Close()
+		if err != nil {
+			ctx.Warn("error in closing serial port: ", err)
+		}
+	}
+}
+
+func GetDnSnP(d int, s float32, p string) (byte, serial.StopBits, serial.Parity, error) {
+	if d < 0 {
+		return 0, 0, 0, fmt.Errorf("invalid databits: value %d is less than 0", d)
+	}
+	if d > 255 {
+		return 0, 0, 0, fmt.Errorf("invalid databits: value %d is more than 255", d)
+	}
+	sd := byte(d)
+	var ss serial.StopBits
+	switch s {
+	case 1:
+		ss = serial.Stop1
+		break
+	case 1.5:
+		ss = serial.Stop1Half
+	case 2:
+		ss = serial.Stop2
+	default:
+		return 0, 0, 0, fmt.Errorf("invalid stopbits:  %f (should be 1/1.5/2)", s)
+	}
+	var sp serial.Parity
+	switch p {
+	case "N":
+		sp = serial.ParityNone
+		break
+	case "O":
+		sp = serial.ParityOdd
+		break
+	case "E":
+		sp = serial.ParityEven
+		break
+	case "M":
+		sp = serial.ParityMark
+		break
+	case "S":
+		sp = serial.ParitySpace
+		break
+	default:
+		return 0, 0, 0, fmt.Errorf("invalid parity:  '%s' (should be N/O/E/M/S)", p)
+	}
+	return sd, ss, sp, nil
+}

+ 173 - 0
tcp_server.go

@@ -0,0 +1,173 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"git.swzry.com/zry/zry-go-program-framework/core"
+	"github.com/GUAIK-ORG/go-snowflake/snowflake"
+	"github.com/puzpuzpuz/xsync"
+	"net"
+	"sync"
+)
+
+var _ core.ISubService = (*TCPServerSubSvc)(nil)
+
+type TCPServerSubSvc struct {
+	bindAddr      string
+	listener      net.Listener
+	snflk         *snowflake.Snowflake
+	sessions      *xsync.MapOf[int64, *ClientSession]
+	rxRoutineCtx  context.Context
+	rxRoutineCncl context.CancelFunc
+}
+
+func NewTCPServerSubSvc() *TCPServerSubSvc {
+	s := &TCPServerSubSvc{
+		sessions: xsync.NewIntegerMapOf[int64, *ClientSession](),
+	}
+	return s
+}
+
+func (s *TCPServerSubSvc) Prepare(ctx *core.SubServiceContext) error {
+	s.bindAddr = Config.Server.TCPConfig.Bind
+	if s.bindAddr == "" {
+		ctx.Info("bind address not specify, will use 'localhost:9600'")
+		s.bindAddr = "localhost:9600"
+	}
+	snflk, err := snowflake.NewSnowflake(0, 0)
+	if err != nil {
+		ctx.Error("failed init snowflake generator: ", err)
+		return errors.New("failed init snowflake generator")
+	}
+	s.snflk = snflk
+	return nil
+}
+
+func (s *TCPServerSubSvc) Run(ctx *core.SubServiceContext) error {
+	s.rxRoutineCtx, s.rxRoutineCncl = context.WithCancel(ctx.GetParentContext())
+	listener, err := net.Listen("tcp", s.bindAddr)
+	if err != nil {
+		ctx.ErrorF("failed listen at '%s': %v", s.bindAddr, err)
+		return errors.New("listen error")
+	}
+	s.listener = listener
+	var wg sync.WaitGroup
+	var rxwg sync.WaitGroup
+	rxwg.Add(1)
+	go s.rxRoutine(&rxwg)
+	ctx.Info("rx routine started.")
+	ctx.InfoF("listening at '%s'", s.bindAddr)
+	for {
+		var conn net.Conn
+		conn, err = listener.Accept()
+		if err != nil {
+			ctx.Warn("error in accepting connection: ", err)
+			break
+		}
+		sid := s.snflk.NextVal()
+		clog := ctx.GetSubLog(fmt.Sprintf("client-%16X", sid))
+		clog.VerboseF("client '%16X' from '%s' connected.", sid, conn.RemoteAddr())
+		sess := &ClientSession{
+			ID:         sid,
+			Conn:       conn,
+			Logger:     clog,
+			ListenerWg: &wg,
+		}
+		s.sessions.Store(sid, sess)
+		wg.Add(1)
+		go s.handleClient(sess)
+	}
+	ctx.Info("listener end.")
+	ctx.Info("wait for rx routines end...")
+	rxwg.Wait()
+	ctx.Info("wait for all conn routines end...")
+	wg.Wait()
+	ctx.Info("tcp server end.")
+	return nil
+}
+
+func (s *TCPServerSubSvc) Stop(ctx *core.SubServiceContext) {
+	if s.listener != nil {
+		err := s.listener.Close()
+		if err != nil {
+			ctx.Warn("error in closing listener: ", err)
+		}
+	}
+	if s.rxRoutineCncl != nil {
+		s.rxRoutineCncl()
+	}
+	s.sessions.Range(func(key int64, value *ClientSession) bool {
+		ctx.DebugF("closing client '%16X'...", key)
+		c := value.Conn
+		if c != nil {
+			err := c.Close()
+			if err != nil {
+				if value.Logger != nil {
+					value.Logger.Warn("error in closing client: ", err)
+				} else {
+					ctx.WarnF("error in closing client '%16X': %v", key, err)
+				}
+			}
+		}
+		return true
+	})
+}
+
+func (s *TCPServerSubSvc) rxRoutine(wg *sync.WaitGroup) {
+	defer wg.Done()
+	for {
+		select {
+		case <-s.rxRoutineCtx.Done():
+			return
+		case rxdata := <-SubSvcSerial.RxChan:
+			s.broadcastToClient(rxdata)
+		}
+	}
+}
+
+func (s *TCPServerSubSvc) broadcastToClient(data []byte) {
+	s.sessions.Range(func(key int64, value *ClientSession) bool {
+		c := value.Conn
+		if c != nil {
+			_, err := c.Write(data)
+			if err != nil {
+				if value.Logger != nil {
+					value.Logger.Verbose("write data error: ", err)
+				}
+				err = c.Close()
+				if err != nil && value.Logger != nil {
+					value.Logger.Verbose("error in closing conn: ", err)
+				}
+			}
+		}
+		return true
+	})
+}
+
+func (s *TCPServerSubSvc) handleClient(sess *ClientSession) {
+	defer s.sessions.Delete(sess.ID)
+	defer sess.ListenerWg.Done()
+	defer sess.Conn.Close()
+	defer sess.Logger.CloseThisLog()
+	defer sess.Logger.VerboseF("client '%16X' end.", sess.ID)
+	buf := make([]byte, Config.Performance.TxBufferSize)
+	for {
+		n, err := sess.Conn.Read(buf)
+		if err != nil {
+			sess.Logger.Verbose("error in read: ", err)
+			return
+		}
+		data := buf[:n]
+		if n > 0 {
+			SubSvcSerial.TxChan <- data
+		}
+	}
+}
+
+type ClientSession struct {
+	ID         int64
+	Conn       net.Conn
+	Logger     core.IModuleLogger
+	ListenerWg *sync.WaitGroup
+}

+ 29 - 0
udp_server.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	"errors"
+	"git.swzry.com/zry/zry-go-program-framework/core"
+)
+
+var _ core.ISubService = (*UDPServerSubSvc)(nil)
+
+type UDPServerSubSvc struct {
+}
+
+func NewUDPServerSubSvc() *UDPServerSubSvc {
+	s := &UDPServerSubSvc{}
+	return s
+}
+
+func (T UDPServerSubSvc) Prepare(ctx *core.SubServiceContext) error {
+	ctx.Error("currently not support udp")
+	return errors.New("currently not support udp")
+}
+
+func (T UDPServerSubSvc) Run(ctx *core.SubServiceContext) error {
+	ctx.Error("currently not support udp")
+	return errors.New("currently not support udp")
+}
+
+func (T UDPServerSubSvc) Stop(ctx *core.SubServiceContext) {
+}

+ 29 - 0
ws_server.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	"errors"
+	"git.swzry.com/zry/zry-go-program-framework/core"
+)
+
+var _ core.ISubService = (*WSServerSubSvc)(nil)
+
+type WSServerSubSvc struct {
+}
+
+func NewWSServerSubSvc() *WSServerSubSvc {
+	s := &WSServerSubSvc{}
+	return s
+}
+
+func (T WSServerSubSvc) Prepare(ctx *core.SubServiceContext) error {
+	ctx.Error("currently not support websocket")
+	return errors.New("currently not support websocket")
+}
+
+func (T WSServerSubSvc) Run(ctx *core.SubServiceContext) error {
+	ctx.Error("currently not support websocket")
+	return errors.New("currently not support websocket")
+}
+
+func (T WSServerSubSvc) Stop(ctx *core.SubServiceContext) {
+}