并发编程和网络编程是现今行业开发中常用的技术。Go语言强大的语法设定使得并发和网络编程都变的简洁而高效。
下面使用并发和网络实现一个简单的网络在线聊天室。在整个聊天室的项目中,充分利用了协程并发,处理不同任务。
模块划分
整个聊天室程序可简单划分为如下模块,都分别使用协程来实现:
-
主协程(服务器):
负责监听、接收用户(客户端)连接请求,建立通信关系。同时启动相应的协程处理任务。
-
处理用户连接协程:HandleConnect:
负责新上线用户的存储,用户消息读取、发送,用户改名、下线处理及超时处理。为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。
-
用户消息广播协程:Manager:
负责在线用户遍历,用户消息广播发送。需要与HandleConnect协程及用户子协程协作完成。
-
协程间应用数据及通信:
- map:存储所有登录聊天室的用户信息
- key:用户的ip+port。
- value:Client结构体。
- Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
- 通道message:协调并发协程间消息的传递。
- map:存储所有登录聊天室的用户信息
广播用户上线
首先,服务器启动,等待用户建立通信连接。
当有用户连接上来,将其存储到map
中,这样就维护了一个“在线用户”的列表。当再有新用户连接上来时,应向该列表中所有用户进行广播通知,提示新用户上线。
简单实现手法可以循环读取列表中的用户,依次向其发送消息通知新用户上线。但这种方式无疑是一种串行的通信手段,实现简单,但执行效率较低。
在go语言中,我们利用协程轻便、高效、并发性好的特性,给每个登录用户维护多个协程来进行数据通信,借助channel不需要使用同步锁,就可以实现高效的并发通信。
初始化配置
全局位置定义用户结构体类型 Client
,存储登录用户信息。成员包含channel
、Name
、Addr
type Client struct {
C chan string
Name string
Addr string
}
定义全局通道message
处理消息
// 创建全局 channel 传递用户消息。
var message = make(chan string)
定义全局map
存储在线用户信息。
Key
为用户网络地址。Value
为用户结构体。
// 创建全局map,存储在线用户
var onlineMap map[string]Client
handleConnet协程
获取用户网络地址(Ip+port),创建新用户结构体,包含成员C、Name、Addr。新用户的Name和Addr初值都是用户网络地址(Ip+port)。将用户结构体存入map中。并创建WriteMsgToClient协程,专门负责给当前用户发送消息。组织新用户上线广播消息内容,写入全局通道message中。
- WriteMsgToClient协程:读取用户结构体C中的数据,没有则阻塞等待,有数据写给登录用户。
func WriteMsgToClient(client Client, conn net.Conn) {
// 监听 用户自带Channel 上是否有消息,有消息则读走,Write 给客户端
for msg := range client.C {
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(client Client, msg string) (buf string) {
// 输出用户发送的消息
buf = "[" + client.Addr + "]" + client.Name + ": " + msg
return
}
func HandleConnect(conn net.Conn) {
defer conn.Close()
// 获取新连接上来的用户的网络地址(IP+port)
netAddr := conn.RemoteAddr().String()
// 给新用户创建结构体。用户名、网络地址一样
client := Client{
make(chan string), netAddr, netAddr}
// 将新创建的结构体,添加到 map 中,key值为获取到的网络地址(IP+port)
onlineMap[netAddr] = client
// 新创建一个协程,专门给当前客户端发送消息。
go WriteMsgToClient(client, conn)
// 广播新用户上线
// MakeMsg 格式化输出数据给 message
message <- MakeMsg(client, "login")
for {
// 不能让当前协程结束。
;
}
}
Manager协程
给map分配空间。循环读取 message 通道中是否有数据。没有,阻塞等待。有则解除阻塞,将message通道中读到的数据写到用户结构体中的C通道。
func Manager() {
// 给map分配空间
onlineMap = make(map[string]Client)
// 循环读取 message 通道中的数据
for {
// 通道 message 中有数据读到 msg 中。 没有,则阻塞
msg := <-message
// 一旦执行到这里,说明message中有数据了,解除阻塞。 遍历 map
for _, client := range onlineMap {
client.C <- msg // 把从Message通道中读到的数据,写到 client 的 C 通道中。
}
}
}
主协程
func main() {
// 创建监听 socket
listener, err := net.Listen("tcp", "127.0.0.1: 8000")
if err != nil {
fmt.Println("Listen err:", err)
return
}
defer listener.Close()
// 创建协程 处理消息
go Manager()
// 循环接收客户端连接请求
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept err:", err)
continue // 失败,监听其他客户端连接
}
defer conn.Close()
// 给新连接的客户端,单独创建一个协程,处理客户端连接请求
go HandleConnect(conn)
}
}
广播用户消息
当某个客户端向服务端发送消息后,服务端应将该消息广播给其它的客户端,达到聊天室的群聊效果。为此,需要开启一个新的协程,为方便传参,可以选择匿名协程。专门负责接收从客户端传递过来的数据,然后将接收到的数据写到messaage通道中。
完成“广播用户消息”给所有在线用户的功能,只需要将读到的数据写到message通道即可达到目的,再通过 Manager 协程广播消息。
func