即时通讯系统
系统架构
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()
}
|
编译运行:
1
2
|
go build -o server main.go server.go
server
|
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的模式
}
|
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版本,敬请期待。
源码地址: