【启程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)}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/463539.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无人机场景 - 目标检测数据集 - 夜间车辆检测数据集下载「包含VOC、COCO、YOLO三种格式」

数据集介绍&#xff1a;无人机场景夜间车辆检测数据集&#xff0c;真实场景高质量图片数据&#xff0c;涉及场景丰富&#xff0c;比如夜间无人机场景城市道路行驶车辆图片、夜间无人机场景城市道边停车车辆图片、夜间无人机场景停车场车辆图片、夜间无人机场景小区车辆图片、夜…

HTML学习笔记十

系列笔记目录 第一章 HTML的概述 第二章 URL简介 第三章 网页元素的属性 第四章 html字符编码 第五章 网页的语义结构 第六章 文本标签 第七章 列表标签 第八章 图像标签 第九章 链接标签 第十章 多媒体标签 多媒体标签 系列笔记目录前言一、简介二、常用标签2.1<video>2…

Thumb 汇编指令集,Thumb 指令编码方式,编译 Thumb 汇编代码

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ Thumb指令集 ARM 指令集&#xff1a;最早在 1985 年随第一代 ARM 处理器问世。ARM 指令集一开始是 32 位固定长度的指令&#xff0c;用于各种计算任务。 Thu…

【Clikhouse 探秘】ClickHouse 物化视图:加速大数据分析的新利器

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…

【HarmonyOS NEXT】在 HarmonyOS NEXT 中实现优雅的加载动画

【HarmonyOS NEXT】在 HarmonyOS NEXT 中实现优雅的加载动画 在移动应用开发中&#xff0c;加载动画是提升用户体验的重要工具。在应用程序处理数据或加载页面时&#xff0c;为用户提供视觉反馈尤为关键。在这篇博客中&#xff0c;我们将探讨如何在 HarmonyOS NEXT 中使用 Sta…

Redis高级篇之缓存一致性详细教程

文章目录 0 前言1.缓存双写一致性的理解1.1 缓存按照操作来分 2. 数据库和缓存一致性的几种更新策略2.1 可以停机的情况2.2 我们讨论4种更新策略2.3 解决方案 总结 0 前言 缓存一致性问题在工作中绝对没办法回避的问题&#xff0c;比如&#xff1a;在实际开发过程中&#xff0c…

C++_day2

目录 1. 引用 reference&#xff08;重点&#xff09; 1.1 基础使用 1.2 特性 1.3 引用参数 2. C窄化&#xff08;了解&#xff09; 3. 输入&#xff08;熟悉&#xff09; 4. string 字符串类&#xff08;掌握&#xff09; 4.1 基础使用 4.2 取出元素 4.3 字符串与数字转换 5. …

Vuex的基本使用

文章目录 一、Vuex概述1.是什么2.使用场景3.优势4.注意二、如何构建vuex多组件共享数据环境1.创建项目2.创建三个组件3.源代码三、vuex 的使用 - 创建仓库1.安装 vuex2.新建 `store/index.js` 专门存放 vuex3.创建仓库 `store/index.js`4 在 main.js 中导入挂载到 Vue 实例上5.…

WPF+MVVM案例实战(二十一)- 制作一个侧边弹窗栏(CD类)

文章目录 1、案例效果1、侧边栏分类2、CD类侧边弹窗实现1、样式代码实现2、功能代码实现3 运行效果4、源代码获取1、案例效果 1、侧边栏分类 A类 :左侧弹出侧边栏B类 :右侧弹出侧边栏C类 :顶部弹出侧边栏D类 :底部弹出侧边栏2、CD类侧边弹窗实现 1、样式代码实现 在原有的…

揭开广告引擎的神秘面纱:如何在0.1秒内精准匹配用户需求?

目录 一、广告系统与广告引擎介绍 &#xff08;一&#xff09;广告系统与广告粗分 &#xff08;二&#xff09;广告引擎在广告系统中的重要性分析 二、广告引擎整体架构和工作过程 &#xff08;一&#xff09;一般概述 &#xff08;二&#xff09;核心功能架构图 三、标…

[论文阅读]A Survey of Embodied Learning for Object-Centric Robotic Manipulation

Abstract --以对象为中心的机器人操纵的Embodied learning是体现人工智能中一个快速发展且具有挑战性的领域。它对于推进下一代智能机器人至关重要&#xff0c;最近引起了人们的极大兴趣。与数据驱动的机器学习方法不同&#xff0c;具身学习侧重于通过与环境的物理交互和感知反…

NFTScan Site:以蓝标认证与高级项目管理功能赋能 NFT 项目

自 NFTScan Site 上线以来&#xff0c;它迅速成为 NFT 市场中的一支重要力量&#xff0c;凭借对各类 NFT 集合、市场以及 NFTfi 项目的认证获得了广泛认可。这个平台帮助许多项目提升了曝光度和可见性&#xff0c;为它们在竞争激烈的 NFT 市场中创造了更大的成功机会。 在最新更…

指数分布的原理和应用

本文介绍指数分布&#xff0c;及其推导原理。 Ref: 指数分布 开始之前&#xff0c;先看个概率密度函数的小问题&#xff1a; 问题描述&#xff1a;你于上午10点到达车站&#xff0c;车在10点到10:30 之间到达的时刻 X 的概率密度函数如图&#xff1a; 则使用分段积分&#xff0…

Javase——正则表达式

正则表达式的相关使用 public static void main(String[] args) {//校验QQ号 System.out.println("3602222222".matches("[1-9][0-9]{4,}"));// 校验18位身份证号 System.out.println("11050220240830901X".matches("^([0-9]){7,18}…

安装中文版 Matlab R2022a

下载安装包 压缩包有点大&#xff0c;大概20G 百度网盘&#xff1a;下载链接 提取码&#xff1a;rmja 安装 解压后打开目录&#xff0c;右键以管理员身份运行 setup.exe 选择输入安装秘钥 输入秘钥&#xff1a; 50874-33247-14209-37962-45495-25133-28159-33348-18070-6088…

SICTF Round #4|MISC

1.派森 腐乳昂木 奥普瑞特儿 阴坡尔特 艾克斯奥尔 腐乳昂木 提克有第爱慕 阴坡尔特 ⭐ 弗拉格 等于 布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉布拉 印刻 等于 左中括号右中括号 佛儿 唉 因 梯软者左括号 零&#xff0c;楞左括号弗拉格右…

保研考研机试攻略:python笔记(2)

&#x1f428;&#x1f428;&#x1f428;宝子们好呀&#xff0c;今天我们继续来学习N诺提供的python笔记&#xff0c;fighting&#xff01;( •̀ ω •́ )✧ 对这个系列感兴趣的宝子欢迎关注保研考研机试攻略专栏哦 ~ 目录 &#x1f428;&#x1f428;&#x1f428;4进制转…

Hyper-V 安装 KylinOS V10【图文教程】

文章目录 下载 KylinOSHyper-V 安装 KylinOS新建虚拟机配置虚拟机启动虚拟机并配置下载 KylinOS KylinOS 没有直接提供下载地址,需要在页面上点试用,填写个人信息后,才能看到下载地址。 https://www.kylinos.cn/support/trial.html?trial=425887 试用地址:产品试用申请国…

LeetCode 0685.冗余连接 II:并查集(和I有何不同分析)——详细题解(附图)

【LetMeFly】685.冗余连接 II&#xff1a;并查集&#xff08;和I有何不同分析&#xff09;——详细题解(附图) 力扣题目链接&#xff1a;https://leetcode.cn/problems/redundant-connection-ii/ 在本问题中&#xff0c;有根树指满足以下条件的 有向 图。该树只有一个根节点&…

mysql查表相关练习

作业要求&#xff1a; 单表练习&#xff1a; 1 . 查询出部门编号为 D2019060011 的所有员工 2 . 所有财务总监的姓名、编号和部门编号。 3 . 找出奖金高于工资的员工。 4 . 找出奖金高于工资 40% 的员工。 5 找出部门编号为 D2019090011 中所有财务总监&#xff0c;和…