当前位置: 首页 > news >正文

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

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

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

目录

socket-server建立

创建msg广播通道

查询用户与重命名

用户主动退出聊天

用户超时退出聊天


socket-server建立

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

package mainimport ("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   stringname stringmsg  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()returncase <-time.After(10 * time.Second):fmt.Println("删除当前用户:", user.name)delete(allUsers, user.id)message <- fmt.Sprintf("[%s]用户超时下线了\n", user.name)_ = conn.Close()returncase <-resTimer:fmt.Printf("连接%s 重置计数器!\n", user.name)}}
}

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

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

完整代码如下所示:

package mainimport ("fmt""net""strings""time"
)// User 定义用户结构体
type User struct {id   stringname stringmsg  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的idclientAddr := conn.RemoteAddr().String()fmt.Println("客户端地址为:", clientAddr)// 创建UsernewUser := User{id:   clientAddr,            // id,不会被修改,作为mao中的keyname: 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 查询当前所有的用户 whoif 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 := <-messagefmt.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()returncase <-time.After(10 * time.Second):fmt.Println("删除当前用户:", user.name)delete(allUsers, user.id)message <- fmt.Sprintf("[%s]用户超时下线了\n", user.name)_ = conn.Close()returncase <-resTimer:fmt.Printf("连接%s 重置计数器!\n", user.name)}}
}

相关文章:

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

欢迎来到Golang的世界&#xff01;在当今快节奏的软件开发领域&#xff0c;选择一种高效、简洁的编程语言至关重要。而在这方面&#xff0c;Golang&#xff08;又称Go&#xff09;无疑是一个备受瞩目的选择。在本文中&#xff0c;带领您探索Golang的世界&#xff0c;一步步地了…...

微信小程序的开发流程

微信小程序开发流程 1. 注册微信小程序账号 进入微信公众平台&#xff08;mp.weixin.qq.com&#xff09;&#xff0c;选择小程序的账号类型按照流程进行注册。注意每个邮箱只能注册一个账号。 2. 下载开发工具 使用账号登录微信公众平台&#xff0c;在开发->开发设置-&g…...

十分钟快速让你搞懂 Vue3 和 React 的区别

前言 Vue 3和 React是市面上目前非常受欢迎的两个前端框架。它们都采用了组件化的开发模式&#xff0c;使得开发者可以将复杂的应用拆分为多个小组件进行开发&#xff0c;从而提高了代码的可维护性和重用性。然而&#xff0c;虽然Vue 3和React都拥有各自的优点&#xff0c;但它…...

头歌——机器学习(线性回归)

文章目录 线性回归简述答案 线性回归算法答案 线性回归实践 - 波斯顿房价预测LinearRegression代码 利用sklearn构建线性回归模型示例代码如下&#xff1a; 代码 线性回归简述 简单线性回归 在生活中&#xff0c;我们常常能碰到这么一种情况&#xff0c;一个变量会跟着另一个变…...

AI驱动无人驾驶:安全与效率能否兼得?

内容概要 如今&#xff0c;人工智能正以其神奇的魔力驱动着无人驾驶的浪潮&#xff0c;带来了无数令人兴奋的可能性。这一领域的最新动态显示&#xff0c;AI技术在车辆的决策过程和实时数据分析中发挥着重要作用&#xff0c;帮助车辆更聪明地应对复杂的交通环境。通过实时监测…...

使用Git LFS管理大型文件

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Git LFS管理大型文件 引言 Git LFS 简介 安装 Git LFS 安装 Git 安装 Git LFS 配置 Git LFS 初始化 Git 仓库 指定需要使用…...

OpenAI终于正式上线搜索功能,搜索行业要变天了?

OpenAI 的 AI 搜索功能也将引发一场激烈的竞争。 各大科技公司都不会坐视不理&#xff0c;他们必然会纷纷加大对 AI 搜索技术的研发投入&#xff0c;试图在这个新兴的领域分一杯羹。这就像是一场没有硝烟的战争&#xff0c;各方势力都在暗中较劲&#xff0c;谁能笑到最后&…...

ssm《数据库系统原理》课程平台的设计与实现+vue

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码看文章最下面 需要定制看文章最下面 目 录 目 录 I 摘 要 III ABSTRACT IV 1 绪论 1 1.1 课题背景 1 1.2 研究现状 1 1.3 研究内容…...

Java SpringBoot调用大模型AI构建AI应用

本文是一个用springboot 结合spring mvc 和spring ai alibaba 调用国产大模型通义千问的具体例子&#xff0c;按照这个做能够快速的搞定Java应用的调用。 然后就可以把这类应用泛化到所有的涉及到非结构化数据结构化的场景中。 Spring AI&#xff1a;简化Java中大模型调用的框…...

MySQL【二】

查询列 SELECT [ALL | DISTINCT ] * | 列名1[,……列名n] FROM 表名; 查询所有选课学生的学号&#xff0c;结果去除重复值 select distinct sno from sc; 选择行 查询满足条件的数据集 SELECT 字段列表 FROM 表名 WHERE 查询条件 查询不属于数学系或外国语系的学生全部信息 …...

SQL 常用语句

目录 我的测试环境 学习文档 进入数据库 基础通关测验 语句-- 查 展示数据库&#xff1b; 进入某个数据库&#xff1b; 展示表&#xff1a; 展示某个表 desc 查询整个表&#xff1a; 查询特定列&#xff1a; 范围查询 等于特定值 不等于 介于 特定字符查询 Li…...

前端埋点系统之如何用heatmap.js画网页热力图

Hello&#xff0c;大家好。在当今数字化时代&#xff0c;理解用户行为成为了企业成功的关键之一。随着互联网的发展&#xff0c;用户与网站、应用和产品的互动变得愈发复杂而多样化。在这样的背景下&#xff0c;埋点系统成为了洞察用户行为的重要工具之一。而其中的热力图分析&…...

CentOS 7系统下Redis Cluster集群一键部署脚本发布

引言 在大数据和云计算时代,Redis作为一款高性能的键值存储数据库,广泛应用于各种场景。然而,手动搭建Redis Cluster集群过程繁琐且容易出错。为了简化这一过程,本文提供了一个在CentOS 7系统下Redis Cluster集群的一键部署脚本,帮助开发者快速搭建Redis Cluster集群。 …...

自编以e为底的对数函数ln,性能接近标准库函数

算法描述&#xff1a; (1). 先做自变量x的范围检查&#xff0c;不能出现负数和0. 自己使用时&#xff0c;如果能通过其它途径保证自变量为正&#xff0c;那么可以省略这两个判断&#xff0c;提高速度。 (2). 根据IEEE 754浮点数的格式&#xff0c;&#xff0c;则 ln(x)kln(2)ln…...

Java中的日期时间

JDK8之前常用的日期时间类 System.currentTimeMillis()&#xff1a;获取当前毫秒数&#xff08;long类型&#xff09; java.util.Date&#xff1a;通用Date类 import java.util.Date;Date date new Date(); // 空参构造器 System.out.println(date.getTime()); // 获取当前时…...

位置编码的表示

位置编码的表示位置编码的表示位置编码的表示位置编码的表示位置编码的表示...

0,国产FPGA(紫光同创)-新建PDS工程

国产FPGA正在蓬勃发展&#xff0c;紫光同创FPGA是大家竞赛时经常遇到的一款国产FPGA&#xff0c;本专栏从IP核开始一直到后续图像处理等。 开发板&#xff1a;盘古50K标准板 1&#xff0c;新建PDS工程 点击File&#xff08;1&#xff09;&#xff0c;然后是New Projects&#…...

c++联合

结构体与联合体的区别 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”&#xff0c;全面&#xff1b;缺点是struct内存空间的分配是粗放的&#xff0c;不管用不用&#xff0c;全分配。 而联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”&#xff…...

Edit Data. Create Cell Editors. Validate User Input 编辑数据。创建 Cell Editors。验证用户输入

Goto Data Grid 数据网格 Edit Data. Create Cell Editors. Validate User Input 编辑数据。创建 Cell Editors。验证用户输入 Get and Modify Cell Values in Code 在代码中获取和修改单元格值 仅当 Grid 及其列已完全初始化时&#xff0c;才使用以下方法。如果需要在表单仍…...

Java 文件操作与IO流

文件 文件有两个概念&#xff0c;在广义来看就是操作系统上对硬件和软件资源抽象为文件。 在侠义上来看&#xff0c;就是我们保存在硬盘上的文件 在这里我们讨论的是狭义的文件&#xff0c;在外面的硬盘上的文件细分又可以分为二进制文件和文本文件&#xff0c;文本文件可以通…...

MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例

一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

Java 8 Stream API 入门到实践详解

一、告别 for 循环&#xff01; 传统痛点&#xff1a; Java 8 之前&#xff0c;集合操作离不开冗长的 for 循环和匿名类。例如&#xff0c;过滤列表中的偶数&#xff1a; List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)

目录 一、&#x1f44b;&#x1f3fb;前言 二、&#x1f608;sinx波动的基本原理 三、&#x1f608;波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、&#x1f30a;波动优化…...

并发编程 - go版

1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...

Qemu arm操作系统开发环境

使用qemu虚拟arm硬件比较合适。 步骤如下&#xff1a; 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载&#xff0c;下载地址&#xff1a;https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)

一、OpenBCI_GUI 项目概述 &#xff08;一&#xff09;项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台&#xff0c;其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言&#xff0c;首次接触 OpenBCI 设备时&#xff0c;往…...

Unity UGUI Button事件流程

场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...

根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要

根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分&#xff1a; 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...