首页 > 解决方案 > 如何使用 Go 的 net.TCPConn 处理客户端并发

问题描述

我有一个通过 TCP 通信的旧服务器。我写了Client处理通信。它可以通过命令行在单线程环境中正常工作,但是在处理并发调用时,由于单个共享的 TCPConn,它显然会失败。

会话在此旧服务器上的工作方式,我必须执行以下步骤:

尽管有一个 sessionID,但这一切都必须通过同一个 TCP 连接发生。

这已经工作了很多年,但是现在 API 服务器变得越来越忙,如果客户端同时访问 API,TCPConn 上的通信就会变得混乱。这很明显为什么会发生这种情况。现在我正在寻找一个理想的解决方案来防止它发生。

我的第一个想法是像 SQL 驱动程序那样透明地拥有一个连接池。这里的问题是,我相信每个 SQL Query()QueryRow()等都可以独立运行,因此驱动程序可以从连接池中获取 TCPConn、查询,然后将 TCPConn 返回到池中。不过,就我而言,我需要在多个查询的生命周期中保留 TCPConn。因此,它不能真正透明,因为我需要在每个服务中手动将 TCPConn 返回到池中,因为客户端无法在每次查询后返回它。

第二个想法,似乎有点粗俗,是将所有逻辑放在每个处理程序中(获取 conn,实例化Client并实例化一个或多个Service(s))。这似乎是多余的,并且随着处理程序数量的不断增长,它只是大量的样板。这将在每个处理程序中从池中释放一个 conn 以及在每个处理程序中释放一个ClientService,然后在最后将 conn 返回到池中。

这是下面的示例代码。

client.go

package whatever

import (
        "fmt"
        "log"
        "net"
        "net/http"
)

// Response from Client
type Response []byte

type Client struct {
        conn      *net.TCPConn
        username  string
        password  string
        sessionID string
}

// Open and Close the TCPConn
func (c *Client) open() error  { return nil }
func (c *Client) close() error { return nil }

// Login and Logout will call open and close respectively.  Login calls Run()
// with user/pass and gets back a sessionID which is set on the struct.
func (c *Client) Login() error  { return nil }
func (c *Client) Logout() error { return nil }

// Run sends commands to the server over the Client.conn and returns a Repsonse
// byte slice.
func (c *Client) Run(cmd string) (Response, error) { return Response{}, nil }

service.go

// UserService provides services to act on users
type UserService struct {
        client *Client
}

// AddUserGroup adds a user and then adds them to GroupA. 
func (s *UserService) AddUserGroup() error {
        err := s.Client.Login()
        if err != nil {
                return err
        }
        defer s.Client.Logout()

        resp, err := s.Client.Run("CreateUser Bob 46 'Los Angeles'")
        if err != nil {
                return err
        }

        userID := resp[1]

        cmd := fmt.Sprintf("GroupAdd %v GroupA", userID)
        resp, _ := s.Client.Run(cmd)
        if err != nil {
                return err
        }

        return nil
}

this-works-command-line.go

func main() {
        c := Client{username: "user", "password": "pass"}

        svc := UserService{client: &c}
        err := svc.AddUserGroup()
        if err != nil {
                log.Fatal(err)
        }
}

this-does-not-work-api.go

type Server struct {
        Svc *UserService
}

func (s *Server) AddUserGroupHandler() http.HandlerFunc {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                err := s.Svc.AddUserGroup()
                if err != nil {
                        w.WriteHeader(http.StatusInternalServerError)
                        return
                }
                w.WriteHeader(http.StatusCreated)
        })
}

func main() {
        c := &Client{username: "user", password: "pass"}
        svc := &UserService{client: c}

        server := Server{Svc: svc}
        err := svc.AddUserGroup()
        if err != nil {
                log.Fatal(err)
        }

        http.Handle("/foo", server.AddUserGroupHandler())
        log.Fatal(http.ListenAndServe(":8080", nil))
}

同样,如果该端点被多次命中,则将使用相同的 TCPConn 并将调用Login()或在处理前一个调用时调用其他东西。

我似乎找不到一个共同的模式来处理这个问题。我最后的想法是只接受传入的请求并将其保存在 DB 或 redis 中,然后让工作人员服务将其接收。

也许我只是没有使用正确的术语进行搜索。

任何帮助表示赞赏。

标签: gotcpconcurrencyclient

解决方案


推荐阅读