在上一篇文章和这个之间还有一个41-用户业务封装,这一块我觉得主要就是OOP封装思想的体现,没有涉及过多的Go语言特性,所以就略过代码解析了。这一篇对应的是在线用户查询和修改用户名的业务逻辑,因为代码比较简单,流程相似,合并在一篇中解析。代码是在进行业务封装的基础之上的。
本期课件
视频:42-在线用户查询
43-修改用户名
代码:
packagemainimport("net""strings")typeUserstruct{NamestringAddrstringCchanstringconn net.Conn server*Server}func(this*User)Online(){this.server.mapLock.Lock()this.server.OnlineMap[this.Name]=this this.server.mapLock.Unlock()//广播当前用户上线消息this.server.BroadCast(this,"已上线")}func(this*User)Offline(){this.server.mapLock.Lock()delete(this.server.OnlineMap,this.Name)this.server.mapLock.Unlock()this.server.BroadCast(this,"下线")}func(this*User)SendMessage(msgstring){this.conn.Write([]byte(msg))}func(this*User)DoMessage(msgstring){ifmsg=="who"{//定义通讯规则,如果用户输入who,则表示查询在线用户this.server.mapLock.Lock()for_,usr:=rangethis.server.OnlineMap{onlineMsg:="["+usr.Addr+"]"+usr.Name+"在线...\n"this.SendMessage(onlineMsg)}this.server.mapLock.Unlock()}elseiflen(msg)>7&&msg[:7]=="rename|"{//定义通信协议,如果用户以rename|XXX这种格式输入,则表示要修改用户名newName:=strings.Split(msg,"|")[1]_,ok:=this.server.OnlineMap[newName]ifok{this.SendMessage("当前用户名被占用\n")}else{this.server.mapLock.Lock()delete(this.server.OnlineMap,this.Name)this.server.OnlineMap[newName]=this this.server.mapLock.Unlock()this.Name=newName this.SendMessage("更新用户名成功:"+this.Name+"\n")}}else{this.server.BroadCast(this,msg)}}funcNewUser(conn net.Conn,server*Server)*User{userAddr:=conn.RemoteAddr().String()user:=&User{Name:userAddr,Addr:userAddr,C:make(chanstring),conn:conn,server:server,}//启动监听当前user channel的goroutinegouser.ListenMessage()returnuser}func(this*User)ListenMessage(){for{msg:=<-this.C this.conn.Write([]byte(msg+"\n"))}}逐行解析
这两个功能主要涉及的就是DoMessage方法中的if分支。
this.server.mapLock.Lock()因为要对OnlineMap进行遍历,而这个DoMessage方法是在协程中执行的,因此是并发环境。Go 中的map不带锁,不是Java那种ConcurrentHashMap,因此读写要上锁for _, usr := range this.server.OnlineMap这句是对map 进行遍历,range 是Go语言的关键字,它会根据你遍历的对象类型,返回不同数量和含义的值。相当于比Java的iterator 封装级别更高的指令,对于Java来说,每一种容器的遍历代码是要自己写的,但是Go 把它们都封装成了range关键词,编译器会根据具体遍历的对象,执行遍历并返回每个元素的副本。比如map 返回的是两个值,K, V。如果要忽略返回值,则需要用_下划线占位。注意for i := range slice:表示只拿索引_, ok := this.server.OnlineMap[newName]这是Go 语言中处理 Map的一种语法,通常被称为 “comma ok” 断言。ok 代表着一个bool 类型的值。在Go 中,访问map 中的Key会返回两个值,一个是Value,如果不存在是零值,一个是布尔类型,表示是否存在,存在时true。当前代码表示不关系返回的具体值,而是更关注是否存在。delete(this.server.OnlineMap, this.Name)delete() 和make()一样,是Go 语言的内置函数,作用是删除map 中的key。如果key 不存在,什么都不做,也不报错。而且这个函数没有返回值
一些问题
为什么 SendMessage 是安全的?内部执行的是 this.conn.Write()。非 Channel 阻塞:conn.Write 是直接将数据写入 TCP 缓冲区。虽然缓冲区满了也会阻塞,但它不依赖另一个 Goroutine 的配合。在并发编程中有一条铁律:持锁期间,绝对不要执行任何可能导致永久阻塞的操作(如无缓冲 Channel、远程网络请求等)。
对于onlineMsg,(只)发送在线信息给当前用户this.SendMessage(onlineMsg) ,为什么不能是this.C <- onlineMsg?
这个是我自己写代码的时候的直觉,this.C <- onlineMsg:是一种跨协程的同步行为,在持有全局锁时做这件事极其危险。可能会造成死锁。这里C是一个无缓冲channel, 即写入是阻塞的,直到读取发生。读取指的是ListenMessage,在另一个协程里跑的。那么,如果在持有 mapLock 的时候去写一个可能导致阻塞的 chan,而此时ListenMessage因为某种原因阻塞了,无法读取,那么mapLock 这个全局锁就无法释放,这个服务会卡死。什么是死锁?
两个或多个进程(协程),在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。不是只有传统的经典死锁:两个锁互相把自己锁死了,解不开,才叫死锁。学术上,死锁必须满足四个条件:
互斥:mapLock 只能被一个协程持有。
占有且等待:DoMessage 占有了锁,同时在等待 Channel 写入。
不可剥夺:锁一旦被占有,除非自己释放,别人抢不走。
循环等待:A 等 B 读 Channel,B 等网络(或 A 释放锁后产生的后续动作)。如果在 range 循环遍历 Map 的过程中执行 delete 操作,会像 Java 那样抛出 ConcurrentModificationException 吗?
不会。在 Go 语言中,在 range 循环里 delete 当前 Map 的 Key 是绝对安全的,不会抛出任何异常。Java 报异常是因为在for 循环中低层对map 的遍历有一个modCount值比较,如果值不相等,则抛异常。但是Go在range设计的时候就考虑到了这一点,Go 在开始 range 循环时,并不是对整个 Map 做了一个快照,而是通过一个专门的迭代器结构体来跟踪进度,当迭代器移动到下一个“桶”(Bucket)时,它只关心当前桶里还剩下什么。如果你刚刚删除了某个桶里的数据,迭代器只会简单地跳过它,继续寻找下一个有效数据。此时只能说明在同一个协程内部,边删边查实不会报错的,但是在并发场景下,不能一个协程删除,另一个协程查询。此时要对被并发协程访问的map上锁。
今天遇到一个主要的问题是并发模型的问题。其它的疑问不是很多。写到今天的时候已经对Go语言的简洁有所领悟了,因为发现代码已经可以上手了……这要是Java,那还差得多呢……