全部文章 笔记与思考

基于Golang的即时通讯系统

这是一个基于Golang的即时通讯系统

即时通讯系统

系统架构

系统架构图

v0.1 基础server构建

v0.1 构建基础Server

此版本只有如下两个文件:

server.go

 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
package main

import (
	"fmt"
	"net"
)

type Server struct {
	Ip   string
	Port int
}

// 创建一个server的接口
func NewServer(ip string, port int) *Server {
	server := &Server{ip, port}

	return server
}

func (this *Server) Handle(conn net.Conn) {
	// ...当前链接的业务
	fmt.Println("链接建立成功")
}

// 启动服务器的接口
func (this *Server) Start() {
	// socket listen,net.Listen用于创建套接字,参数为(network, address string),返回值为(Listener, error)
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	// close listen socket
	defer fmt.Println("关闭链接")
	defer listener.Close()

	for {
		// accept,会返回一个连接Conn,和err
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener.Accept err:", err)
			continue
		}

		// do handler
		go this.Handle(conn)
	}

}

main.go

1
2
3
4
5
6
package main

func main() {
	server := NewServer("127.0.0.1", 8888)
	server.Start()
}

编译运行:

  • Linux系统:
1
2
go build -o server main.go server.go
server
  • Windows系统
1
2
go build -o server.exe main.go server.go
.\server.exe

最后运行测试的时候,根据系统输入如下指令进行测试:

  • Linux系统可以执行nc 127.0.0.1 8888
  • Windows系统可以执行curl 127.0.0.1:8888

若得到如下输出,则表示构建成功。

v0.2 用户上线及广播功能

user.go(新增)

user类型

1
2
3
4
5
6
type User struct {
	Name string
	Addr string
	C    chan string // 与用户通信的管道
	conn net.Conn    // 用户的网络连接
}

方法

创建一个用户的API
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func NewUser(conn net.Conn) *User {
	userAddr := conn.RemoteAddr().String() // 获取用户的网络地址并转为字符串,作为用户名

	user := &User{
		Name: userAddr,
		Addr: userAddr,
		C:    make(chan string), // 创建一个用户消息管道
		conn: conn,              // 保存用户的网络连接
	}

	// 创建完用户对象后,就启动监听当前 User 的消息管道的 goroutine
	go user.ListenMessage()

	return user
}

监听当前User的channel的方法,一旦有消息,就发送给对端客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func (this *User) ListenMessage() {
	for {
		/*
			从 channel C 接收消息字符串 msg
			在消息末尾加换行符
			转换为字节切片
			通过网络连接发送给客户端
		*/

		msg := <-this.C

		this.conn.Write([]byte(msg + "\n")) // 将消息发送给客户端
		// 将字符串转换为字节切片(因为 Write() 方法需要接收字节切片)
	}
}

server.go

server类型:新增OnlineMap和Message属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Server struct {
	Ip   string
	Port int

	// 在线用户列表
	OnlineMap map[string]*User // map[key]value,key是用户名,value是User对象指针
	mapLock   sync.RWMutex     // 保护在线用户列表的锁

	// 消息广播的channel
	Message chan string
}

// 创建一个server的接口
func NewServer(ip string, port int) *Server {
	server := &Server{
		Ip:        ip,
		Port:      port,
		OnlineMap: make(map[string]*User),
		Message:   make(chan string),
	}

	return server
}

修改Handler方法

在处理客户端上线的Handler创建并添加⽤用户

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (this *Server) Handle(conn net.Conn) {
	// ...当前链接的业务
	fmt.Println("链接建立成功!")

	user := NewUser(conn)

	// 用户上线,将用户加入到OnlineMap中
	this.mapLock.Lock()
	this.OnlineMap[user.Name] = user
	this.mapLock.Unlock()

	// 广播当前用户上线消息
	this.BroadCast(user, "已上线\n")

	// 当前handler阻塞:若当前handler结束,则所有子go程也都会结束
	select {}
}

新增广播消息方法

1
2
3
4
5
6
// 广播消息的方法
func (this *Server) BroadCast(user *User, msg string) {
	sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg

	this.Message <- sendMsg
}

新增监听广播消息channel方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 监听Message广播消息channel的goroutine,一旦有消息就发送给全部的在线User
func (this *Server) ListenMessager() {
	for {
		msg := <-this.Message // 不断地从this.Message中读数据

		// 将msg发送给全部的在线User
		this.mapLock.Lock()
		for _, cli := range this.OnlineMap { // key:_  value:cli
			cli.C <- msg
		}
		this.mapLock.Unlock()
	}
}

修改Start方法

用一个goroutine单独监听Message

 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
// 启动服务器的接口
func (this *Server) Start() {
	// socket listen,net.Listen用于创建套接字,参数为(network, address string),返回值为(Listener, error)
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	// close listen socket
	defer fmt.Println("关闭连接")
	defer listener.Close()

	//--------------在此修改--------------
	// 启动监听Message的goroutine
	go this.ListenMessager()
	//------------------------------------

	for {
		// accept,会返回一个连接Conn,和err
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener.Accept err:", err)
			continue
		}

		// do handler
		go this.Handle(conn)
	}
}

测试

测试结果如下图所示,可以实现用户上线消息的广播。

v0.3 用户消息广播功能

此版本只需完善server.go中的handler方法即可。

 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
func (this *Server) Handle(conn net.Conn) {
	// ...当前链接的业务
	fmt.Println("链接建立成功!")

	user := NewUser(conn)

	// 用户上线,将用户加入到OnlineMap中
	this.mapLock.Lock()
	this.OnlineMap[user.Name] = user
	this.mapLock.Unlock()

	// 广播当前用户上线消息
	this.BroadCast(user, "上线了\n")

	//--------------新增部分--------------
	// 接收客户端发送的消息:对socket进行基本的read操作
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := conn.Read(buf) // 从当前conn套接字中读取数据到buf中,返回值是读取的字节数n和错误信息err
			if n == 0 {
				this.BroadCast(user, "下线了\n")
				return
			}

			if err != nil && err != io.EOF {
				fmt.Println("Conn Read err:", err)
				return
			}

			// 提取用户的消息并去除结尾的'\n'
			msg := string(buf[:n-1])

			// 将得到的消息进行广播
			this.BroadCast(user, msg)
		}
	}()
	//---------------------------------

	// 当前handler阻塞:若当前handler结束,则所有子go程也都会结束
	select {}
}

测试 测试结果如下图所示,可以实现广播用户的信息,以及用户下线消息。

v0.4 用户业务封装

与v0.3的功能相同,主要是将用户上线下线以及发送消息的方法封装到user.go中。

user类添加新成员变量server

1
2
3
4
5
6
7
8
type User struct {
	Name string
	Addr string
	C    chan string // 与用户通信的管道
	conn net.Conn    // 用户的网络连接

	server *Server   // 所属服务器
}

新增Online方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 用户上线的业务
func (this *User) Online() {
	// 用户上线,将用户加入到OnlineMap中
	this.server.mapLock.Lock()
	this.server.OnlineMap[this.Name] = this
	this.server.mapLock.Unlock()

	// 广播当前用户上线消息
	this.server.BroadCast(this, "上线了\n")
}

新增Offline方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 用户下线的业务
func (this *User) Offline() {
	// 用户下线,将用户从OnlineMap中删除
	this.server.mapLock.Lock()
	delete(this.server.OnlineMap, this.Name)
	this.server.mapLock.Unlock()

	// 广播当前用户下线消息
	this.server.BroadCast(this, "下线了\n")
}

新增DoMessage方法

1
2
3
4
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
	this.server.BroadCast(this, msg)
}

替换

需要替换的部分

v0.5 在线用户查询

提供SendMsg向对象客户端发送消息API

1
2
3
4
// 给当前user对应的客户端发送消息
func (this *User) SendMsg(msg string) {
	this.conn.Write([]byte(msg))
}

在DoMessage方法中,加上对“who”指令的处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
	if msg == "who" {
		// 查询当前在线用户都有哪些
		this.server.mapLock.Lock()
		for _, user := range this.server.OnlineMap {
			onlineMesg := "[" + user.Addr + "]" + user.Name + ":" + "在线\n"
			this.SendMsg(onlineMesg)
		}
		this.server.mapLock.Unlock()

	} else {
		this.server.BroadCast(this, msg)
	}
}

一个小bug

需要注意的是,前面对用户输入的消息仅去掉了最末尾的’\n’,但这是不够的,因为Windows下默认的换行标识符是’\r\n’,因此需要去掉末尾的两个元素。 具体修改位置

另外,不同操作系统的默认换行标识符也是不同的,若下图所示: 不同操作系统下的默认换行标识符

测试

**用户输入"who"即可查看当前在线用户。如此,即可实现查询在线用户的操作 查询示例

v0.6 修改用户名

修改代码

 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
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
	if msg == "who" {
		// 查询当前在线用户都有哪些
		this.server.mapLock.Lock()
		for _, user := range this.server.OnlineMap {
			onlineMesg := "[" + user.Addr + "]" + user.Name + ":" + "在线\n"
			this.SendMsg(onlineMesg)
		}
		this.server.mapLock.Unlock()
	} else if len(msg) > 7 && msg[:7] == "rename|" {
		// 消息格式:rename|张三
		// string.Split(msg, "sign")可以将字符串msg以sign为断点来切分,返回的是一个字符串数组
		newName := strings.Split(msg, "|")[1]

		// 判断name是否存在
		_, ok := this.server.OnlineMap[newName]
		if ok {
			this.SendMsg("当前用户名已被使用!\n")
		} else {
			this.server.mapLock.Lock()
			delete(this.server.OnlineMap, this.Name)
			this.server.OnlineMap[newName] = this
			this.server.mapLock.Unlock()

			this.Name = newName
			this.SendMsg("您已成功更新用户名:" + this.Name + "!\n")
		}
	} else {
		this.server.BroadCast(this, msg)
	}
}

在此处进行修改

测试

测试结果如下,成功实现修改用户名及Online Map操作:

v0.7 超时强踢功能

修改Handler方法

 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
func (this *Server) Handler(conn net.Conn) {
	// ...当前链接的业务
	fmt.Println("链接建立成功!")

	user := NewUser(conn, this)

	user.Online()

	// -----------修改位置-----------
	// 监听用户是否活跃的channel
	isLive := make(chan bool)
	// -----------------------------

	// 接收客户端发送的消息:对socket进行基本的read操作
	go func() {
		buf := make([]byte, 4096)
		for {
			// 从当前conn套接字中读取数据到buf中,返回值是读取的字节数n和错误信息err
			n, err := conn.Read(buf)

			if n == 0 {
				user.Offline()
				return
			}

			if err != nil && err != io.EOF {
				fmt.Println("Conn Read err:", err)
				return
			}

			// 提取用户的消息并去除结尾的'\r\n'
			msg := string(buf[:n-2])

			// 用户针对msg进行消息处理
			user.DoMessage(msg)

			// 用户的任意消息,都能代表用户当前是活跃的
			isLive <- true
		}
	}()

	// -----------修改位置-----------
	// 当前handler阻塞:若当前handler结束,则所有子go程也都会结束
	for {
		select {
		case <-isLive:
			// 当前用户是活跃的,应该充值定时器
			// 不用做任何事情,只是为了结束本次的select,循环进入新的一个select,以重置下面的定时器,妙妙妙

		case <-time.After(time.Minute * 30): // time.After也是一个channel,作为计时器使用,这里计时30分钟,超时后channel中就会有一个数据
			// 已经超时
			// 将当前的user强制关闭

			user.SendMsg("由于长时间未操作,你被踢了。\n")

			// 销毁用户资源
			close(user.C)

			// 关闭连接
			conn.Close()

			// 退出当前handler
			// runtime.Goexit()
			return
		}
	}
	// -----------------------------
}

select在这个地方起到了非常妙的效果,具体详见注释。

测试

v0.8 用户私聊功能

修改DoMessage

此版本只需在DoMessage方法中新增一个分支即可:

 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
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
	if msg == "who" {
		// 查询当前在线用户都有哪些
		this.server.mapLock.Lock()
		for _, user := range this.server.OnlineMap {
			onlineMesg := "[" + user.Addr + "]" + user.Name + ":" + "在线\n"
			this.SendMsg(onlineMesg)
		}
		this.server.mapLock.Unlock()
	} else if len(msg) > 4 && msg[:3] == "to|" {
		// 私聊消息格式:to|张三|消息内容

		// 1.获取对方用户名
		remoteName := strings.Split(msg, "|")[1]
		if remoteName == "" {
			this.SendMsg("消息格式不正确,请使用 \"to|张三|你好啊\"的格式.\n")
			return
		}
		// 2.根据用户名得到对方的User对象
		remoteUser, ok := this.server.OnlineMap[remoteName]
		if !ok {
			this.SendMsg(remoteName + "当前不在线!\n")
		}

		// 3.获取消息内容,通过对方的User对象将消息内容发送过去
		content := strings.Split(msg, "|")[2]
		if content == "" {
			this.SendMsg("无消息内容,请重发。\n")
			return
		}
		remoteUser.SendMsg(this.Name + "对您说:" + content + "\n")
	} else if len(msg) > 7 && msg[:7] == "rename|" {
		// 修改用户名消息格式:rename|张三
		// string.Split(msg, "sign")可以将字符串msg以sign为断点来切分,返回的是一个字符串数组
		newName := strings.Split(msg, "|")[1]

		// 判断name是否存在
		_, ok := this.server.OnlineMap[newName]
		if ok {
			this.SendMsg("当前用户名已被使用!\n")
		} else {
			this.server.mapLock.Lock()
			delete(this.server.OnlineMap, this.Name)
			this.server.OnlineMap[newName] = this
			this.server.mapLock.Unlock()

			this.Name = newName
			this.SendMsg("您已成功更新用户名:" + this.Name + "!\n")
		}
	} else {
		if msg == "" {
			this.SendMsg("无消息内容,请重发。\n")
			return
		}
		this.server.BroadCast(this, msg)
	}
}

测试

实现预期功能:

v0.9 客户端实现

v0.9.1 建立连接

在这个版本中,要新创建一个client.go。但当前服务器的ip和端口是写死的,在后续版本中会实现在客户端指定服务器的ip和端口。

定义client类型

1
2
3
4
5
6
type Client struct {
	ServerIp   string
	ServerPort int
	Name       string   // 重命名后在赋值
	conn       net.Conn // 连成功后再赋值
}

创建client对象,同时连接服务器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func NewClient(serverIp string, serverPort int) *Client {
	// 创建客户端对象
	client := &Client{
		ServerIp:   serverIp,
		ServerPort: serverPort,
	}

	// 连接server,net.Dial是 Go 语言 net 标准库中最核心的函数之一,用于从客户端发起一个网络连接。参数是协议和地址(network, address),升级版本加了超时控制参数
	// conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
	conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort), 5*time.Second)

	// 如果连接失败
	if err != nil {
		fmt.Println("net.DialTimeout error:", err)
		return nil
	}

	client.conn = conn

	// 返回对象
	return client
}

主业务创建client对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
	client := NewClient("127.0.0.1", 8888)
	if client == nil {
		fmt.Println(">>>>> 连接服务器失败...")
		return
	}

	fmt.Println(">>>>> 连接服务器成功!")

	// 启动客户端的业务
	for {
	}
}

测试

测试结果

v0.9.2 命令行解析

修改client.go

**NewClient方法无需修改,只需将NewClient方法之后的代码替换成如下代码即可。

 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
// 定义了两个全局变量,用来存储最终要连接的 IP 和端口。
var serverIp string
var serverPort int

// init() 函数会在 main() 函数执行之前自动被调用。这确保了程序一开始就配置好了这些参数规则。
// flag包,用于实现命令行参数解析的功能,允许在运行程序(.exe)时,通过命令行动态地指定服务器的IP和端口,而不是把IP写死在代码里。
// .\client -ip 127.0.0.1 -port 8888
func init() {
	flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认为127.0.0.1)")
	flag.IntVar(&serverPort, "port", 8888, "设置服务器IP地址(默认为8888)")
}

func main() {
	// 用于读取并解析用户输入的命令
	flag.Parse()

	// client := NewClient("127.0.0.1", 8888)
	client := NewClient(serverIp, serverPort)

	if client == nil {
		fmt.Println(">>>>> 连接服务器失败...")
		return
	}

	fmt.Println(">>>>> 连接服务器成功!")

	// 启动客户端的业务
	for {
	}
}

测试

v0.9.3 菜单显示

Client新增flag属性

1
2
3
4
5
6
7
type Client struct {
	ServerIp   string
	ServerPort int
	Name       string   // 重命名后在赋值
	conn       net.Conn // 连成功后再赋值
	flag       int      // 表示当前client的模式
}

新增menu方法,获取用户输入的模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func (client *Client) menu() bool {
	var flag int

	fmt.Println("1.公聊模式")
	fmt.Println("2.私聊模式")
	fmt.Println("3.更新用户名")
	fmt.Println("0.退出")

	fmt.Scanln(&flag)

	if flag >= 0 && flag <= 3 {
		client.flag = flag // flag合法,赋值给用户的状态
		return true
	} else {
		fmt.Println(">>>>>请输入合法范围内的谁<<<<<")
		return false
	}
}

新增Run主业务循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (client *Client) Run() {
	// 应该根据用户menu的提示结果来选择不同的模式

	for client.flag != 0 {
		for client.menu() != true {
		}

		// 根据不同的模式处理不同的业务
		// 在 Go 语言中的 switch 是默认不穿透的,每个 case 执行完后会自动退出,因此不需要显式地写 break。
		// 如果希望执行完 case 1 后,继续执行 case 2(就像 C 语言那样),就需要显式使用 fallthrough 关键字
		switch client.flag {
		case 1:
			// 公聊模式
			fmt.Println("公聊模式选择...")
			// fallthrough		// case穿透
		case 2:
			// 私聊模式
			fmt.Println("私聊模式选择...")
		case 3:
			// 更新用户名
			fmt.Println("更新用户名选择...")
		}
	}
}

测试

v0.9.4 更新用户名(封装版)

新增UpdateName()更新用户名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func (client *Client) UpdateName() bool {
	// 提示输入用户名->用户输入->发给服务器
	var name string
	fmt.Println(">>>>>请输入用户名,exit退出:")
	fmt.Scanln(&name)

	if name == "exit" {
		return false
	}

	client.Name = name

	sendMsg := "rename|" + client.Name + "\r\n"
	_, err := client.conn.Write([]byte(sendMsg))
	if err != nil {
		fmt.Println("conn.Write err:", err)
		return false
	}

	return true
}

把UpdateName加入到Run业务分支中

 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
func (client *Client) Run() {
	// 应该根据用户menu的提示结果来选择不同的模式

	for client.flag != 0 {
		for client.menu() != true {
		}

		// 根据不同的模式处理不同的业务
		// 在 Go 语言中的 switch 是默认不穿透的,每个 case 执行完后会自动退出,因此不需要显式地写 break。
		// 如果希望执行完 case 1 后,继续执行 case 2(就像 C 语言那样),就需要显式使用 fallthrough 关键字
		switch client.flag {
		case 1:
			// 公聊模式
			fmt.Println("公聊模式选择...")
			// fallthrough		// case穿透
		case 2:
			// 私聊模式
			fmt.Println("私聊模式选择...")
		case 3:
			// 更新用户名
			// fmt.Println("更新用户名选择...")
			client.UpdateName()
		}
	}
}

添加处理server回执消息的DealResponse()方法

1
2
3
4
5
6
// 处理server回应的消息,直接显示到标准输出即可
func (client *Client) DealResponse() {
	// 建立一条直通管道,把服务器发来的所有原始数据,原封不动地直接打印到屏幕上
	// 一旦client.conn有数据,就直接copy到stdout标准输出上,io.Copy内部自带了一个死循环,永久阻塞监听
	io.Copy(os.Stdout, client.conn) // 不断阻塞等待client.conn中的数据,如果有数据就直接反倒os.Stdout中,而且是永久阻塞
}

在main方法中开启一个go,去承载DealResponse()流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
	// 用于读取并解析用户输入的命令
	flag.Parse()

	// client := NewClient("127.0.0.1", 8888)
	client := NewClient(serverIp, serverPort)

	if client == nil {
		fmt.Println(">>>>> 连接服务器失败...")
		return
	}

	// 单独开启一个goroutine去从处理server的回执消息
	go client.DealResponse()

	fmt.Println(">>>>> 连接服务器成功!")

	// 启动客户端的业务
	client.Run()
}

测试

v0.9.5 封装版公聊、私聊、查询在线用户

公聊:新增PublicChat方法

 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
func (client *Client) PublicChat() {
	// 提示输入消息内容->用户输入->发给服务器

	var chatMsg string
	fmt.Println(">>>>>请输入聊天内容,exit退出:")
	fmt.Scanln(&chatMsg)

	for chatMsg != "exit" {
		// 发送给服务器

		// 消息不为空则发送
		if len(chatMsg) != 0 {
			sendMsg := chatMsg + "\r\n"
			_, err := client.conn.Write([]byte(sendMsg))
			if err != nil {
				fmt.Println("conn.Write err:", err)
				break
			}
		}

		chatMsg = ""
		fmt.Println(">>>>>请输入聊天内容,exit退出:")
		fmt.Scanln(&chatMsg)
	}
}

私聊:新增PrivateChat方法

 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
func (client *Client) PrivateChat() {
	// 提示输入消息内容->用户输入->发给服务器

	var chatName string
	var chatMsg string

	client.QueryOnline()
	fmt.Println(">>>>>请输入要私聊的用户名:")
	fmt.Scanln(&chatName)

	fmt.Println(">>>>>请输入与", chatName, "的聊天内容,exit退出:")
	fmt.Scanln(&chatMsg)

	for chatMsg != "exit" {
		// 发送给服务器

		// 消息不为空则发送
		if len(chatMsg) != 0 {
			sendMsg := "to" + "|" + chatName + "|" + chatMsg + "\r\n"
			_, err := client.conn.Write([]byte(sendMsg))
			if err != nil {
				fmt.Println("conn.Write err:", err)
				break
			}
		}

		chatMsg = ""
		fmt.Println(">>>>>请输入与", chatName, "的聊天内容,exit退出:")
		fmt.Scanln(&chatMsg)
	}
}

查询在线用户:新增QueryOnline方法

1
2
3
4
5
6
7
8
9
func (client *Client) QueryOnline() {
	// 查询当前在线用户
	sendMsg := "who\r\n"
	_, err := client.conn.Write([]byte(sendMsg))
	if err != nil {
		fmt.Println("conn.Write err:", err)
		return
	}
}

调用这些方法:修改Run中的分支

 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
func (client *Client) Run() {
	// 应该根据用户menu的提示结果来选择不同的模式

	for client.flag != 0 {
		for client.menu() != true {
		}

		// 根据不同的模式处理不同的业务
		// 在 Go 语言中的 switch 是默认不穿透的,每个 case 执行完后会自动退出,因此不需要显式地写 break。
		// 如果希望执行完 case 1 后,继续执行 case 2(就像 C 语言那样),就需要显式使用 fallthrough 关键字
		switch client.flag {
		case 1:
			// 公聊模式
			// fmt.Println("公聊模式选择...")
			client.PublicChat()
			// fallthrough		// case穿透
		case 2:
			// 私聊模式
			// fmt.Println("私聊模式选择...")
			client.PrivateChat()
		case 3:
			// 更新用户名
			// fmt.Println("更新用户名选择...")
			client.UpdateName()
		case 4:
			// 查询在线用户
			fmt.Println("当前在线的用户有...")
			client.QueryOnline()
		}
	}
}

此外,优化了菜单的显示方式

 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
func (client *Client) menu() bool {
	var flag int

	menuContent := []string{
		"1.公聊模式",
		"2.私聊模式",
		"3.更新用户名",
		"4.查询在线用户",
		"0.退出",
	}
	menuSize := len(menuContent)

	for _, line := range menuContent {
		fmt.Println(line)
	}

	// fmt.Println("1.公聊模式")
	// fmt.Println("2.私聊模式")
	// fmt.Println("3.更新用户名")
	// fmt.Println("4.查询在线用户")
	// fmt.Println("0.退出")

	fmt.Scanln(&flag)

	if flag >= 0 && flag < menuSize {
		client.flag = flag // flag合法,赋值给用户的状态
		return true
	} else {
		fmt.Println(">>>>>请输入合法范围内的数字<<<<<")
		return false
	}
}

测试

完结撒花

此文档根据B站刘丹冰老师的《8小时转职Golang工程师(如果你想低成本学习Go语言)》教程构建了一个即时通讯系统,但目前只有命令行的操作方式,未来或许会新增GUI版本,敬请期待。

源码地址:

  • Github:https://github.com/SymphonyW/IM-System.git
    可运行git clone https://github.com/SymphonyW/IM-System.git直接拉去
  • 百度网盘:https://pan.baidu.com/s/1j_cU9qtjDnDy_fmVYBX1Mw?pwd=3v17 提取码: 3v17