初学golang,从网上修改了一个聊天室的代码,并发时遇到了一些问题。
##服务端源码
```
package main
import (
"fmt"
"net"
"os"
"sync"
)
var (
readySendCount int
sendCount int
reciveCount int
chanreadySendCount int
chansendCount int
chanreciveCount int
lock sync.Mutex
)
func main() {
tcpAddr, err := net.ResolveTCPAddr("tcp4", ":9000")
if err != nil {
os.Exit(1)
}
server, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
os.Exit(1)
}
conns := make(map[string]net.Conn)
messages := make(chan string, 200)
go echoHandler(conns, messages)
fmt.Println("Listening...")
defer server.Close()
for {
conn, err := server.Accept()
if err != nil {
os.Exit(1)
}
lock.Lock()
conns[conn.RemoteAddr().String()] = conn
lock.Unlock()
fmt.Println("new connect :", conn.RemoteAddr().String())
go Handler(conns, conn, messages)
}
fmt.Println("close")
}
func Handler(conns map[string]net.Conn, conn net.Conn, messages chan<- string) {
buf := make([]byte, 4096)
defer conn.Close()
for {
length, err := conn.Read(buf)
if err != nil {
break
}
fmt.Printf("[ %s ] -> %d byte\n", conn.RemoteAddr().String(), length)
reciveStr := string(buf[0:length])
readySendCount++
reciveCount++
messages <- reciveStr
chanreadySendCount++
chanreciveCount++
}
lock.Lock()
delete(conns, conn.RemoteAddr().String())
lock.Unlock()
}
func echoHandler(conns map[string]net.Conn, messages <-chan string) {
for {
readySendCount--
sendCount++
msg := <-messages
chanreadySendCount--
chansendCount++
i := 0
for key, value := range conns {
_, err := value.Write([]byte(msg))
if err != nil {
lock.Lock()
delete(conns, key)
lock.Unlock()
} else {
i++
}
}
fmt.Printf("send : %d -> %d byte | Queue: %d | ReadySend / Send / All: [ %d / %d / %d ] chan: [ %d / %d / %d ]\n", i, len(msg), len(messages), readySendCount, sendCount, reciveCount, chanreadySendCount, chansendCount, chanreciveCount)
}
}
```
##本机测试
100并发测试,每50ms发送1byte数据:
```
send : 0 -> 1 byte | Queue: 5 | ReadySend / Send / All: [ 5 / 81437 / 81442 ] chan: [ -35 / 81437 / 81405 ]
send : 0 -> 1 byte | Queue: 4 | ReadySend / Send / All: [ 4 / 81438 / 81442 ] chan: [ -36 / 81438 / 81405 ]
send : 0 -> 1 byte | Queue: 3 | ReadySend / Send / All: [ 3 / 81439 / 81442 ] chan: [ -37 / 81439 / 81405 ]
send : 0 -> 1 byte | Queue: 2 | ReadySend / Send / All: [ 2 / 81440 / 81442 ] chan: [ -38 / 81440 / 81405 ]
send : 0 -> 1 byte | Queue: 1 | ReadySend / Send / All: [ 1 / 81441 / 81442 ] chan: [ -39 / 81441 / 81405 ]
send : 0 -> 1 byte | Queue: 0 | ReadySend / Send / All: [ 0 / 81442 / 81442 ] chan: [ -40 / 81442 / 81405 ]
```
1W并发测试:
```
[ 127.0.0.1:51409 ] -> 181 byte
send : 10000 -> 1 byte | Queue: 200 | ReadySend / Send / All: [ 10200 / 81 / 10281 ] chan: [ 200 / 81 / 281 ]
[ 127.0.0.1:51244 ] -> 186 byte
send : 10000 -> 1 byte | Queue: 200 | ReadySend / Send / All: [ 10200 / 82 / 10282 ] chan: [ 200 / 82 / 282 ]
[ 127.0.0.1:50654 ] -> 182 byte
send : 10000 -> 1 byte | Queue: 200 | ReadySend / Send / All: [ 10200 / 83 / 10283 ] chan: [ 200 / 83 / 283 ]
[ 127.0.0.1:51234 ] -> 186 byte
send : 10000 -> 1 byte | Queue: 200 | ReadySend / Send / All: [ 10200 / 84 / 10284 ] chan: [ 200 / 84 / 284 ]
...
send : 0 -> 215 byte | Queue: 5 | ReadySend / Send / All: [ 5 / 10314 / 10319 ] chan: [ 4 / 10314 / 10316 ]
send : 0 -> 166 byte | Queue: 4 | ReadySend / Send / All: [ 4 / 10315 / 10319 ] chan: [ 3 / 10315 / 10316 ]
send : 0 -> 153 byte | Queue: 3 | ReadySend / Send / All: [ 3 / 10316 / 10319 ] chan: [ 2 / 10316 / 10316 ]
send : 0 -> 153 byte | Queue: 2 | ReadySend / Send / All: [ 2 / 10317 / 10319 ] chan: [ 1 / 10317 / 10316 ]
send : 0 -> 153 byte | Queue: 1 | ReadySend / Send / All: [ 1 / 10318 / 10319 ] chan: [ 0 / 10318 / 10316 ]
send : 0 -> 56 byte | Queue: 0 | ReadySend / Send / All: [ 0 / 10319 / 10319 ] chan: [ -1 / 10319 / 10316 ]
```
##问题
可以发现高并发时chanreciveCount和chanreadySendCount是不准确的,这是什么原因导致的?速度方面,因为聊天室是广播聊天给每一个人,1w在线时每人每秒发一个包,服务端每秒就要发1w^2=1亿个包,不太现实,这种高并发不可能通过简单修改就能实现。
你的代码封装方式略微改进:
1、map没有加锁,按经验,连接断开与新建的时候,会崩溃。或许是客户端增加连接的时候,没有执行断开操作。
2、对于补充的计数器,也面临两个go携程的并发问题,可以封装一下,内部加锁,提供准确的计数。mutex还是很快的。
3、发送消息的时候每次都是按序执行,可以用go func来包装发送服务,参见常见的订阅发布模型的写法:(协程太多也不合适,需要考虑控制协程数)
```Go
for key, value := range conns {
go func(){//<----协程调用
_, err := value.Write([]byte(msg))
if err != nil {
delete(conns, key)
} else {
i++
}
}()
}
```
4、你的打印语句严重影响性能,可以1w5很不错了。
#1
更多评论
代码有点问题,需要增加输入参数.Sorry
```Go
for key, value := range conns {
go func(bytes []byte){//<----协程调用
_, err := value.Write(bytes)
if err != nil {
delete(conns, key)
} else {
i++
}
}(msg)
}
```
#2
谢谢。看来速度方面小修改帮助不大,只能参考模式来写了。开始map没有加锁也没出错是因为只有两个协程,操作不频繁所以没发生冲突。不过你改的参数有问题,要这样
```
for key, value := range conns {
go func(key string, value net.Conn) {
_, err := value.Write([]byte(msg))
if err != nil {
lock.Lock()
delete(conns, key)
lock.Unlock()
} else {
i++
}
}(key, value)
}
```
并发应该不影响计数,为什么`messages <- reciveStr`之后计数就不准确了呢?
#3