go语言实现聊天室

2021/12/6 go

🌙 一、概述

实现一个网络聊天室(群),

功能分析:

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

效果展示:

1586310223509

技术点分析:

  1. sockt tcp编程

  2. map结构,

    1. 存储所有的用户
    2. map遍历
    3. map删除
  3. go程、channel

  4. select(超时退出,主动退出)

  5. timer 定时器

🌙 二、实现基础

第一阶段:

  1. 思路分析

🌙 1.tcp socket,建立多个连接

package main

import (
	"fmt"
	"net"
)

//将所有的代码写在一个文件中,不做代码整理

func main() {
	//创建服务器
	listener, err := net.Listen("tcp", ":8080")
	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("conn.Read err:", err)
			return
		}

		fmt.Println("服务器接收客户端发送过来的数据为: ", string(buf[:cnt-1]), ", cnt:", cnt)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

go run chatroom.go

启动nc

1586311154143

🌙 2.分析

1586312815870

数据流向:

1586313422882

🌙 3. 定义User/map结构

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

//创建一个全局的map结构,用户保存所有的用户
var allUsers = make(map[string]User)
1
2
3
4
5
6
7
8
9
10
11

在handler中调用(建立连接之后)

1586314505040

🌙 4. 定义message通道

1586315174565

创建监听广播go程函数

1586315219473

启动,全局唯一:

1586315243921

写入上线数据:

1586315273487

bug修复:

1586316489973

1586316506637

效果:

1586316531673

🌙 5. 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))
	}
}
1
2
3
4
5
6
7
8
9
10
11

1586317568413

效果:

1586317618285

当前整体代码:

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", ":8080")
	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("conn.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

		fmt.Println("message 接收到消息:", info)

		//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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

🌙 三、增加功能

🌙 1. 查询用户

查询命令:who ==》 将当前所有的登陆的用户,展示出来, id,name , 返回给当前用户

在handler中,处理业务逻辑:

		fmt.Println("服务器接收客户端发送过来的数据为: ", string(buf[:cnt-1]), ", cnt:", cnt)
		//-------- 业务逻辑处理  开始----------
		//1. 查询当前所有的用户 who
		//	a. 先判断接收的数据是不是who  ==> 长度&&字符串
		userInput := string(buf[:cnt-1]) //这是用户输入的数据,最后一个是回车,我们去掉它
		if len(userInput) == 3 && userInput == "who" {
			//  b. 遍历allUsers这个map (key : userid  value: user本身),将id和name拼接成一个字符串,返回给客户端
			fmt.Println("用户即将查询所有用户信息!")

			//这个切片包含所有的用户信息
			var userInfos []string

			//[]string{"userid:z3, username:z3", "userid:l4, username:l4","userid:w5, username:w5"}
			for _, user := range allUsers {
				userInfo := fmt.Sprintf("userid:%s, username:%s", user.id, user.name)
				userInfos = append(userInfos, userInfo)
			}

			//最终写到管道中,一定是一个字符串
			r := strings.Join(userInfos, "\n") //连接数字切片,生成字符串
			//strings.Split() //分割字符串
			//`
			//	"userid:z3, username:z3"
			//	"userid:l4, username:l4"
			//	"userid:w5, username:w5"
			//`

			//将数据返回给查询的客户端
			newUser.msg <- r

		} else {
			//如果用户输入的不是命令,只是普通的聊天信息,那么只需要写到广播通道中即可,由其他的go程进行常规转发
			message <- userInput
		}
		//-------- 业务逻辑处理  结束----------
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

1586328031581

🌙 2. 重命名

规则: rename|Duke

  1. 读取数据判断长度7,判断字符是rename
  2. 使用|进行分割,获取|后面的部分,作为名字
  3. 更新用户名字newUser.name = Duke
  4. 通知客户端,更新成功
else if len(userInput) > 9 && userInput[:7] == "\\rename" {
			//[:3]  // 0, 1, 2  ==> 左闭右开

			//	规则:  rename|Duke
			//1. 读取数据判断长度7,判断字符是rename
			//2. 使用|进行分割,获取|后面的部分,作为名字
			//func Split(s, sep string) []string
			//arry := strings.Split(userInput, "|")
			//name := arry[1]
			//3. 更新用户名字newUser.name = Duke
			newUser.name = strings.Split(userInput, "|")[1]
			allUsers[newUser.id] = newUser //更新map中的user

			//4. 通知客户端,更新成功
			newUser.msg <- "rename successfully!"
		} else {
			//如果用户输入的不是命令,只是普通的聊天信息,那么只需要写到广播通道中即可,由其他的go程进行常规转发
			message <- userInput
		}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

效果:

1586330276420

🌙 3. 主动退出

两种形式:==》 quit,ctrl+c

用户退出:清理工作

  1. 从map中删除
  2. 对应的conn要close

每个用户都有自己的watch go程,负责监听退出信号

func watch(user *User, conn net.Conn, isQuit <-chan bool) {
	fmt.Println("222222 启动监听退出信号的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
		}
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在handler中启动go watch,同时传入相应信息:

1586331878303

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

1586331934522

测试结果:

1586331770362

🌙 4. 超时退出

使用定时器来进行超时管理

如果60s每没有发送任何数据,那么直接将这个连接关闭掉:

<- time.After(60 *time.second)   //chan time
1
更新watch函数

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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

创建并传入restTimer管道,

1586334173527

1586334226670

效果:

1586334266296

1586334104346

🌙 5. map

package main

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

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

//map不允许同时读写,如果有不同go程同时操作map,需要对map上锁
// concurrent map read and map write
func main() {
	go func() {
		for {
			fmt.Println("111111")
			lock.Lock()
			fmt.Println("2222222")
			idnames[0] = "duke"
			fmt.Println("3333333")
			lock.Unlock()
		}
	}()

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

	for {
		fmt.Println("OVER")
		time.Sleep(1 * time.Second)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41