9-Go综合案例:并发聊天室

一、模块简述

  • (1)主go程:负责监听、接收用户(客户端)连接请求,建立通信关系;同时启动相应的协程处理任务
  • (2)处理用户连接协程HandleConnect:负责新上线用户的存储,用户消息读取、发送,用户改名、下线处理及超时处理;为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务
  • (3)用户消息广播协程Manager:负责在线用户遍历,用户消息广播发送,需要与HandleConnect协程及用户子协程协作完成
  • (4)协程间应用数据及通信
    • map:存储所有登录聊天室的用户信息, key:用户的ip+port。Value:Client结构体
    • Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
    • 通道message:协调并发协程间消息的传递
  • (5)广播用户上线
    • 首先,服务器启动,等待用户建立通信连接。当有用户连接上来,将其存储到map中,这样就维护了一个“在线用户”的列表。当再有新用户连接上来时,应向该列表中所有用户进行广播通知,提示xxx用户上线
    • 在go语言中,我们利用协程轻便、高效、并发性好的特性,给每个登录用户维护多个协程来进行数据通信,借助channel不需要使用同步锁,就可以实现高效的并发通信
      在这里插入图片描述

二、流程分析

  • 用户结构体:全局位置定义用户结构体类型 Client,存储登录用户信息。成员包含channel、Name、Addr
	type Client struct {
		C chan string
		Name string
		Addr string
}
  • 定义全局通道message:处理消息
  • 定义全局map:存储在线用户信息。Key为用户网络地址。Value为用户结构体
  • 主协程:监听客户端连接请求,当有新的客户端连接,创建新协程handleConnet处理用户连接
  • handleConnet协程:获取用户网络地址(Ip+port),创建新用户结构体,包含成员C、Name、Addr。新用户的Name和Addr初值都是用户网络地址(Ip+port)。将用户结构体存入map中。并创建WriteMsgToClient协程,专门负责给当前用户发送消息。组织新用户上线广播消息内容,写入全局通道message中
  • WriteMsgToClient协程:读取用户结构体C中的数据,没有则阻塞等待,有数据写出给登录用户
  • Manager协程:给map分配空间。循环读取 message 通道中是否有数据。没有,阻塞等待。有则解除阻塞,将message通道中读到的数据写到用户结构体中的C通道

三、源码实现

package main

import (
	"fmt"
	"net"
	"strings"
	"time"
)

// Client 创建用户结构体类型
type Client struct {
	C    chan string
	Name string
	Addr string
}

// 创建全局map,存储在线用户
var onlineMap map[string]Client

// 创建全局 channel 传递用户消息
var message = make(chan string)

func WriteMsgToClient(clnt Client, conn net.Conn) {
	// 监听 用户自带Channel上是否有消息
	for msg := range clnt.C {
		conn.Write([]byte(msg + "\n"))
	}
}

func MakeMsg(clnt Client, msg string) (buf string) {
	buf = "[" + clnt.Addr + "]" + clnt.Name + ": " + msg
	return
}

func HandlerConnect(conn net.Conn) {
	defer conn.Close()
	// 创建channel判断,用户是否活跃
	hasData := make(chan bool)

	// 获取用户 网络地址 IP+port
	netAddr := conn.RemoteAddr().String()
	// 创建新连接用户的结构体,默认用户是 IP+port
	clnt := Client{make(chan string), netAddr, netAddr}

	// 将新连接用户,添加到在线用户map中。key: IP+port value:client
	onlineMap[netAddr] = clnt

	// 创建专门用来给当前用户发送消息的go程
	go WriteMsgToClient(clnt, conn)

	// 发送用户上线消息到全局channel 中
	//message <- "[" + netAddr + "]" + clnt.Name + "login"
	message <- MakeMsg(clnt, "login")

	// 创建一个 channel,用来判断用退出状态
	isQuit := make(chan bool)

	// 创建一个匿名go程, 专门处理用户发送的消息
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := conn.Read(buf)
			if n == 0 {
				isQuit <- true
				fmt.Printf("检测到客户端:%s退出\n", clnt.Name)
				return
			}
			if err != nil {
				fmt.Println("conn.Read err:", err)
				return
			}
			// 将读到的用户消息,保存到msg中,string类型
			msg := string(buf[:n-1])

			// 提取在线用户列表
			if msg == "who" && len(msg) == 3 {
				conn.Write([]byte("online user list:\n"))
				// 遍历当前map ,获取在线用户
				for _, user := range onlineMap {
					userInfo := user.Addr + ":" + user.Name + "\n"
					conn.Write([]byte(userInfo))
				}
				// 判断用户发送了改名命令
			} else if len(msg) >= 8 && msg[:6] == "rename" { // rename|
				newName := strings.Split(msg, "|")[1] // msg[8:]
				clnt.Name = newName                   // 修改结构体成员name
				onlineMap[netAddr] = clnt             // 更新 onlineMap
				conn.Write([]byte("rename successful\n"))
			} else {
				// 将读到的用户消息,写入到message中。
				message <- MakeMsg(clnt, msg)
			}
			hasData <- true
		}
	}()

	// 保证 不退出
	for {
		// 监听 channel 上的数据流动
		select {
		case <-isQuit:
			delete(onlineMap, clnt.Addr)       // 将用户从 online移除
			message <- MakeMsg(clnt, "logout") // 写入用户退出消息到全局channel
			return
		case <-hasData:
			// 什么都不做。 目的是重置 下面 case 的计时器。
		case <-time.After(time.Second * 60):
			delete(onlineMap, clnt.Addr)                // 将用户从 online移除
			message <- MakeMsg(clnt, "time out leaved") // 写入用户退出消息到全局channel
			return
		}
	}
}

func Manager() {
	// 初始化 onlineMap
	onlineMap = make(map[string]Client)

	// 监听全局channel中是否有数据, 有数据存储至msg, 无数据阻塞
	for {
		msg := <-message
		// 循环发送消息给所有在线用户。要想执行,必须msg := <-message 执行完,解除阻塞
		for _, clnt := range onlineMap {
			clnt.C <- msg
		}
	}
}

func main() {
	// 创建监听套接字
	listener, err := net.Listen("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Println("Listen err", err)
		return
	}
	defer listener.Close()

	// 创建管理者go程,管理map 和全局channel
	go Manager()

	// 循环监听客户端连接请求
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Accept err", err)
			return
		}
		// 启动go程处理客户端数据请求
		go HandlerConnect(conn)
	}
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无休止符

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值