【启程Golang之旅】并发编程构建简易聊天系统

欢迎来到Golang的世界!在当今快节奏的软件开发领域,选择一种高效、简洁的编程语言至关重要。而在这方面,Golang(又称Go)无疑是一个备受瞩目的选择。在本文中,带领您探索Golang的世界,一步步地了解这门语言的基础知识和实用技巧。

在这篇文章中,我们将用Go语言实现一个简易网络聊天应用,重点探讨Socket编程、map结构用于管理用户、goroutines与channels实现并发通信、select语句处理超时与主动退出,以及timer定时器的应用。这些概念将帮助我们构建高效且实用的聊天系统。让我们开始吧!

目录

socket-server建立

创建msg广播通道

查询用户与重命名

用户主动退出聊天

用户超时退出聊天


socket-server建立

socket-server的作用是实现网络通信的基础,允许不同设备(如客户端和服务器)通过网络交换数据,下面我们模拟TCP服务器能够接收多个客户端的连接请求,并在每个连接上启动一个新的goroutine进行数据处理。每当有数据从客户端发送到服务器时,服务器会读取并打印这些数据:

package main

import (
	"fmt"
	"net"
)

func main() {
	// 01 创建服务器
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("net.listen err:", err)
		return
	} else {
		fmt.Println("服务器启动成功...")
	}
	for {
		fmt.Println("主go程监听中...")
		// 02 监听服务器
		connect, err := listener.Accept()
		if err != nil {
			fmt.Println("listener.accept err:", err)
			return
		}
		fmt.Println("建立连接成功...")
		// 03 启动处理业务的go程
		go handler(connect)
	}

}

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
		} else {
			fmt.Println("服务器接收客户端发送过来的数据为:", string(buf[:cnt-1]), "cnt:", cnt)
		}
	}

}

这种设计使得服务器具有并发处理能力,可以同时处理多个客户端的请求,这里我们借助nc工具来模拟请你,不了解工具的可以参考我之前的文章:地址 ,具体如下所示:

创建msg广播通道

要知道我们程度当中是有很多用户的,当一个用户发送消息能让所有的用户看到的话是需要有一个进行全局广播的管道:message,如下所示全局广播的message获取到“hello”,然后遍历所有的用户并向用户msg管道发送hello,在go程中每一个用户连接一个需要再启动一个go程,读取message数据之后发送给客户端:

接下来我们开始创建User结构,用于管理每次创建用户的结构:

// User 定义用户结构体
type User struct {
	id   string
	name string
	msg  chan string
}

// 创建全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

然后我们再每次创建go程的时候以连接的key作为唯一添加到用户的map结构当中:

接下来我们定义全局的管道,用于接收任何人发送过来的消息:

// 定义一个message全局通道,用于接收任何人发送过来的消息
var message = make(chan string, 10)

接下来再每次创建新用户上线的时候,写入message:

接下来创建一个全局唯一的广播通道用于通知用户消息,然后在main函数中调用一次下面的go程即可:

// 向所有的用户广播消息,启动全局唯一go程
func broadcast() {
	fmt.Println("启动广播go程...")
	defer fmt.Println("broadcast程序结束...") // 程序结束,关闭广播go程
	for {
		fmt.Println("广播go程监听中...")
		// 01 从message通道中读取消息
		info := <-message
		// 02 遍历map结构,向每个用户发送消息
		for _, user := range allUsers {
			// 03 向每个用户发送消息
			user.msg <- info
		}
	}
}

接下来每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端:

// 每个用户监听自己的msg通道,负责将数据返回给客户端
func writeBackToClient(user *User, conn net.Conn) {
	fmt.Println("启动用户", user.name, "的writeBackToClient go程...")
	for data := range user.msg {
		fmt.Printf("user: %s 写回给客户端的数据为: %s\n", user.name, data)
		_, _ = conn.Write([]byte(data))
	}
}

查询用户与重命名

查询用户:当用户输入查询命令who,则将当前所有登录的用户展示出来,id与name返回给当前用户:

// 01 查询当前所有的用户 who
if len(buf[:cnt-1]) == 3 && string(buf[:cnt-1]) == "who" {
	var userInfos []string
	// 遍历map结构,获取所有的用户信息
	for _, user := range allUsers {
		userInfo := fmt.Sprintf("userid:%s, username: %s", user.id, user.name)
		userInfos = append(userInfos, userInfo)
	}
	// 最终写到管道中
	message <- strings.Join(userInfos, "\n")
}

重命名:这里我们可以设置一个规则:rename | Duke,使用竖线进行分割获取竖线后面的部分作为名字,通过设置 newUser.name = Duke,然后通知客户端更新名字成功,为了避免想输入命令作为消息,这里我们对命令做一个处理:

// 01 查询当前所有的用户 who
if len(buf[:cnt-1]) == 4 && string(buf[:cnt-1]) == "\\who" {
	var userInfos []string
	// 遍历map结构,获取所有的用户信息
	for _, user := range allUsers {
		userInfo := fmt.Sprintf("userid:%s, username: %s", user.id, user.name)
		userInfos = append(userInfos, userInfo)
	}
	// 最终写到管道中
	newUser.msg <- strings.Join(userInfos, "\n")
} else if len(buf[:cnt-1]) > 9 && string(buf[:7]) == "\\rename" {
	// 更新名字
	newUser.name = strings.Split(string(buf[:cnt-1]), "|")[1]
	allUsers[newUser.id] = newUser // 更新map结构中的用户信息
	// 通知客户端更新成功
	newUser.msg <- fmt.Sprintf("改名成功, 新的名字为: %s", newUser.name)
} else {
	message <- string(buf[:cnt-1])
}

用户主动退出聊天

接下来我们通过使用ctrl+c的方式进行退出程序,用户退出还需要做一下清理工作,需要从map当中删除用户信息,还需要将对应的conn连接进行close,具体如下所示:

// 启动一个go程,负责监听退出信号,通知所有go程退出
func watch(user *User, conn net.Conn, isQuit chan bool) {
	fmt.Println("启动用户", user.name, "的watch go程...")
	defer fmt.Println("watch程序结束...") // 程序结束,关闭监听go程
	for {
		select {
		case <-isQuit: // 收到退出信号,通知所有go程退出
			delete(allUsers, user.id)
			fmt.Println("删除当前用户:", user.name)
			message <- fmt.Sprintf("[%s][%s]下线了", user.id, user.name)
			_ = conn.Close()
		}
	}
}

在handler中启动go watch并传入对应信息:

然后在read之后,通过读取cnt判断用户是否退出,向isQuit中写入信息:

最终实现的效果如下所示:

用户超时退出聊天

这里我们可以设置使用定时器来进行超时管理,如果60s内没有发送任何消息的情况下就直接将这个连接关闭掉:

// 启动一个go程,负责监听退出信号,通知所有go程退出
func watch(user *User, conn net.Conn, isQuit chan bool, resTimer chan bool) {
	fmt.Println("启动用户", user.name, "的watch go程...")
	defer fmt.Println("watch程序结束...") // 程序结束,关闭监听go程
	for {
		select {
		case <-isQuit: // 收到退出信号,通知所有go程退出
			delete(allUsers, user.id)
			fmt.Println("删除当前用户:", user.name)
			message <- fmt.Sprintf("[%s][%s]下线了\n", user.id, user.name)
			_ = conn.Close()
			return
		case <-time.After(10 * time.Second):
			fmt.Println("删除当前用户:", user.name)
			delete(allUsers, user.id)
			message <- fmt.Sprintf("[%s]用户超时下线了\n", user.name)
			_ = conn.Close()
			return
		case <-resTimer:
			fmt.Printf("连接%s 重置计数器!\n", user.name)
		}
	}
}

这里我们定义一个重置的管道,只要用户不断输入就不会超时,如果用户没有输入超过10s就会触发超时退出的操作:

// 创建一个用于重置计算器的管道,用于告知watch函数当前用户正在输入
var resTimer = make(chan bool)
// 启动go程,负责监听用户退出
go watch(&newUser, conn, isQuit, resTimer)

完整代码如下所示:

package main

import (
	"fmt"
	"net"
	"strings"
	"time"
)

// User 定义用户结构体
type User struct {
	id   string
	name string
	msg  chan string
}

// 创建全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

// 定义一个message全局通道,用于接收任何人发送过来的消息
var message = make(chan string, 10)

func main() {
	// 01 创建服务器
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("net.listen err:", err)
		return
	} else {
		fmt.Println("服务器启动成功...")
		// 启动全局唯一go程,用于广播消息
		go broadcast()
	}
	for {
		fmt.Println("主go程监听中...")
		// 02 监听服务器
		connect, err := listener.Accept()
		if err != nil {
			fmt.Println("listener.accept err:", err)
			return
		}
		fmt.Println("建立连接成功...")
		// 03 启动处理业务的go程
		go handler(connect)
	}

}

func handler(conn net.Conn) {
	fmt.Println("启动处理业务")
	// 客户端与服务器建立连接的时候,会有ip与port,可以当成user的id
	clientAddr := conn.RemoteAddr().String()
	fmt.Println("客户端地址为:", clientAddr)
	// 创建User
	newUser := User{
		id:   clientAddr,            // id,不会被修改,作为mao中的key
		name: clientAddr,            // 可以修改,会提供rename命令修改,建立连接时初始值与id相同
		msg:  make(chan string, 10), // 消息通道,注意分配空间
	}
	// 添加user到map结构中
	allUsers[newUser.id] = newUser
	// 定义一个退出信号,用于通知所有go程退出
	var isQuit = make(chan bool)
	// 创建一个用于重置计算器的管道,用于告知watch函数当前用户正在输入
	var resTimer = make(chan bool)
	// 启动go程,负责监听用户退出
	go watch(&newUser, conn, isQuit, resTimer)
	// 启动用户自己的writeBackToClient go程
	go writeBackToClient(&newUser, conn)
	// 向message写入消息,用于通知所有人有用户上线
	message <- fmt.Sprintf("[%s][%s]上线了", newUser.id, newUser.name)
	for {
		buf := make([]byte, 1024)
		// 读取客户端发送的数据
		cnt, err := conn.Read(buf)
		if cnt == 0 {
			fmt.Println("客户端主动关闭ctrl+c,准备退出")
			// 在这里不进行真正的退出动作,只是通知所有go程退出
			isQuit <- true
		}
		if err != nil {
			fmt.Println("conn.read err:", err, "cnt", cnt)
			return
		} else {
			fmt.Println("服务器接收客户端发送过来的数据为:", string(buf[:cnt-1]), "cnt:", cnt)
			// -------业务逻辑处理开始-------
			// 01 查询当前所有的用户 who
			if len(buf[:cnt-1]) == 4 && string(buf[:cnt-1]) == "\\who" {
				var userInfos []string
				// 遍历map结构,获取所有的用户信息
				for _, user := range allUsers {
					userInfo := fmt.Sprintf("userid:%s, username: %s", user.id, user.name)
					userInfos = append(userInfos, userInfo)
				}
				// 最终写到管道中
				newUser.msg <- strings.Join(userInfos, "\n")
			} else if len(buf[:cnt-1]) > 9 && string(buf[:7]) == "\\rename" {
				// 更新名字
				newUser.name = strings.Split(string(buf[:cnt-1]), "|")[1]
				allUsers[newUser.id] = newUser // 更新map结构中的用户信息
				// 通知客户端更新成功
				newUser.msg <- fmt.Sprintf("改名成功, 新的名字为: %s", newUser.name)
			} else {
				message <- string(buf[:cnt-1])
			}
			resTimer <- true // 发送一个信号,告知watch函数当前用户正在输入
			// -------业务逻辑处理结束-------
		}
	}
}

// 向所有的用户广播消息,启动全局唯一go程
func broadcast() {
	fmt.Println("启动广播go程...")
	defer fmt.Println("broadcast程序结束...") // 程序结束,关闭广播go程
	for {
		fmt.Println("广播go程监听中...")
		// 01 从message通道中读取消息
		info := <-message
		fmt.Println("广播消息为:", info)
		// 02 遍历map结构,向每个用户发送消息
		for _, user := range allUsers {
			// 03 向每个用户发送消息
			user.msg <- info
		}
	}
}

// 每个用户监听自己的msg通道,负责将数据返回给客户端
func writeBackToClient(user *User, conn net.Conn) {
	fmt.Println("启动用户", user.name, "的writeBackToClient go程...")
	for data := range user.msg {
		fmt.Printf("user: %s 写回给客户端的数据为: %s\n", user.name, data)
		_, _ = conn.Write([]byte(data))
	}
}

// 启动一个go程,负责监听退出信号,通知所有go程退出
func watch(user *User, conn net.Conn, isQuit chan bool, resTimer chan bool) {
	fmt.Println("启动用户", user.name, "的watch go程...")
	defer fmt.Println("watch程序结束...") // 程序结束,关闭监听go程
	for {
		select {
		case <-isQuit: // 收到退出信号,通知所有go程退出
			delete(allUsers, user.id)
			fmt.Println("删除当前用户:", user.name)
			message <- fmt.Sprintf("[%s][%s]下线了\n", user.id, user.name)
			_ = conn.Close()
			return
		case <-time.After(10 * time.Second):
			fmt.Println("删除当前用户:", user.name)
			delete(allUsers, user.id)
			message <- fmt.Sprintf("[%s]用户超时下线了\n", user.name)
			_ = conn.Close()
			return
		case <-resTimer:
			fmt.Printf("连接%s 重置计数器!\n", user.name)
		}
	}
}