如何在Go中所有已连接的TCP客户端之间访问变量?

时间:2019-05-29 14:21:00

标签: go tcp global-variables

我正在用go编写的宠物项目中设置tcp服务器。我希望能够维护所有连接的客户端的一部分,然后在新客户端与服务器连接或断开连接时对其进行修改。

我目前的主要精神障碍是我应该声明一个包级切片,还是只是将一个切片传递给我的处理程序。

我的第一个想法是将ClientList切片(我知道在这里切片可能不是我的最佳选择,但我决定现在将其保留为包级变量) 。虽然我认为这行得通,但我看到许多帖子不鼓励使用它们。

我的另一种想法是在主函数中将ClientList声明为片,然后将ClientList传递给HandleClient函数,因此无论何时客户端连接/断开,我都可以调用AddClientRemoveClient并传递此分片并添加/删除适当的客户端。

此实现如下所示。该代码肯定还有其他问题,但是我被困在试图使事情看起来很简单的事情上。

type Client struct {
    Name string  
    Conn net.Conn
}

type ClientList []*Client

// Identify is used to set the name of the client
func (cl *Client) Identify() error {
// code here to set the client's name in the based on input from client
}

// This is not a threadsafe way to do this - need to use mutex/channels
func (cList *ClientList) AddClient(cl *Client) {
    *cList = append(*cList, cl)
}
func (cl *Client) HandleClient(cList *ClientList) {
    defer cl.Conn.Close()
    cList.AddClient(cl)
    err := cl.Identify()
    if err != nil {
        log.Println(err)
        return
    }
    for {
        err := cl.Conn.SetDeadline(time.Now().Add(20 * time.Second))
        if err != nil {
            log.Println(err)
            return
        }
        cl.Conn.Write([]byte("What command would you like to perform?\n"))
        netData, err := bufio.NewReader(cl.Conn).ReadString('\n')
        if err != nil {
            log.Println(err)
            return
        }
        cmd := strings.TrimSpace(string(netData))
        if cmd == "Ping" {
            cl.Ping() //sends a pong msg back to client
        } else {
            cl.Conn.Write([]byte("Unsupported command at this time\n"))
        }

    }
}
func main() {
    arguments := os.Args

    PORT := ":" + arguments[1]
    l, err := net.Listen("tcp4", PORT)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer l.Close()
    fmt.Println("Listening...")

    // Create a new slice to store pointers to clients
    var cList ClientList

    for {
        c, err := l.Accept()
        if err != nil {
            log.Println(err)
            return
        }
        // Create client cl1
        cl1 := Client{Conn: c}

        // Go and handle the client
        go cl1.HandleClient(&cList)
    }
}

从我的初步测试来看,这似乎可行。我可以打印出我的客户列表,并且可以看到正在添加新的客户,并且在调用Identify()之后也要添加其名称。

当我使用-race标志运行它时,会收到数据竞争警告,因此我知道我将需要一种线程安全的方式来处理添加客户端。当我添加客户端时,删除客户端也是如此。

将ClientList传递到HandleClient可能还会遗漏其他问题吗?或者,将ClientList声明为包级变量会带来任何好处?

1 个答案:

答案 0 :(得分:3)

这种方法存在几个问题。

首先,您的代码包含一个数据争用:每个TCP连接由一个单独的goroutine提供服务,并且它们都尝试同时修改分片。

您可以尝试使用go build -race(或go install -race-无论使用什么)来构建代码,并通过启用的运行时检查来查看崩溃。

这个很容易修复。最直接的方法是将互斥变量添加到ClientList类型中:

type ClientList struct {
  mu      sync.Mutex
  clients []*Client
}

…并使类型的方法在突变clients字段时保留互斥量,如下所示:

func (cList *ClientList) AddClient(cl *Client) {
  cList.mu.Lock()
  defer cList.mu.Unlock()

  cList.clients = append(cList.clients, o)
}

(如果您遇到ClientList类型的典型用法是频繁调用仅 read 包含的列表的方法,则可以开始使用sync.RWLock而是允许多个并发阅读器。)

第二,我从处理程序功能中分离出了“标识”客户端的部分。 到目前为止,在处理程序中,如果识别失败,则处理程序将退出,但客户端不会被除名。

我想最好先识别它,并且只在客户端被认为可以正常运行时才运行处理程序。

另外,应该值得在处理程序主体顶部的RemoveClient之类的地方添加一个延迟调用,以便在处理程序完成后正确地将客户端除名。

爱荷华州,我希望看到这样的东西:

func (cl *Client) HandleClient(cList *ClientList) {
    defer cl.Conn.Close()

    err := cl.Identify()
    if err != nil {
        log.Println(err)
        return
    }

    cList.AddClient(cl)
    defer cList.RemoveClient(cl)

    // ... the rest of the code
}