我正在用go编写的宠物项目中设置tcp服务器。我希望能够维护所有连接的客户端的一部分,然后在新客户端与服务器连接或断开连接时对其进行修改。
我目前的主要精神障碍是我应该声明一个包级切片,还是只是将一个切片传递给我的处理程序。
我的第一个想法是将ClientList
切片(我知道在这里切片可能不是我的最佳选择,但我决定现在将其保留为包级变量) 。虽然我认为这行得通,但我看到许多帖子不鼓励使用它们。
我的另一种想法是在主函数中将ClientList
声明为片,然后将ClientList
传递给HandleClient
函数,因此无论何时客户端连接/断开,我都可以调用AddClient
或RemoveClient
并传递此分片并添加/删除适当的客户端。
此实现如下所示。该代码肯定还有其他问题,但是我被困在试图使事情看起来很简单的事情上。
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声明为包级变量会带来任何好处?
答案 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
}