(十四)Go聊天室实践

并发编程和网络编程是现今行业开发中常用的技术。Go语言强大的语法设定使得并发网络编程都变的简洁而高效。

下面使用并发和网络实现一个简单的网络在线聊天室。在整个聊天室的项目中,充分利用了协程并发,处理不同任务。

模块划分

整个聊天室程序可简单划分为如下模块,都分别使用协程来实现:

  • 主协程(服务器)

    负责监听、接收用户(客户端)连接请求,建立通信关系。同时启动相应的协程处理任务。

  • 处理用户连接协程:HandleConnect

    负责新上线用户的存储,用户消息读取、发送,用户改名、下线处理及超时处理。为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务。

  • 用户消息广播协程:Manager

    负责在线用户遍历,用户消息广播发送。需要与HandleConnect协程及用户子协程协作完成。

  • 协程间应用数据及通信

    • map:存储所有登录聊天室的用户信息
      • key:用户的ip+port。
      • value:Client结构体。
    • Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
    • 通道message:协调并发协程间消息的传递。

广播用户上线

首先,服务器启动,等待用户建立通信连接。

当有用户连接上来,将其存储到map中,这样就维护了一个“在线用户”的列表。当再有新用户连接上来时,应向该列表中所有用户进行广播通知,提示新用户上线。

简单实现手法可以循环读取列表中的用户,依次向其发送消息通知新用户上线。但这种方式无疑是一种串行的通信手段,实现简单,但执行效率较低。

在go语言中,我们利用协程轻便、高效、并发性好的特性,给每个登录用户维护多个协程来进行数据通信,借助channel不需要使用同步锁,就可以实现高效的并发通信。

初始化配置

全局位置定义用户结构体类型 Client,存储登录用户信息。成员包含channelNameAddr

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值