Go - 项目 - 网络并发聊天室

代码实现的思路 : 


Go - 项目 - 网络并发聊天室_第1张图片
分析草图

模块划分:

        主go程:

                监听客户端连接请求。创建 go程 处理客户端事件。创建 Manager go程(管理 消息,广播给在线用户)。

        HandleConnect go程:

                处理用户事件:广播用户上线、发送消息、查询在线用户、改名、退出、超时踢下线。

                将用户添加 在线用户列表    onLineMap[IP+port]Client Client{Name, Addr, channel}

        Manager 管理者go程:

                监听 全局 channel —— Message 上是否有数据。(读) ,没有——阻塞

                读到数据,解除阻塞,遍历在线用户列表。将 Message 中读到的数据,写给每个用户的 channel 上。

        写消息给用户 go程:WriteMsgToClient()

                循环 监听用户自带 的 channel上是否有数据。 一旦读到,写给客户端

        其他全局数据:

                全局 channel —— Message : 所有用户需要广播的消息,都写入该 channel

                全局 Map —— 在线用户列表  onLineMap[IP+port]Client

                结构体类型 —— Client{Name, Addr, channel}

广播用户上线:

                1. 创建 监听socket  —— listener

                2. 循环 Accept() —— conn, 创建  handlerConnect go程。处理客户端事件

                3. 在 循环 Accept 之前,创建 Manager go程。

                                1) 创建 结构体类型 Client{Name, Addr, channel}

                                2) 创建 全局 map —— onLineMap

                                3) 创建 全局 channel —— Message

                                4) 实现 Manager go程。

                                5) 循环 监听 Message 上是否有数据 (读)无数据——阻塞、有数据——继续

                                6) 将读到的数据写给 每个用户自带 channel

                4. 实现 handlerConnect

                                1)defer conn.Close()

                                2)  获取客户端地址结构 conn.RemoteAddr() ——> clitAddr

                                3)  组织 客户端结构体 {Name, Addr, channel} 初始化。 Name == IP+port == clitAddr

                                4)将新用户添加到全局 onLineMap 中

                                5)创建 writeMsgToClient(client, conn) go程,读用户自带channel ,写给用户。并实现

                                                1>  for 循环 从 用户自带channel读, 无数据——阻塞、有数据——继续

                                                2>  conn.Write()写给客户端。

                                6)组织用户上线消息,写入到全局 Message 中 。 —— 广播。

                                7)在 handlerConnect 的结尾处,添加 死循环,防止 该go程提前退出。

全局map 添加读写锁保护:

                1. 全局位置创建读写锁  var  rwMutex sync.RWMutex

                2. 对全局 map 读操作前后,加读锁。rwMutex.RLock()/rwMutex.RUnLock() —— Manager go程循环中

                3. 对全局 map 写操作前后,加写锁。rwMutex.Lock()/rwMutex.UnLock() —— handlerConnect 中

                4. 将 onLineMap 初始化放在全局位置完成。

广播用户聊天信息:

                1. 封装 MakeMsg 函数。组织 “用户上线”、“用户聊天”信息。 MakeMsg(clit Client, str string) string

                2. 在 handlerConnect go程 中广播用户上线,之后 创建 匿名go程。

                3. for 循环读取 用户发送的聊天信息。判断 == 0 , err

                4. 将读到的消息 使用MakeMsg 函数,组织。

                5. 写入全局 channel  —— 广播。

展示在线用户列表:

                1. 匿名子go程中,将读到的数据最后一个字节的‘\n’去除。

                2. 判断 if == “who”,遍历在线用户列表 onLineMap。

                3. 组织 在线用户信息 ,使用 MakeMsg 函数。

                4. 将组织好的每一个在线用户信息,写到当前客户端 conn.Write 。 —— 不广播!!!

                5. 遍历之初,加读锁。遍历结束解锁。

修改用户名:

                1. 匿名子go程中,判断 if ==“rename|”&& len(msg) > 7  用户要进行改名操作

                2. 使用 Split() 按“|”拆分, 将新用户名提取保存

                3. 修改用户名,写入到 全局map。

                4. 写之前,加写锁。写结束解锁。

                5. 提示当前用户, 改名成功。 —— 不广播!!!

用户退出:

                1. 在 匿名子go程 之前, 创建判断用户是否下线的 channel  isQuit := make(chan bool)

                2. 匿名子go程 中 conn.Read() ----> n == 0 时, isQuit <- true 。 return 当前匿名子go程

                3. 在 handlerConnect go程 结束位置,添加 for select ( 替换掉 for 死循环。)

                4. case <-isQuit:  读满足

                                1)  关闭 WriteMsgToClient go程。 close(clit.C)  ——> 促使 WriteMsgToClient 中的 range 循环,结束。从而结束go程

                                2)delete函数,将当前用户,从在线用户列表中移除。 使用 读写锁 rwMutex 保护。

                                3)广播用户下线。 MakeMsg 函数。写入全局 channel 。

                                4)return 将 handlerConnect go程结束。

超时强踢:

                1. 给 select 添加分支:case <-time.After(倒计时时间):

                2. case <-time.After:读满足。 后续操作与 “用户退出” 操作一致。

                3.  在 匿名子go程 之前, 创建判断用户是否活跃的 channel  isLife := make(chan bool)

                4. 给 select 添加分支:case <-isLife :, 该分支存在目的,是重置计时器 time.After,没有代码执行

5. 在 匿名go程的 for 循环结束位置,添加 isLife <- true。 用户做“改名”、“查询在线列表”、“发聊天消息”任意一个操作,都会  isLife <- true。

                    能将计时器重置。


代码 :


Go - 项目 - 网络并发聊天室_第2张图片
Go - 项目 - 网络并发聊天室_第3张图片
Go - 项目 - 网络并发聊天室_第4张图片


Go - 项目 - 网络并发聊天室_第5张图片
Go - 项目 - 网络并发聊天室_第6张图片

复制 代码 到 IDE :    CTRL + ALT + L 对齐后查看


package main

import (

"net"

"fmt"

"sync"

"strings"

"time"

)

// ----- 创建读写锁

var rwMutex sync.RWMutex// 有空间吗??有!!!可以直接使用

// 创建用户结构体类型

type Clientstruct {

Name string

Addr string

Cchan string

}

// 创建 全局channel —— Message

var Message = make(chan string)

// 创建全局  map —— onLineMap  开辟空间。

var onLineMap = make(map[string]Client)

// 用于处理用户事件的 go程

func handleConnect(conn net.Conn)  {

defer conn.Close()

// 获取客户端的地址结构, 转换为 string 类型

  clitAddr := conn.RemoteAddr().String()

// 组织用户结构体信息。 初始用户名 == 用户地址IP+Port

  clit := Client{clitAddr, clitAddr, make(chan string)}

// 将当前用户添加到 在线用户列表。

  rwMutex.Lock()// 加 写锁

  onLineMap[clitAddr] = clit// 对全局map的写

  rwMutex.Unlock()// 解锁

  // 创建 go程,从 用户再带 chnnel 中读数据,写给客户端

  go WriteMsgToClient(clit, conn)

// 组织用户上线的广播消息

  //msg := "[" + clitAddr + "]" + clit.Name + ":" + "login"

  msg := MakeMsg(clit,"login")

// 写入全局 channel —— 广播

  Message<-msg

// 创建判断用户是否下线的channel

  isQuit := make(chan bool)

// 创建判断用户是否活跃的channel

  isLife := make(chan bool)

// 创建 匿名go程,循环 读取用户输入的消息,广播给所有在线用户

  go func() {

buf := make([]byte,4096)

for {

n, err := conn.Read(buf)

if n ==0 {

fmt.Println("客户端下线")

isQuit <-true

            return

        }

if err != nil {

panic(err)

}

fmt.Println("---测试读到内容:", buf[:n])

// 提取用户输入信息

        msg := string(buf[:n-1])

// 判断是 否 是 查询用户在线列表

        if msg =="who" {

rwMutex.RLock()

// 遍历全局 map ,获取在线用户

            for _, client:=range onLineMap {

// 组织用户在线显示消息

              //[127.0.0.1:8800]:127.0.0.1:8800:[OnLine]

              onlineMsg := MakeMsg(client,"[OnLine]")

// 写给自己

              conn.Write([]byte(onlineMsg +"\n"))

}

rwMutex.RUnlock()

// 判断是 否 是 改名操作

        }else if len(msg) >7 && msg[:7] =="rename|" {

//} else if msg[:7] == "rename|" && len(msg) > 7{

            // 拆分字符串,提取用户的新用户名

            newName := strings.Split(msg,"|")[1]

clit.Name = newName

rwMutex.Lock()

// 将 新用户名添加到全局 在线用户列表

            onLineMap[clit.Addr] = clit

rwMutex.Unlock()

// 提示当前用户改名成功

            conn.Write([]byte("rename successful!!!\n"))

}else {

// 将读到的消息,写入全局 channel —— 广播给所有在线用户。

            msg := MakeMsg(clit, msg)

Message<-msg// —— 广播

        }

// 向 isLife 的channel 写数据, 代表当前用户处于活跃状态。

        isLife <-true

      }

}()

/* // 添加一个 循环,防止 当前 go程提前结束for {

runtime.GC()

}*/

  for {

select {

case <-isLife:

// 无需添加代码,存在目的,是重置计时器time.After

        case <-isQuit:

// 关闭 WriteMsgToClient go程

            close(clit.C)

// 将用户从在线用户列表删除

            rwMutex.Lock()

delete(onLineMap, clit.Addr)

rwMutex.Unlock()

// 广播给所有用户。

            msg := MakeMsg(clit,"logout")

// 写入全局 channel —— 广播

            Message <- msg

return // 结束当前的 handlerConnect() —— break 不行!只能跳出 一个 case分支。不能跳出for

        case <-time.After(time.Second *15):

close(clit.C)

rwMutex.Lock()

delete(onLineMap, clit.Addr)

rwMutex.Unlock()

Message <- MakeMsg(clit,"time out to Leave")

return

      }

}

}

// 封装组织消息的函数

func MakeMsg(clit Client, str string) string {

msg :="[" + clit.Addr +"]" + clit.Name +":" + str

return msg

}

// 从用户自带channel中读取数据,写回给 客户端。

func WriteMsgToClient(clit Client, conn net.Conn)  {

/* for {

      msg := <- clit.C        // 无数据——阻塞、有数据——继续conn.Write([]byte(msg + "\n") )

}*/

  for msg :=range clit.C {

conn.Write([]byte(msg +"\n") )

}

}

// 分配空间给 Map, 读全局 Message , 遍历在线用户列表

func Manager()  {

for {// 循环着读取全局的channel

      msg := <-Message// 无数据——阻塞、有数据——继续

      rwMutex.RLock()// 读全局 map 之前,加 读锁

      for _, client:=range onLineMap {

client.C <- msg// 将数据写入全局channel —— 广播

      }

rwMutex.RUnlock()// 全局 map 读取结束,解锁

  }

}

func main() {

// 创建监听listener

  listener, err := net.Listen("tcp","127.0.0.1:8800")

if err != nil {

panic(err)

}

defer listener.Close()

// 创建 Manager 管理者go程

  go Manager()

// 循环 接收客户端连接请求,

  for {

conn, err := listener.Accept()

if err != nil {

//panic(err)

        fmt.Println("Accept err:", err)

continue

      }

// 创建 go程处理 客户端事件

      go handleConnect(conn)

}

}

你可能感兴趣的:(Go - 项目 - 网络并发聊天室)