【启程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的世界!在当今快节奏的软件开发领域,选择一种高效、简洁的编程语言至关重要。而在这方面,Golang(又称Go)无疑是一个备受瞩目的选择。在本文中,带领您探索Golang的世界,一步步地了…...
微信小程序的开发流程
微信小程序开发流程 1. 注册微信小程序账号 进入微信公众平台(mp.weixin.qq.com),选择小程序的账号类型按照流程进行注册。注意每个邮箱只能注册一个账号。 2. 下载开发工具 使用账号登录微信公众平台,在开发->开发设置-&g…...
十分钟快速让你搞懂 Vue3 和 React 的区别
前言 Vue 3和 React是市面上目前非常受欢迎的两个前端框架。它们都采用了组件化的开发模式,使得开发者可以将复杂的应用拆分为多个小组件进行开发,从而提高了代码的可维护性和重用性。然而,虽然Vue 3和React都拥有各自的优点,但它…...

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

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

使用Git LFS管理大型文件
💓 博客主页:瑕疵的CSDN主页 📝 Gitee主页:瑕疵的gitee主页 ⏩ 文章专栏:《热点资讯》 使用Git LFS管理大型文件 引言 Git LFS 简介 安装 Git LFS 安装 Git 安装 Git LFS 配置 Git LFS 初始化 Git 仓库 指定需要使用…...
OpenAI终于正式上线搜索功能,搜索行业要变天了?
OpenAI 的 AI 搜索功能也将引发一场激烈的竞争。 各大科技公司都不会坐视不理,他们必然会纷纷加大对 AI 搜索技术的研发投入,试图在这个新兴的领域分一杯羹。这就像是一场没有硝烟的战争,各方势力都在暗中较劲,谁能笑到最后&…...

ssm《数据库系统原理》课程平台的设计与实现+vue
系统包含:源码论文 所用技术:SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习,获取源码看文章最下面 需要定制看文章最下面 目 录 目 录 I 摘 要 III ABSTRACT IV 1 绪论 1 1.1 课题背景 1 1.2 研究现状 1 1.3 研究内容…...

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

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

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

前端埋点系统之如何用heatmap.js画网页热力图
Hello,大家好。在当今数字化时代,理解用户行为成为了企业成功的关键之一。随着互联网的发展,用户与网站、应用和产品的互动变得愈发复杂而多样化。在这样的背景下,埋点系统成为了洞察用户行为的重要工具之一。而其中的热力图分析&…...
CentOS 7系统下Redis Cluster集群一键部署脚本发布
引言 在大数据和云计算时代,Redis作为一款高性能的键值存储数据库,广泛应用于各种场景。然而,手动搭建Redis Cluster集群过程繁琐且容易出错。为了简化这一过程,本文提供了一个在CentOS 7系统下Redis Cluster集群的一键部署脚本,帮助开发者快速搭建Redis Cluster集群。 …...

自编以e为底的对数函数ln,性能接近标准库函数
算法描述: (1). 先做自变量x的范围检查,不能出现负数和0. 自己使用时,如果能通过其它途径保证自变量为正,那么可以省略这两个判断,提高速度。 (2). 根据IEEE 754浮点数的格式,,则 ln(x)kln(2)ln…...
Java中的日期时间
JDK8之前常用的日期时间类 System.currentTimeMillis():获取当前毫秒数(long类型) java.util.Date:通用Date类 import java.util.Date;Date date new Date(); // 空参构造器 System.out.println(date.getTime()); // 获取当前时…...
位置编码的表示
位置编码的表示位置编码的表示位置编码的表示位置编码的表示位置编码的表示...

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

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

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 及其列已完全初始化时,才使用以下方法。如果需要在表单仍…...

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

[yolov11改进系列]基于yolov11引入特征融合注意网络FFA-Net的python源码+训练源码
【FFA-Net介绍】 北大和北航联合提出的FFA-net: Feature Fusion Attention Network for Single Image Dehazing图像增强去雾网络,该网络的主要思想是利用特征融合注意力网络(Feature Fusion Attention Network)直接恢复无雾图像,…...

关于神经网络中的激活函数
这篇博客主要介绍一下神经网络中的激活函数以及为什么要存在激活函数。 首先,我先做一个简单的类比:激活函数的作用就像给神经网络里的 “数字信号” 加了一个 “智能阀门”,让机器能学会像人类一样思考复杂问题。 没有激活i函数的神经网络…...

通信算法之280:无人机侦测模块知识框架思维导图
1. 无人机侦测模块知识框架思维导图, 见文末章节。 2. OFDM参数估计,基于循环自相关特性。 3. 无人机其它参数估计...

【深度学习】12. VIT与GPT 模型与语言生成:从 GPT-1 到 GPT4
VIT与GPT 模型与语言生成:从 GPT-1 到 GPT4 本教程将介绍 GPT 系列模型的发展历程、结构原理、训练方式以及人类反馈强化学习(RLHF)对生成对齐的改进。内容涵盖 GPT-1、GPT-2、GPT-3、GPT-3.5(InstructGPT)、ChatGPT …...
HTTPS加密通信详解及在Spring Boot中的实现
HTTPS(Hyper Text Transfer Protocol Secure)是HTTP的安全版本,通过SSL/TLS协议为通讯提供加密、身份验证和数据完整性保护。 一、HTTPS核心原理 1.加密流程概述 客户端发起HTTPS请求(连接到服务器443端口)服务器返…...
Vue3.5 企业级管理系统实战(二十一):菜单权限
有了菜单及角色管理后,我们还需要根据用户访问的token,去获取用户信息,根据用户的角色信息,拉取所有的菜单权限,进而生成左侧菜单树数据。 1 增加获取用户信息 api 在 src/api/user.ts 中,添加获取用户信…...

电脑驱动程序更新工具, 3DP Chip 中文绿色版,一键更新驱动!
介绍 3DP Chip 是一款免费的驱动程序更新工具,可以帮助用户快速、方便地识别和更新计算机硬件驱动程序。 驱动程序更新工具下载 https://pan.quark.cn/s/98895d47f57c 软件截图 软件特点 简单易用:用户界面简洁明了,操作方便,…...
python学习打卡day40
DAY 40 训练和测试的规范写法 知识点回顾: 彩色和灰度图片测试和训练的规范写法:封装在函数中展平操作:除第一个维度batchsize外全部展平dropout操作:训练阶段随机丢弃神经元,测试阶段eval模式关闭dropout 作业&#…...

2024年09月 C/C++(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
C/C++编程(1~8级)全部真题・点这里 第1题:有几个PAT 字符串 APPAPT 中包含了两个单词 PAT,其中第一个 PAT 是第 2 位,第 4 位(A),第 6 位(T);第二个 PAT 是第 3 位,第 4 位(A),第 6 位(T)。 现给定字符串,问一共可以形成多少个 PAT? 时间限制:1000 内存限制:26214…...

【案例分享】蓝牙红外线影音遥控键盘:瑞昱RTL8752CJF
蓝牙红外线影音遥控键盘 Remotec的无线控制键盘采用瑞昱蓝牙RTL8752CJF解决方案,透过蓝牙5.0与手机配对后,连线至 Remotec 红外 code server 取得对应影音视觉设备的红外 code后,即可控制多达2个以上的影音视觉设备,像是智能电视…...