diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55e332b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +inp2025 diff --git a/cmds/client.go b/cmds/client.go new file mode 100644 index 0000000..9c8e459 --- /dev/null +++ b/cmds/client.go @@ -0,0 +1,45 @@ +package cmds + +import ( + "fmt" + "inp2025/tcp" + + "github.com/spf13/cobra" +) + +func ping() { + msgs := []string{"hello, world", "goodbye"} + for _, msg := range msgs { + fmt.Printf("client sending: '%s'\n", msg) + + b, err := tcp.Post(":8080", "/", []byte(msg)) + if err != nil { + panic(err) + } + + fmt.Printf("server reply: '%s'\n", string(b)) + } +} + +func pull() { + socket, err := tcp.Dial(":8080", "/") + if err != nil { + panic(err) + } + defer socket.Shutdown() + for i := 0; i < 5; i++ { + b, err := socket.Read() + if err != nil { + panic(err) + } + + fmt.Printf("server send: '%s'\n", string(b)) + } +} + +var clientCmd = &cobra.Command{ + Use: "client", + Run: func(cmd *cobra.Command, args []string) { + pull() + }, +} diff --git a/cmds/root.go b/cmds/root.go new file mode 100644 index 0000000..5ea89d1 --- /dev/null +++ b/cmds/root.go @@ -0,0 +1,34 @@ +package cmds + +import ( + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +var RootCmd = &cobra.Command{ + Use: "lab4", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.BindPFlags(cmd.PersistentFlags()) + viper.BindPFlags(cmd.Flags()) + + cfg := zap.NewProductionConfig() + logger, err := cfg.Build() + if err != nil { + panic(err) + } + + zap.ReplaceGlobals(logger) + }, +} + +func init() { + cobra.EnableTraverseRunHooks = true + + RootCmd.AddCommand(serverCmd) + RootCmd.AddCommand(clientCmd) +} diff --git a/cmds/server.go b/cmds/server.go new file mode 100644 index 0000000..1a4176d --- /dev/null +++ b/cmds/server.go @@ -0,0 +1,40 @@ +package cmds + +import ( + "fmt" + "inp2025/middlewares" + "inp2025/tcp" + "io" + "time" + + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +func pongHandler(w tcp.ResponseWriter, req *tcp.Request) error { + w.WriteHeader(tcp.StatusOK) + _, err := io.WriteString(w, string(req.Body)) + return err +} + +func tickHandler(w tcp.ResponseWriter, req *tcp.Request) error { + zap.L().Info("Run tickHandler") + for { + w.SocketSendString(fmt.Sprintf("time: %s", time.Now().String())) + time.Sleep(time.Second) + } + return nil +} + +var serverCmd = &cobra.Command{ + Use: "server", + Run: func(cmd *cobra.Command, args []string) { + router := tcp.NewRouter() + router.Use(middlewares.ErrorHandler) + router.Use(middlewares.AccessLog) + router.Register(tcp.MethodSOCKET, "/", tickHandler) + router.Register(tcp.MethodPOST, "/", pongHandler) + + router.Listen(":8080") + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..98197ff --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module inp2025 + +go 1.25.2 + +require ( + github.com/spf13/cobra v1.10.1 + github.com/spf13/viper v1.21.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.28.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2018475 --- /dev/null +++ b/go.sum @@ -0,0 +1,60 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work new file mode 100644 index 0000000..864cc4b --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.25.2 + +use . diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..dc41675 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,3 @@ +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d662f8e --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "inp2025/cmds" + +func main() { + cmds.RootCmd.Execute() +} diff --git a/middlewares/accessLog.go b/middlewares/accessLog.go new file mode 100644 index 0000000..dcb016d --- /dev/null +++ b/middlewares/accessLog.go @@ -0,0 +1,16 @@ +package middlewares + +import ( + "inp2025/tcp" + + "go.uber.org/zap" +) + +func AccessLog(next tcp.Handler) tcp.Handler { + return func(w tcp.ResponseWriter, req *tcp.Request) error { + zap.L().Info("access", + zap.String("method", string(req.Method)), + zap.String("route", req.Route)) + return next(w, req) + } +} diff --git a/middlewares/errorHandler.go b/middlewares/errorHandler.go new file mode 100644 index 0000000..d95e4f1 --- /dev/null +++ b/middlewares/errorHandler.go @@ -0,0 +1,57 @@ +package middlewares + +import ( + "inp2025/tcp" + "io" + "net/http" + + "go.uber.org/zap" +) + +type Error struct { + StatusCode int `json:"code"` + Message string `json:"message"` + OriginError error `json:"-"` +} + +func (e Error) Error() string { + return e.Message +} + +func NewError(err error) Error { + return Error{ + StatusCode: http.StatusInternalServerError, + Message: "Internal server error with unknown reason", + OriginError: err, + } +} + +func ErrorHandler(next tcp.Handler) tcp.Handler { + return func(w tcp.ResponseWriter, req *tcp.Request) error { + originErr := next(w, req) + + var err Error + switch originErr := originErr.(type) { + case nil: + return nil + + case Error: + err = originErr + + default: + err = NewError(originErr) + } + + if err.OriginError == nil { + zap.L().Warn(err.Message) + } else { + zap.L().Error(err.Message, + zap.Error(err.OriginError)) + } + + w.WriteHeader(err.StatusCode) + io.WriteString(w, err.Message) + + return err + } +} diff --git a/tcp/client.go b/tcp/client.go new file mode 100644 index 0000000..a966464 --- /dev/null +++ b/tcp/client.go @@ -0,0 +1,45 @@ +package tcp + +import ( + "encoding/json" + "net" +) + +func Do(addr, route string, method Method, body []byte) ([]byte, error) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return []byte{}, err + } + defer conn.Close() + header := RequestHeader{ + Method: method, + Route: route, + } + + rawHeader, err := json.Marshal(header) + if err != nil { + return []byte{}, err + } + if err := sendFrame(conn, rawHeader); err != nil { + return []byte{}, err + } + + if err := sendFrame(conn, body); err != nil { + return []byte{}, err + } + + b, err := readFrame(conn) + if err != nil { + return []byte{}, err + } + + return b, nil +} + +func Get(addr, route string) ([]byte, error) { + return Do(addr, route, MethodGET, []byte{}) +} + +func Post(addr, route string, b []byte) ([]byte, error) { + return Do(addr, route, MethodPOST, b) +} diff --git a/tcp/request.go b/tcp/request.go new file mode 100644 index 0000000..8f6a76c --- /dev/null +++ b/tcp/request.go @@ -0,0 +1,53 @@ +package tcp + +import ( + "encoding/json" + "net" +) + +type Method string + +const ( + MethodGET = "GET" + MethodPOST = "POST" + MethodPUT = "PUT" + MethodDELETE = "DELETE" + + MethodSOCKET = "SOCKET" +) + +type RequestHeader struct { + Method Method `json:"method"` + Route string `json:"route"` +} + +type Request struct { + Method Method + Route string + + RemoteAddr string + + Body []byte +} + +func (self *Request) Header() ([]byte, error) { + b, err := json.Marshal(RequestHeader{ + Method: self.Method, + Route: self.Route, + }) + if err != nil { + return []byte{}, err + } + return b, nil +} + +func NewRequest(conn net.Conn, header RequestHeader, body []byte) *Request { + return &Request{ + Method: header.Method, + Route: header.Route, + + RemoteAddr: conn.RemoteAddr().String(), + + Body: body, + } +} diff --git a/tcp/response.go b/tcp/response.go new file mode 100644 index 0000000..9d29ca5 --- /dev/null +++ b/tcp/response.go @@ -0,0 +1,90 @@ +package tcp + +import ( + "encoding/json" + "net" + "net/http" + "strings" +) + +const ( + StatusOK int = 200 + + StatusBadRequest = 400 + StatusUnauthorized = 401 + StatusForbidden = 403 + StatusNotFound = 404 + + StatusInternalServerError = 500 +) + +type ResponseHeader struct { + StatusCode int `json:"status_code"` +} + +type Response struct { + StatusCode int + + Body []byte + + RemoteAddr string +} + +func (self *Response) Header() ([]byte, error) { + b, err := json.Marshal(ResponseHeader{ + StatusCode: self.StatusCode, + }) + if err != nil { + return []byte{}, err + } + return b, nil +} + +type ResponseWriter interface { + Write([]byte) (int, error) + WriteHeader(statusCode int) + SocketSend([]byte) error + SocketSendString(string) error +} + +type ResponseBuilder struct { + // For socket to send without closing connection + conn net.Conn + + b strings.Builder + statusCode int +} + +func NewResponseBuilder(conn net.Conn) *ResponseBuilder { + return &ResponseBuilder{ + conn: conn, + + statusCode: http.StatusInternalServerError, + } +} + +func (self *ResponseBuilder) Write(b []byte) (int, error) { + return self.b.Write(b) +} + +func (self *ResponseBuilder) WriteHeader(statusCode int) { + self.statusCode = statusCode +} + +func (self *ResponseBuilder) Build(req *Request) *Response { + return &Response{ + StatusCode: self.statusCode, + + RemoteAddr: req.RemoteAddr, + + Body: []byte(self.b.String()), + } +} + +func (self *ResponseBuilder) SocketSend(b []byte) error { + return sendFrame(self.conn, b) +} + +func (self *ResponseBuilder) SocketSendString(s string) error { + return self.SocketSend([]byte(s)) +} diff --git a/tcp/router.go b/tcp/router.go new file mode 100644 index 0000000..94fdf94 --- /dev/null +++ b/tcp/router.go @@ -0,0 +1,114 @@ +package tcp + +import ( + "encoding/json" + "io" + "net" + + "go.uber.org/zap" +) + +type Handler func(w ResponseWriter, req *Request) error +type Middleware func(next Handler) Handler + +type Router struct { + middlewares []Middleware + routes map[Method]map[string]Handler +} + +func NewRouter() *Router { + return &Router{ + routes: make(map[Method]map[string]Handler), + } +} + +func (self *Router) Use(middleware Middleware) { + self.middlewares = append(self.middlewares, middleware) +} + +func (self *Router) Register(method Method, route string, handler Handler) { + _, ok := self.routes[method] + if !ok { + self.routes[method] = make(map[string]Handler) + } + + for _, middleware := range self.middlewares { + handler = middleware(handler) + } + self.routes[method][route] = handler +} + +func (self *Router) run(conn net.Conn, req *Request) { + handler, ok := self.routes[req.Method][req.Route] + if !ok { + zap.L().Warn("route not exist", + zap.String("route", req.Route)) + return + } + + w := NewResponseBuilder(conn) + err := handler(w, req) + if err != nil { + zap.L().Error("failed to run handler", + zap.Error(err)) + return + } + + res := w.Build(req) + header, err := res.Header() + if err != nil { + zap.L().Error("failed to marshal header", + zap.Error(err)) + return + } + if err := sendFrame(conn, header); err != nil { + zap.L().Error("failed to write header", + zap.Error(err)) + return + } + if err := sendFrame(conn, res.Body); err != nil { + zap.L().Error("failed to write body", + zap.Error(err)) + return + } +} + +func (self *Router) Listen(addr string) error { + listener, err := net.Listen("tcp", addr) + if err != nil { + return err + } + defer listener.Close() + + for { + conn, err := listener.Accept() + if err != nil { + return err + } + + rawHeader, err := readFrame(conn) + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + body, err := readFrame(conn) + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + var header RequestHeader + if err := json.Unmarshal(rawHeader, &header); err != nil { + return err + } + req := NewRequest(conn, header, body) + go self.run(conn, req) + } + + return nil +} diff --git a/tcp/socket.go b/tcp/socket.go new file mode 100644 index 0000000..52ec817 --- /dev/null +++ b/tcp/socket.go @@ -0,0 +1,51 @@ +package tcp + +import ( + "encoding/json" + "net" +) + +type ShutdownFunc func() error + +type SocketConn struct { + conn net.Conn + Shutdown ShutdownFunc +} + +func (self *SocketConn) Read() ([]byte, error) { + return readFrame(self.conn) +} + +func (self *SocketConn) Write(b []byte) error { + return sendFrame(self.conn, b) +} + +func Dial(addr, route string) (*SocketConn, error) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + header := RequestHeader{ + Method: MethodSOCKET, + Route: route, + } + + rawHeader, err := json.Marshal(header) + if err != nil { + return nil, err + } + if err := sendFrame(conn, rawHeader); err != nil { + return nil, err + } + // Empty body + if err := sendFrame(conn, []byte{}); err != nil { + return nil, err + } + + return &SocketConn{ + conn: conn, + Shutdown: func() error { + return conn.Close() + }, + }, nil +} diff --git a/tcp/utils.go b/tcp/utils.go new file mode 100644 index 0000000..c07a6ef --- /dev/null +++ b/tcp/utils.go @@ -0,0 +1,44 @@ +package tcp + +import ( + "encoding/binary" + "io" + "net" +) + +const ( + LengthPrefixSize = 4 +) + +func readFrame(conn net.Conn) ([]byte, error) { + lengthBytes := make([]byte, LengthPrefixSize) + _, err := io.ReadFull(conn, lengthBytes) + if err != nil { + return []byte{}, err + } + + messageLength := binary.BigEndian.Uint32(lengthBytes) + + payload := make([]byte, messageLength) + _, err = io.ReadFull(conn, payload) + if err != nil { + return []byte{}, err + } + + return payload, nil +} + +func sendFrame(conn net.Conn, data []byte) error { + messageLength := uint32(len(data)) + + lengthBytes := make([]byte, LengthPrefixSize) + binary.BigEndian.PutUint32(lengthBytes, messageLength) + + _, err := conn.Write(lengthBytes) + if err != nil { + return err + } + + _, err = conn.Write(data) + return err +}