1.聊天室设计分析

一. 概览

实现 个网络聊天室(群)
功能分析:

  1. 上线下线
  2. 聊天,其他人,自己都可以看到聊天消息
  3. 查询当前聊天室用户名字 who
  4. 可以修改自己名字 rename | Duke
  5. 超时踢出

技术点分析:
1 . sock tcp 编程
2 . map结构 (存储当前用户,map遍历,map删除)
3 . go程,channel
4 . select(超时退出,主动退出)
5 . timer定时器

二、实现基础

第一阶段:
tcp socket,建立多个连接

package main

import (
"fmt"
"net"
)

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

for {
fmt.Println("==>主go程监听中......")
// 监听
conn,err := listener.Accept()
if err != nil{
fmt.Println("listener.Accept err:",err)
return
}

// 建立连接
fmt.Println("建立连接成功!")
// 启动处理业务的go程
go handler(conn)

}
}

// 处理具体业务
func handler(conn net.Conn){
for{
fmt.Println("启动业务...")
// TODO // 代表这里以后再具体实现
buf := make([]byte,1024)
// 读取客户端发送来的数据
cnt,err := conn.Read(buf)
if err != nil{
fmt.Println("listener.Read err:",err)
return
}
fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
}

}

go run chatroom.go

启动nc

Golang网络聊天室案例_数据


​​ nc下载地址 ​

2、定义User/map结构

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

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

在Handler中调用

// 处理具体业务
func handler(conn net.Conn){
for{
fmt.Println("启动业务...")
//
// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
clientAddr := conn.RemoteAddr().String()
fmt.Println("clientAddr:",clientAddr)
// 创建user
newUser := User{
id:clientAddr,// id 我们不会修改,这个作为map中的key
name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
msg:make(chan string), // 注意需要make空间,否则无法写入数据
}
// 添加user到map结构
allUsers[newUser.id] = newUser
/
buf := make([]byte,1024)
// 读取客户端发送来的数据
cnt,err := conn.Read(buf)
if err != nil{
fmt.Println("listener.Read err:",err)
return
}
fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
}

3.定义message管道

Golang网络聊天室案例_网络_02


创建监听广播go程函数

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
fmt.Println("广播go程启动成功...")
// 1. 从message中读取数据
info := <-message
// 2. 将数据写入到每一个用户的msg管道中
for _,user := range allUsers{
user.msg <- info
}
}

启动,全局唯一

Golang网络聊天室案例_网络_03


写入上线数据

Golang网络聊天室案例_数据_04


当前整体源码

package main

import (
"fmt"
"net"
)

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

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

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

func main(){
// 创建服务器
listener,err := net.Listen("tcp",":8087")
if err != nil{
fmt.Println("net.Listen err:",err)
return
}
// 启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

fmt.Println("服务器启动成功,监听中...")

for {
fmt.Println("==>主go程监听中......")
// 监听
conn,err := listener.Accept()
if err != nil{
fmt.Println("listener.Accept err:",err)
return
}

// 建立连接
fmt.Println("建立连接成功!")
// 启动处理业务的go程
go handler(conn)

}
}

// 处理具体业务
func handler(conn net.Conn){
for{
fmt.Println("启动业务...")
// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
clientAddr := conn.RemoteAddr().String()
fmt.Println("clientAddr:",clientAddr)
// 创建user
newUser := User{
id:clientAddr,// id 我们不会修改,这个作为map中的key
name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
}
// 添加user到map结构
allUsers[newUser.id] = newUser

// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name)
message <- loginInfo

buf := make([]byte,1024)
// 读取客户端发送来的数据
cnt,err := conn.Read(buf)
if err != nil{
fmt.Println("listener.Read err:",err)
return
}
fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
}

}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
fmt.Println("广播go程启动成功...")
defer fmt.Println("broadcast 程序退出!")

for {
// 1. 从message中读取数据
fmt.Println("broadcast监听message中...")
info := <-message
// 2. 将数据写入到每一个用户的msg管道中
for _,user := range allUsers{
// 如果msg是非缓冲,那么会在这里阻塞
user.msg <- info
}
}
}

4.user监听通道go程

每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
for data := range user.msg{
fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

// Write(b []byte)(n int,err error)
_,_ = conn.Write([]byte(data))
}
}

Golang网络聊天室案例_数据_05


当前代码整体为

package main

import (
"fmt"
"net"
)

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

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

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

func main(){
// 创建服务器
listener,err := net.Listen("tcp",":8087")
if err != nil{
fmt.Println("net.Listen err:",err)
return
}
// 启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

fmt.Println("服务器启动成功,监听中...")

for {
fmt.Println("==>主go程监听中......")
// 监听
conn,err := listener.Accept()
if err != nil{
fmt.Println("listener.Accept err:",err)
return
}

// 建立连接
fmt.Println("建立连接成功!")
// 启动处理业务的go程
go handler(conn)

}
}

// 处理具体业务
func handler(conn net.Conn){

fmt.Println("启动业务...")
// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
clientAddr := conn.RemoteAddr().String()
fmt.Println("clientAddr:",clientAddr)
// 创建user
newUser := User{
id:clientAddr,// id 我们不会修改,这个作为map中的key
name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
}
// 添加user到map结构
allUsers[newUser.id] = newUser

// 启动go程,负责将msg的信息返回给客户端
go writeBackToClient(&newUser,conn)

// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name)
message <- loginInfo

for{
// 具体业务逻辑

buf := make([]byte,1024)
// 读取客户端发送来的数据
cnt,err := conn.Read(buf)
if err != nil{
fmt.Println("listener.Read err:",err)
return
}
fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
}

}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
fmt.Println("广播go程启动成功...")
defer fmt.Println("broadcast 程序退出!")

for {
// 1. 从message中读取数据
fmt.Println("broadcast监听message中...")
info := <-message
// 2. 将数据写入到每一个用户的msg管道中
for _,user := range allUsers{
// 如果msg是非缓冲,那么会在这里阻塞
user.msg <- info
}
}
}

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
for data := range user.msg{
fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

// Write(b []byte)(n int,err error)
_,_ = conn.Write([]byte(data))
}
}

三、增加功能

  1. 查询用户
    查询命令:who==>将当前所有的登录的用户,展示出来,id,name,返回给当前用户
package main

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

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

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

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

func main(){
// 创建服务器
listener,err := net.Listen("tcp",":8087")
if err != nil{
fmt.Println("net.Listen err:",err)
return
}
// 启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

fmt.Println("服务器启动成功,监听中...")

for {
fmt.Println("==>主go程监听中......")
// 监听
conn,err := listener.Accept()
if err != nil{
fmt.Println("listener.Accept err:",err)
return
}

// 建立连接
fmt.Println("建立连接成功!")
// 启动处理业务的go程
go handler(conn)

}
}

// 处理具体业务
func handler(conn net.Conn){

fmt.Println("启动业务...")
// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
clientAddr := conn.RemoteAddr().String()
fmt.Println("clientAddr:",clientAddr)
// 创建user
newUser := User{
id:clientAddr,// id 我们不会修改,这个作为map中的key
name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
}
// 添加user到map结构
allUsers[newUser.id] = newUser

// 启动go程,负责将msg的信息返回给客户端
go writeBackToClient(&newUser,conn)

// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name)
message <- loginInfo

for{
// 具体业务逻辑

buf := make([]byte,1024)
// 读取客户端发送来的数据
cnt,err := conn.Read(buf)
if err != nil{
fmt.Println("listener.Read err:",err)
return
}
fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
// -------------业务逻辑处理 开始-------------
// 1.查询当前所有的用户 who
// a. 先判断接收的数据是不是who ==》 长度&&字符串
userInput := string(buf[:cnt-1]) // 这是用户输入的数据,最后一个是回车,我们去掉他
if len(userInput) == 3 && userInput == "who"{
// b.遍历allUser这个map(key:userid value:user 本身)。将id和name拼接成一个字符,返回给客户端
fmt.Println("用户即将查询所有用户信息!")
// 这个切片包含所有的用户信息
var userInfos []string
for _,user := range allUsers{
userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name)
userInfos = append(userInfos,userInfo)
}

// 最终写入到通道中,一定是一个字符串
r := strings.Join(userInfos,"\n")

// 将数据返回到客户端
newUser.msg <- r
}else{
// 如果不是用户输入的命令,只是聊天信息,那么只需要写到广播中即可,由其他的go程常规转发
message <- userInput
}

// -------------业务逻辑处理 结束-------------

}

}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
fmt.Println("广播go程启动成功...")
defer fmt.Println("broadcast 程序退出!")

for {
// 1. 从message中读取数据
fmt.Println("broadcast监听message中...")
info := <-message
// 2. 将数据写入到每一个用户的msg管道中
for _,user := range allUsers{
// 如果msg是非缓冲,那么会在这里阻塞
user.msg <- info
}
}
}

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
for data := range user.msg{
fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

// Write(b []byte)(n int,err error)
_,_ = conn.Write([]byte(data))
}
}
  1. 重命名
    规则:rename|Duke
    获取数据判断长度7,判断字符是rename
    使用|进行分割,获取|后面的部分,作为名字
    更新用户名字newUser.name = Duke
    通知客户端,更新成功
  2. 主动退出
    每个用户都有自己的watch go程,仅负责监听退出信号
// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理
func watch(user *User,conn net.Conn,isQuit <-chan bool){
fmt.Println("启动监听信号退出的go程...")
defer fmt.Println("watch go程退出!")
for{
select{
case <-isQuit:
logoutInfo := fmt.Sprintf("%s exit already!",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message<-logoutInfo
conn.Close()
return
}
}
}
在handler中启动go watch,同时传入相应信息:
// 定义一个退出信号,用来监听client退出
var isQuit = make(chan bool)

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

在read之后,通过cnt判断用户退出,向isQuit写入信号:

Golang网络聊天室案例_网络_06

测试截图

Golang网络聊天室案例_网络_07

  1. 超时退出
    使用定时器来进行超时管理
    如果60s没有发送任何数据,那么直接将这个链接关闭
<-time.After(60*time.second)

更新watch函数

// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理
func watch(user *User,conn net.Conn,isQuit,restTimer <-chan bool){
fmt.Println("启动监听信号退出的go程...")
defer fmt.Println("watch go程退出!")
for{
select{
case <-isQuit:
logoutInfo := fmt.Sprintf("%s exit already!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message<-logoutInfo

conn.Close()
return
case <-time.After(10*time.Second):
logoutInfo := fmt.Sprintf("%s timeout exit elready!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message<-logoutInfo

conn.Close()
return
case <-restTimer:
fmt.Printf("连接%s 重置计数器!\n",user.name)
}
}
}

创建并传入restTimer管道

Golang网络聊天室案例_客户端_08

Golang网络聊天室案例_开发语言_09


效果:

Golang网络聊天室案例_golang_10


最终代码

package main

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

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

}

// 创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

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

func main(){
// 创建服务器
listener,err := net.Listen("tcp",":8087")
if err != nil{
fmt.Println("net.Listen err:",err)
return
}
// 启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

fmt.Println("服务器启动成功,监听中...")

for {
fmt.Println("==>主go程监听中......")
// 监听
conn,err := listener.Accept()
if err != nil{
fmt.Println("listener.Accept err:",err)
return
}

// 建立连接
fmt.Println("建立连接成功!")
// 启动处理业务的go程
go handler(conn)

}
}

// 处理具体业务
func handler(conn net.Conn){

fmt.Println("启动业务...")
// 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id
clientAddr := conn.RemoteAddr().String()
fmt.Println("clientAddr:",clientAddr)
// 创建user
newUser := User{
id:clientAddr,// id 我们不会修改,这个作为map中的key
name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同
msg:make(chan string,10), // 注意需要make空间,否则无法写入数据
}
// 添加user到map结构
allUsers[newUser.id] = newUser


// 定义一个退出信号,用来监听client退出
var isQuit = make(chan bool)
// 创建一个用于重置计数器的管道,用于告知watch函数,当前用户正在输入
var restTimer = make(chan bool)
// 启动go程,负责监听退出信号
go watch(&newUser,conn,isQuit,restTimer)

// 启动go程,负责将msg的信息返回给客户端
go writeBackToClient(&newUser,conn)

// 向message写入数据,当我用户上线的消息,用于通知所有人(广播)
loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!\n",newUser.id,newUser.name)
message <- loginInfo

for{
// 具体业务逻辑
buf := make([]byte,1024)

// 读取客户端发送来的数据
cnt,err := conn.Read(buf)
if cnt == 0 {
fmt.Println("客户端主动关闭ctrl+c,准备退出!")
// map 删除,用户 conn close掉
// 服务器还可以主动的退出
// 在这里不进行真正的退出动作,而是发出一个退出信号,统一做退出处理,可以使用新的管道来做信号传递
isQuit <- true
}

if err != nil{
fmt.Println("listener.Read err:",err)
return
}
fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt)
// -------------业务逻辑处理 开始-------------
// 1.查询当前所有的用户 who
// a. 先判断接收的数据是不是who ==》 长度&&字符串
userInput := string(buf[:cnt-1]) // 这是用户输入的数据,最后一个是回车,我们去掉他
if len(userInput) == 3 && userInput == "who"{
// b.遍历allUser这个map(key:userid value:user 本身)。将id和name拼接成一个字符,返回给客户端
fmt.Println("用户即将查询所有用户信息!")
// 这个切片包含所有的用户信息
var userInfos []string
for _,user := range allUsers{
userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name)
userInfos = append(userInfos,userInfo)
}

// 最终写入到通道中,一定是一个字符串
r := strings.Join(userInfos,"\n")

// 将数据返回到客户端
newUser.msg <- r
}else if len(userInput) > 8 && userInput[:6] == "rename"{

// 规则:rename|Duke
// 获取数据判断长度7,判断字符是rename
// 使用|进行分割,获取|后面的部分,作为名字
// 更新用户名字newUser.name = Duke
newUser.name = strings.Split(userInput,"|")[1]
allUsers[newUser.id] = newUser // 更新map中的user
// 通知客户端,更新成功
message <- userInput
}else{
// 如果不是用户输入的命令,只是聊天信息,那么只需要写到广播中即可,由其他的go程常规转发
message <- userInput
}
restTimer <- true
// -------------业务逻辑处理 结束-------------

}

}

// 向所有的用户广播消息,启动一个全局唯一的go程
func broadcast(){
fmt.Println("广播go程启动成功...")
defer fmt.Println("broadcast 程序退出!")

for {
// 1. 从message中读取数据
fmt.Println("broadcast监听message中...")
info := <-message
// 2. 将数据写入到每一个用户的msg管道中
for _,user := range allUsers{
// 如果msg是非缓冲,那么会在这里阻塞
user.msg <- info
}
}
}

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User,conn net.Conn){
fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name)
for data := range user.msg{
fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data)

// Write(b []byte)(n int,err error)
_,_ = conn.Write([]byte(data))
}
}

// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理
func watch(user *User,conn net.Conn,isQuit,restTimer <-chan bool){
fmt.Println("启动监听信号退出的go程...")
defer fmt.Println("watch go程退出!")
for{
select{
case <-isQuit:
logoutInfo := fmt.Sprintf("%s exit already!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message<-logoutInfo

conn.Close()
return
case <-time.After(10*time.Second):
logoutInfo := fmt.Sprintf("%s timeout exit elready!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message<-logoutInfo

conn.Close()
return
case <-restTimer:
fmt.Printf("连接%s 重置计数器!\n",user.name)
}
}
}

这里还有问题就是,上锁问题。记得在操作map的时候加上读锁和写锁
案例

package main

import(
"fmt"
"sync"
"time"
)

var idnames = make(map[int]string)
var lock sync.RwMutex

// map不允许同事读写,如果有不同go程同时操作map,需要对map上锁

func main(){
go func(){
for{
lock.lock()
idnames[0] = "duke"
lock.Unlock()
}
}()

go func(){
for{
lock.Lock()
name := idnames[0]
fmt.Println("name:",name)
lock.Unlock()
}
}()

for{
fmt.Println("OVER")
time.Sleep(1*time.Second)
}
}

感谢大家观看,我们下次见