Golang实现群聊天室(200行代码)

前言 

使用Go语言开发一个网络群聊天室

部署在Linux环境 CentOS系统

类似于QQ群的效果

功能:

用户上线时通知所有人

用户下线时通知所有人

用户发送消息 所有在线用户会接收到

用户挂机超时会被自动下线

用户可以查询当前所有在线的用户 \who命令

用户可以更改自己的昵称 \rename|new_name命令

核心思路

一、启动主GO程 用于接收各个客户端发来的连接请求

二、对于每一个成功连接的客户端 启动一个子GO程进行业务处理

        1.通过MAP数据结构管理所有在线的用户信息

        2.每一个连接(用户)都有一个私有管道msg用于接收数据

        3.由一个全局管道message和配套的全局广播函数实现群发功能

细节处理 

一、如何使所有在线的用户(客户端)接收到消息?       

        先在主GO程中启动全局广播函数:

                创建一个全局管道变量message

                全局广播函数遍历MAP数据结构 确定所有在线用户

                将message管道中的信息依次发送给每一个用户的私有msg管道

        每一个用户接收群发消息:

                在处理每一个连接的子GO程中

                再启动一个Go程负责监听自己的msg私有管道

                并将写入msg管道的数据输出至连接对应的客户端

二、如何实现用户下线功能和超时退出?

        在每一个连接的子GO程中

        再启动一个GO程负责检查退出状态

        使用select语句 检测主动退出

        使用计时器实现超时退出

        且在有新的消息产生时 重置计时器

三、对MAP的操作应当使用读写锁

完整代码

package main

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

type User struct {
	//姓名
	name string
	//唯一的ID
	id string
	//管道
	msg chan string
}

//全局的MAP结构 存储所有用户的信息
//MAP不能同时读写 如果有不同的GO程同时操作MAP 需要对MAP上锁
var lock sync.RWMutex
var allUsers = make(map[string]User)

//定义一个message全局通道 用于接收任何人发来的消息
var message = make(chan string, 64)

func handler(conn net.Conn) {
	//服务端提取客户端连接的时候
	//会获取客户端的IP和PORT
	clientAddr := conn.RemoteAddr().String()

	//为本次连接的客户端创建用户
	newUser := User{
		id:   clientAddr,            //ID作为MAP中的KEY值
		name: clientAddr,            //用户可以自定义 默认初始值与ID相同
		msg:  make(chan string, 64), //有缓冲的管道 非阻塞
	}

	//将User变量添加入allUsers
	lock.Lock()
	allUsers[newUser.id] = newUser
	lock.Unlock()

	//定义一个退出信号 用于监听对应client是否退出
	var isQuit = make(chan bool)

	//创建一个重置计数器的管道
	//告知watchExit函数 当前用户仍在输入
	var resetTimer = make(chan bool)

	//启动GO程负责监听退出的信号
	go watchExit(&newUser, conn, isQuit, resetTimer)

	//向message写入数据 通知所有人 当前用户已经上线
	loginInfo := fmt.Sprintf("[%s] is online\n", newUser.name)
	message <- loginInfo

	//每个连接启动一个GO程从服务端写回客户端
	go writeBackToClient(&newUser, conn)

	//业务逻辑 多次读取客户端发送的消息
	for {
		buf := make([]byte, 1024)

		cnt, err := conn.Read(buf)

		if cnt == 0 {
			//发送一个退出信号 由相关GO程负责处理
			isQuit <- true
			return
		}

		if err != nil {
			fmt.Println("conn.Read err:", err)
			return
		}

		userInput := string(buf[:cnt-1]) //去掉最后一个回车
		if len(userInput) == 4 && string(userInput) == "\\who" {
			//用户输入的是who命令
			//查询当前所有在线的用户
			fmt.Println("Checking all the users online...")

			//创建切片存储所有在线用户的name
			var userInfos []string

			lock.RLock()
			for _, user := range allUsers {
				userInfo := fmt.Sprintf(user.name)
				userInfos = append(userInfos, userInfo)
			}
			lock.RUnlock()
			resWho := strings.Join(userInfos, "\n")
			resWho += "\n"
			newUser.msg <- resWho

		} else if len(userInput) > 9 && userInput[:7] == "\\rename" {
			//用户输入的是rename命令
			//修改自己的用户名
			newUser.name = strings.Split(userInput, "|")[1]
			lock.Lock()
			allUsers[newUser.id] = newUser
			lock.Unlock()
			newUser.msg <- "rename successfully\n"
		} else {
			//用户输入的不是命令 是普通消息 则进行转发即可
			fmt.Println("receive data from", newUser.name, userInput)
			bufName := fmt.Sprintf("[%s] : %s\n", newUser.name, userInput)
			message <- bufName
		}
        //仍有来自客户端的输入
        //发送定时器重置信号 不能超时退出
		resetTimer <- true
	}
}

func writeBackToClient(user *User, conn net.Conn) {
	for data := range user.msg {
		_, _ = conn.Write([]byte(data))
	}
}

//向所有的用户广播消息 全局唯一的GO程
func broadcast() {
	for {
		//从message中读取数据
		info := <-message

		//将数据写入到每一个用户的msg管道中
		lock.RLock()
		for _, user := range allUsers {
			//有缓冲的msg不会阻塞
			user.msg <- info
		}
		lock.RUnlock()
	}
}

//启动一个GO程负责监听退出信号
//触发后进行清理工作 delete map close con
func watchExit(user *User, conn net.Conn, isQuit <-chan bool, resetTimer <-chan bool) {
	for {
		select {
		case <-isQuit:
			lock.RLock()
			logoutInfo := fmt.Sprintf("[%s] is offline\n", allUsers[user.id].name)
			fmt.Println("user proactive exit:", allUsers[user.id].name)
			delete(allUsers, user.id)
			conn.Close()
			message <- logoutInfo
			lock.RUnlock()
			return
		case <-time.After(60 * time.Second): //60秒没有输入就超时退出
			lock.RLock()
			logoutInfo := fmt.Sprintf("[%s] is offline\n", allUsers[user.id].name)
			fmt.Println("user timeout exit:", allUsers[user.id].name)
			delete(allUsers, user.id)
			conn.Close()
			message <- logoutInfo
			lock.RUnlock()
			return
		case <-resetTimer:
		}
	}
}

func main() {
	//创建服务器 监听
	listener, err := net.Listen("tcp", ":666")
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}

	//启动全局广播GO程
	go broadcast()

	for {
		fmt.Println("Waiting for a new client...")
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listen.Accept err:", err)
			return
		}
		fmt.Println("Accept success!")

		// 启动GO程处理业务
		go handler(conn)
	}
}

效果截图展示

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

没伞的男孩

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

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

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

打赏作者

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

抵扣说明:

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

余额充值