聊天室的创建,主要是由两部分组成,服务端和客户端, 新增一个客户端相当于新来一个用户,陆续参与进来进行群聊,服务端就是处理所有客户端的操作然后反馈出去。
服务端具体作用:一直监听所有客户端的连接net.Conn(有进入、发送消息、退出这样的行为操作),然后通过广播器(自定义一个函数)将客户端的这些操作,广播到其他所有的客户端并显示,这样每个客户端就可以看到其余所有人的聊天状态与信息了。
其中广播器用到了select的多路复用技术,case判断是什么通道从而进行不同的操作,如果是进入通道,相当于新来一个用户,就进行新增用户操作;如果是信息通道,就将信息发送给全部的客户端;如果是退出通道,说明这个用户离开聊天室,那么就删除这个用户并关闭它的连接。
客户端比较简单,就类似拨号上网,先连接到服务端,然后进行信息的输入和发送即可。
这里使用了goroutine并发机制,对于看过本人前面文章的读者来说,就会比较熟悉这个并发,通过创建chan多通道(channel),进行互相独立的通信。
Go语言进阶,闭包、指针、并发
https://blog.csdn.net/weixin_41896770/article/details/127547900
Go语言并发比较二叉树(Binary Tree)
https://blog.csdn.net/weixin_41896770/article/details/127569147
Go语言进阶,详解并发中的通道机制
https://blog.csdn.net/weixin_41896770/article/details/127748316
尤其建议先看完上面最后一篇Go语言进阶,详解并发中的通道机制,这样对于并发通道,就显得要容易点。
服务端的代码
- package main
-
- import (
- "bufio"
- "fmt"
- "log"
- "net"
- "strings"
- )
-
- func main() {
- listener, err := net.Listen("tcp", "127.0.0.1:65535")
- if err != nil {
- log.Fatal(err)
- }
- // 启动广播器
- go broadcaster()
- for {
- // 将下一个连接返回给侦听器
- conn, err := listener.Accept()
- if err != nil {
- log.Print(err)
- continue
- }
- // 不断处理客户端新的连接
- go handleConn(conn)
- }
- }
-
- // 定义一个名为user_ch的只发送的单通道
- type user_ch chan<- string
-
- // 进入通道,退出通道以及信息通道
- var (
- entry_ch = make(chan user_ch)
- exit_ch = make(chan user_ch)
- msgs_ch = make(chan string)
- )
-
- // 广播器
- func broadcaster() {
- user_chs := make(map[user_ch]bool)
- for {
- select {
- // 如果是进入通道,就新增一个用户
- case user := <-entry_ch:
- user_chs[user] = true
- // 如果是信息通道,将信息遍历发送给所有客户端
- case msg := <-msgs_ch:
- for user := range user_chs {
- user <- msg
- }
- // 如果是退出通道,需删除这个客户端然后关闭
- case user := <-exit_ch:
- delete(user_chs, user)
- close(user)
- }
- }
- }
-
- // 处理客户端的连接请求、信息发送、连接关闭的操作
- // 使用goroutine机制开启客户端的写操作,将信息写入到连接中
- // 其中的net.Conn是一个接口,有读写等方法
- func handleConn(conn net.Conn) {
- ch := make(chan string)
- go userWrite(conn, ch)
-
- // 这里将获取的客户端的IP地址+端口当作用户名
- userName := conn.RemoteAddr().String()
- ch <- fmt.Sprintf("欢迎 %s,进入聊天室", userName)
-
- entry_ch <- ch //进入的通道
- msgs_ch <- fmt.Sprintf("%s,上线了", userName)
-
- // 扫描并读取用户的输入内容,发送给信息通道
- scanInfo := bufio.NewScanner(conn)
- for scanInfo.Scan() {
- // 不能发送空消息
- if len(strings.TrimSpace(scanInfo.Text())) == 0 {
- continue
- }
- msgs_ch <- userName + ": " + scanInfo.Text()
- }
-
- exit_ch <- ch // 退出的通道
- msgs_ch <- fmt.Sprintf("%s,下线了", userName)
- conn.Close() //下线记得关闭连接
- }
-
- func userWrite(conn net.Conn, ch <-chan string) {
- for msg := range ch {
- fmt.Fprintln(conn, msg)
- }
- }
客户端的代码
- package main
-
- import (
- "io"
- "log"
- "net"
- "os"
- )
-
- func main() {
- conn, err := net.Dial("tcp", "127.0.0.1:65535")
- if err != nil {
- log.Fatal(err)
- }
-
- // 服务端的通道
- server_ch := make(chan struct{})
- go func() {
- io.Copy(os.Stdout, conn)
- log.Println("服务端关闭")
- server_ch <- struct{}{} // 往服务端通道发送信息
- }()
-
- // 将标准输入的信息写入到conn里
- copyInfo(conn, os.Stdin)
- conn.Close()
- //<-server_ch
- }
-
- func copyInfo(dst io.Writer, src io.Reader) {
- if _, err := io.Copy(dst, src); err != nil {
- log.Fatal(err)
- }
- }
效果如图:

这里是使用了IP+端口来标识用户,也可以使用自定义昵称进入,在进入前注册一个,我们仿照handleConn这个函数来写一个注册用户名的函数userRegiste,判断输入的用户名是否重复,然后返回用户名。
- package main
-
- import (
- "bufio"
- "fmt"
- "log"
- "net"
- "strings"
- )
-
- func main() {
- listener, err := net.Listen("tcp", "127.0.0.1:65535")
- if err != nil {
- log.Fatal(err)
- }
- // 启动广播器
- go broadcaster()
- for {
- // 将下一个连接返回给侦听器
- conn, err := listener.Accept()
- if err != nil {
- log.Print(err)
- continue
- }
- // 不断处理客户端新的连接
- go handleConn(conn)
- }
- }
-
- // 定义一个名为user_ch的只发送的单通道
- // 修改处:定义个用户名的结构体
- type user_ch chan<- string
- type userInfo struct {
- name string
- ch user_ch
- }
-
- // 进入通道,退出通道以及信息通道
- // 修改处:进入和退出的通道,使用了userInfo结构体类型
- var (
- entry_ch = make(chan userInfo)
- exit_ch = make(chan userInfo)
- msgs_ch = make(chan string)
- )
-
- // 广播器
- func broadcaster() {
- user_chs := make(map[string]user_ch)
- for {
- select {
- case user := <-register:
- // 先判断新用户名是否有重复
- _, ok := user_chs[user.name]
- user.ch <- !ok
-
- // 如果是进入通道,就新增一个用户
- case user := <-entry_ch:
- var usernames []string
- for username := range user_chs {
- //if username == user.name {
- // user.ch <- fmt.Sprintf("其他用户已使用: %s", strings.Join(usernames, "; "))
- //}
- usernames = append(usernames, username)
- }
- user_chs[user.name] = user.ch
- // 如果是信息通道,将信息遍历发送给所有客户端
- case msg := <-msgs_ch:
- for _, user := range user_chs {
- user <- msg
- }
- // 如果是退出通道,需删除这个用户然后关闭它的通道
- case user := <-exit_ch:
- delete(user_chs, user.name)
- close(user.ch)
- }
- }
- }
-
- // 处理客户端的连接请求、信息发送、连接关闭的操作
- // 使用goroutine机制开启客户端的写操作,将信息写入到连接中
- // 其中的net.Conn是一个接口,有读写等方法
- func handleConn(conn net.Conn) {
- userName := userRegiste(conn) //修改处:注册用户名的操作
- ch := make(chan string)
- go userWrite(conn, ch)
-
- ch <- fmt.Sprintf("欢迎 %s,进入聊天室", userName)
-
- // 修改处:进入和退出的通道需要使用userInfo的结构体
- userinfo := userInfo{userName, ch}
- entry_ch <- userinfo //进入的通道
- msgs_ch <- fmt.Sprintf("%s,上线了", userName)
-
- // 扫描并读取用户的输入内容,发送给信息通道
- scanInfo := bufio.NewScanner(conn)
- for scanInfo.Scan() {
- // 不能发送空消息
- if len(strings.TrimSpace(scanInfo.Text())) == 0 {
- continue
- }
- msgs_ch <- userName + ": " + scanInfo.Text()
- }
-
- exit_ch <- userinfo // 退出的通道
- msgs_ch <- fmt.Sprintf("%s,下线了", userName)
- conn.Close() //下线记得关闭连接
- }
-
- func userWrite(conn net.Conn, ch <-chan string) {
- for msg := range ch {
- fmt.Fprintln(conn, msg)
- }
- }
-
- // 修改处:定义一个注册用户名的结构体
- type registeInfo struct {
- name string
- ch chan<- bool
- }
-
- // 新建一个注册用户名的通道
- var register = make(chan registeInfo)
-
- func userRegiste(conn net.Conn) (uname string) {
- ch := make(chan bool)
- fmt.Fprint(conn, "输入昵称: ")
- scanInfo := bufio.NewScanner(conn)
- for scanInfo.Scan() {
- if len(strings.TrimSpace(scanInfo.Text())) == 0 {
- continue
- }
- uname = scanInfo.Text()
- register <- registeInfo{uname, ch}
- if <-ch {
- break
- }
- fmt.Fprintf(conn, "昵称:%q,已存在!\r\n换一个昵称: ", uname)
- }
- return uname
- }
