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

代码实现的思路 : 


分析草图

模块划分:

        主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。

                    能将计时器重置。


代码 :



复制 代码 到 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)

}

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351