Go项目实战:01-聊天室+map竞争需要上锁

news2024/11/24 2:12:40

实现一个聊天室(群):

功能分析:

  • 1、上线下线
  • 2、聊天:其他人和自己都可以看到聊天消息
  • 3、查询当前的聊天室用户所有人的名字
  • 4、可以修改自己的名字
  • 5、超时潜水踢出机制

技术点分析:

  • 1、socket tcp编程
  • 2、map结构,存储所有的用户
    • 1、存储所有的用户
    • 2、map遍历
    • 3、map删除
  • 3、go 程、channel
  • 4、select (超时退出、主动退出)
  • 5、timer 定时器

效果展示

请添加图片描述

具体实现

1、tcp socket,建立多个连接

package main

import (
	"fmt"
	"net"
)

//课程将所有代码写在一个文件中,没有做代码整理了
func main() {
	//创建服务器
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	fmt.Println("服务器启动成功,监听中...")

	for {
		fmt.Println("====>主go程监听中...")
		//监听
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener.Accept err:", err)
			return
		}
		//建立连接
		fmt.Println("建立连接成功")

		//启动处理业务的go程
		go handler(conn)
	}

}

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
		}

		fmt.Println("客户端发送过来的数据为:", string(buf[:cnt-1]), ",cnt:", cnt)
	}
}

2、思路分析

思路分析图

请添加图片描述

数据流分析

请添加图片描述

3、定义User/map结构

type User struct {
	//名字
	name string
	//唯一的id
	id string
	//管道
	msg chan string
}

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

在handler中调用

请添加图片描述

4、定义message通道

请添加图片描述

创建监听广播go程函数
请添加图片描述

启动:读全局唯一
请添加图片描述

写入上线数据
请添加图片描述

bug修复
请添加图片描述
效果:
请添加图片描述

5、user监听通道go程

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

func writeBackToClient(user *User, conn net.Conn){
	//TODO
	fmt.Printf("user : %s 的go程正在监听自己的msg管道:\n", user.name)
	for data := range user.msg{
		fmt.Printf("user: %s写回给客户端的数据为:%s \n", user.name, data)
		//Write(b []byte) (n int,err error)
		_, _ = conn.Write([]byte(data))
	}
}

请添加图片描述
请添加图片描述

代码

package main

import (
	"fmt"
	"net"
)

//课程将所有代码写在一个文件中,没有做代码整理了

type User struct {
	//名字
	name string
	//唯一的id
	id string
	//管道
	msg chan string
}

//创建一个全局的map结构,用户保存所有的用户
var allUsers = make(map[string]User)
//定义一个message全局通道,用于接收任何人发送过来的消息
var message = make(chan string, 10)

func main() {
	//创建服务器
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	//启动全局唯一的go程,负责监听message通道,写给所有的用户
	go broadcast()

	fmt.Println("服务器启动成功,监听中...")

	for {
		fmt.Println("====>主go程监听中...")
		//监听
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener.Accept err:", err)
			return
		}
		//建立连接
		fmt.Println("建立连接成功")

		//启动处理业务的go程
		go handler(conn)
	}

}

func handler(conn net.Conn) {

		fmt.Println("启动业务...")

		clientAddr := conn.RemoteAddr().String()
		fmt.Println("clientAddr:",clientAddr)
		//创建user
		newUser := User{
			id: clientAddr, //id,我们不会修改。这个作为map中的key
			name: clientAddr,//可以修改,会提供rename命令修改,建立连接时,初始值与id相同
			msg: make(chan string, 10),//注意需要make空间,否则无法导入数据
		}
		//添加user到map结构
		allUsers[newUser.id] = newUser

		//启动go程,负责将msg心息返回给客户端
		go writeBackToClient(&newUser, conn)

		//向message写入数据。当前用户上线的消息,用于通知所有人(广播)
		loginInfo:= fmt.Sprintf("[%s]:[%s] ===> 上线了login!", newUser.id, newUser.name)//字符串拼接
		message <- loginInfo

		for {
		//具体业务逻辑
		buf := make([]byte, 1024)

		//读取客户端发送过来的请求数据
		cnt, err := conn.Read(buf)
		if err != nil{
			fmt.Println("conn.Read err:", err)
			return
		}

		fmt.Println("服务器接收客户端发送过来的数据为:", string(buf[:cnt-1]), ",cnt:", cnt)
	}
}

//向所有用户广播消息,启动一个全局唯一的go程
func broadcast() {
	 fmt.Println("广播go程启动成功...")
	 defer fmt.Println("broadcast程序退出!")

	 for{
	 	 //1、从message中读取数据
		 fmt.Println("broadcast监听message中...")
		 info := <- message
		 //2、将数据写入到每一个用户的msg管道中
		 for _, user := range allUsers{
			 //如果msg是非缓冲的,将会在这里阻塞
			 user.msg <- info
		 }
	 }
}
//每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端
func writeBackToClient(user *User, conn net.Conn){
	//TODO
	fmt.Printf("user : %s 的go程正在监听自己的msg管道:\n", user.name)
	for data := range user.msg{
		fmt.Printf("user: %s写回给客户端的数据为:%s \n", user.name, data)
		//Write(b []byte) (n int,err error)
		_, _ = conn.Write([]byte(data))
	}
}

增加功能

1、查询用户

查询命令:who ==》将当前所有的登录的用户,展示出来 ,id、name、返回给当前用户
在handler中,处理业务逻辑:

fmt.Println("服务器接收客户端发送过来的数据为:", string(buf[:cnt-1]), ",cnt:", cnt)

			//----业务逻辑处理   开始 -------
			//1、查询当前所有的用户 who
			//	a、先判断接收的数据是不是who ==》长度&&字符串
			userInput := string(buf[:cnt-1]) // 这是用户输入的数据,最后一个是回车,我们去掉它
			if len(userInput) == 3 && userInput == "who" {
				//	b、遍历allUsers这个map :key := user   id value:user本身,将id和name拼接程一个字符串,返回给客户端
				fmt.Println("用户即将查询所有用户信息!")

				//这个切片包含所有的用户信息
				var userInfotable []string
				//[]string{"userid:z3, username:z3","userid:l4,username:l4","userid:w5,username:w5"}
				for _, user := range allUsers {
					userInfo := fmt.Sprintf("userid:%s,username:%s", user.id, user.name)
					userInfotable = append(userInfotable, userInfo)
				}

				//最终写道管道中,一定是一个字符串,现在相当于是n个字符串
				r := strings.Join(userInfotable,"\n")//链接数组切片,生成字符串
				//strings.Split()  分割字符串
				//`
				// "userid:z3, username:z3",
				// "userid:l4,username:l4",
				// "userid:w5,username:w5"
				//`
				newUser.msg <- r

			}else{
				//如果用户输入的不是命令知识不普通的聊天信息,则只需要写到广播通道即可,由其他的go程进行处理,进行常规转发
				message <- userInput
			}
			

效果:请添加图片描述

2、重命名

规则: rename|Duke

  • 1、读取数据判断长度是否>8,并且判断字符是否为rename
  • 2、使用|进行分割,获取|后面的部分,作为名字
  • 3、update 用户名字 newUser = Duike
  • 4、通知客户端,更新成功
else if len(userInput) > 9 && userInput[:7] == "\\rename" {
				//[:3] //0, 1, 2 ==> 左闭右开


				//规则: rename|Duke
				//- 1、读取数据判断长度是否>8,并且判断字符是否为rename

			    //- 2、使用|进行分割,获取|后面的部分,作为名字

				//arry := strings.Split(userInput,"|")
			    //name := array[1]

				//- 3、update 用户名字 newUser = Duke
				newUser.name = strings.Split(userInput,"|")[1]
				allUsers[newUser.id] = newUser //更新为map中的user
			    //- 4、通知客户端,更新成功
				newUser.msg <- "rename successfuly!"

			} 

效果:
请添加图片描述

3、主动退出

quit && contro + c

用户退出:清理工作
1、从map中删除
2、对应的conn要close

每个用户都有自己的watch go程,负责监听退出信号

//启动一个go程只负责用来监听退出信号isQuit,触发后,进行清零工作:delete map,close conn
func watch(user *User, conn net.Conn, isQuit <-chan bool) {
	fmt.Println("2222启动监听退出信号的go程")
	defer fmt.Println("watch go程退出!")
	for {
		select {
		case <- isQuit:
			logoutInfo := fmt.Sprintf("%s exit already", user.name)
			fmt.Println("删除当前用户",user.name)
			delete(allUsers, user.id)
			message <-logoutInfo
			conn.Close()
			return
		}
	}
}

在handle中启动go watch ,同时传入相应信息:

		//启动go程负责监听退出信号
		go watch(&newUser, conn, isQuit)

请添加图片描述

在read之后,通过读取的cnt判断用户退出,向isQuit中写入信号:
请添加图片描述

测试结果:
请添加图片描述

4、超时退出

使用定时器来进行超时管理

如果60秒内没有发送任何数据,那么直接将这个连接关闭:

<- time.After(60 *time.second) //chan time
func watch(user *User, conn net.Conn, isQuit, restTimer <- chan bool) {
	//func watch(user *User, conn net.Conn, isQuit <- chan bool, restTimer <- chan bool)   |   i, j int
	fmt.Println("2222启动监听退出信号的go程")
	defer fmt.Println("watch go程退出!")
	for {
		select {
		case <- isQuit:
			logoutInfo := fmt.Sprintf("%s exit already\n", user.name)
			fmt.Println("删除当前用户",user.name)
			delete(allUsers, user.id)
			message <-logoutInfo//消息告诉所有人

			conn.Close()
			return
		case <- time.After(10 *  time.Second):
			logoutInfo := fmt.Sprintf("%s timeout exit already\n", user.name)
			fmt.Println("删除当前用户",user.name)
			delete(allUsers, user.id)
			message <-logoutInfo//消息告诉所有人

			conn.Close()
			return
		case <- restTimer:
			fmt.Sprintf("连接%s,重置计数器\n", user.name)
		}
	}
}

创建并传入restTimer管道
请添加图片描述
请添加图片描述
效果:
请添加图片描述

map需要上锁

package main

import (
	"fmt"
	"sync"
	"time"
)

var idnames sync.Map
var lock sync.RWMutex

//map不允许同时读写,如果有不同go程同时操作map, 需要对map上锁
//concurrent map read and map write
func main() {
	go func() {
		for {
			//lock.Lock()
			fmt.Println("111111")
			idnames.Store("duke",0)
			fmt.Println("222222")
			//lock.Unlock()
		}
	}()
	go func() {
		for {
			//lock.Lock()
			fmt.Println("aaaaaa")
			name, _ := idnames.Load("duke")
			fmt.Println("name:", name)
			fmt.Println("bbbbbb")
			//lock.Unlock()
		}
	}()

	for {
		fmt.Println("OVER")
		time.Sleep(1 * time.Second)
	}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Adam算法及python实现

文章目录算法介绍代码实现结果展示参考算法介绍 Adam算法的发展经历了&#xff1a;SGD->SGDM->SGDNA->AdaGrad->AdaDelta->Adam->Adamax的过程。它是神经网络优化中的常用算法&#xff0c;在收敛速度上比较快&#xff0c;比SGD对收敛速度的纠结上有了很大的…

单商户商城系统功能拆解46—应用中心—足迹气泡

单商户商城系统&#xff0c;也称为B2C自营电商模式单店商城系统。可以快速帮助个人、机构和企业搭建自己的私域交易线上商城。 单商户商城系统完美契合私域流量变现闭环交易使用。通常拥有丰富的营销玩法&#xff0c;例如拼团&#xff0c;秒杀&#xff0c;砍价&#xff0c;包邮…

基于微信小程序的课程分享平台-计算机毕业设计

项目介绍 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用java语言技术和mysql数据库来完成对系统的设计。整…

[附源码]Node.js计算机毕业设计高校就业管理信息系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

学生竞赛网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 模块划分&#xff1a;通知类型、通知信息、学院信息、学生信息、学科信息、竞赛信息、报名信 息、成果上传、评分排名 管…

YOLOv5小目标切图检测

当我们在检测较大分辨率的图片时&#xff0c;对小目标的检测效果一直是较差的&#xff0c;所以就有了下面几种方法&#xff1a; 将图片压缩成大尺寸进行训练&#xff08; 想法&#xff1a;没显存&#xff0c;搞不来&#xff09;添加小检测头&#xff08;想法&#xff1a;P5模型…

【爬虫实战项目】Python爬虫批量下载相亲网站数据并保存本地(附源码)

前言 今天给大家介绍的是Python爬虫批量下载相亲网站图片数据&#xff0c;在这里给需要的小伙伴们代码&#xff0c;并且给出一点小心得。 首先是爬取之前应该尽可能伪装成浏览器而不被识别出来是爬虫&#xff0c;基本的是加请求头&#xff0c;但是这样的纯文本数据爬取的人会…

数据结构---树和二叉树

树和二叉树定义二叉树二叉树的物理结构链式存储数组二叉树应用查找维持相对顺序二叉树的遍历深度优先遍历前序遍历中序遍历后序遍历二叉树广度优先遍历层序遍历定义 有且仅有一个特定的称为根的节点。当n>1时&#xff0c;其余节点可分为m&#xff08;m>0&#xff09;个互…

数据结构与算法——Java实现栈、逆波兰计算器(整数加减乘除)

目录 一、栈 1.1 基本介绍 1.2 栈的思路分析 1.3 栈的代码实现 二、栈实现综合计算器 2.1 思路分析 2.2 代码实现&#xff08;中缀表达式实现&#xff09; 三、栈的前缀&#xff08;波兰&#xff09;、中缀、后缀&#xff08;逆波兰&#xff09;表达式 3.1 表达式的介绍…

访问pcie总线地址内容

调用代码如下&#xff1a; uint32_t value;void * addr;printk("------1--------\n");addr0x2730000;struct resource *res;char const *name dev_name(&pdev->dev);printk("dev_name%s\n", name);res request_mem_region(addr, 16, "name1&…

【语音之家公开课】SRD: A Dataset and Benchmark Perspective

本次语音之家公开课邀请到陈果果进行分享Speech Recognition Development: A Dataset and Benchmark Perspective。 公开课简介 主题&#xff1a;Speech Recognition Development: A Dataset and Benchmark Perspective 时间&#xff1a;12月15日&#xff08;周四&#xff09…

web网页设计期末课程大作业:美食餐饮文化主题网站设计——HTML+CSS+JavaScript美食餐厅网站设计与实现 11页面

&#x1f468;‍&#x1f393;静态网站的编写主要是用HTML DIVCSS JS等来完成页面的排版设计&#x1f469;‍&#x1f393;,常用的网页设计软件有Dreamweaver、EditPlus、HBuilderX、VScode 、Webstorm、Animate等等&#xff0c;用的最多的还是DW&#xff0c;当然不同软件写出的…

C# IO及文件管理

一 System.IO ① System.IO名字空间&#xff1b; ② 提供了许多用于&#xff1b; ③ 文件和数据流进行读写操作的类&#xff1b; 二 流的分类 1 Stream类 按存取位置分&#xff1a;FileStream,MemeryStream,BufferedStream; 2 读写类 BinaryReader和BinaryWriter; TextRe…

从 0 到 1 搞一个 Compose Desktop 版本的玩天气之打包

从 0 到 1 搞一个 Compose Desktop 版本的玩天气之打包 大家好&#xff0c;前两篇文章大概介绍了下上手 Compose Desktop 和自定义绘制时遇到的一些问题&#xff0c;项目的最终实现效果如下&#xff1a; 视频代码写好了&#xff0c;该弄的动画也弄了&#xff0c;该请求的网络数…

【数据结构】八大排序算法详解

&#x1f9d1;‍&#x1f4bb;作者&#xff1a; 情话0.0 &#x1f4dd;专栏&#xff1a;《数据结构》 &#x1f466;个人简介&#xff1a;一名双非编程菜鸟&#xff0c;在这里分享自己的编程学习笔记&#xff0c;欢迎大家的指正与点赞&#xff0c;谢谢&#xff01; 排序前言一…

汇编语言第一章:基础知识

1. 基础知识 机器语言 机器语言是机器指令的集合&#xff0c;是一台机器可以正确执行的命令。现在一般电子计算机的机器指令是一列二进制数字。机器指令集是机器语言。 汇编语言 机器语言难以辨别和记忆&#xff0c;所以产生了汇编语言。汇编语言的主体是汇编指令。 操作&…

on-device training

又搬来个好玩呃 说来又想试试了 , 仅用256KB就实现单片机上的神经网络训练&#xff08;training,notinference&#xff09;&#xff0c;从此终端智能不再是单纯的推理&#xff0c;而是能持续的自我学习自我进化 On-Device Training under 256KB Memory 说到神经网络训练&#…

编译原理实验四

编译原理实验四 实验要求 cminus-f的词法分析和语法分析部分已经完成&#xff0c;最终得到的是语法分析树。而为了产生目标代码&#xff0c;还需要将语法分析树转为抽象语法树&#xff0c;通过抽象语法分析树生成中间代码(即IR)&#xff0c;最后使用中间代码来进行优化并生成…

easyExcel导出表头合并 不得不说真牛

有个导出单元格合并的任务&#xff0c;表头不规则合并格式&#xff0c;看得就烦&#xff0c;尤其是对于没玩儿过合并的我来说&#xff0c;任务放在哪里不知咋做&#xff0c;网上也看了一堆合并的方法&#xff0c;自己写注解来写的那些&#xff0c;麻烦得要命&#xff0c;我写一…

48.python break语句-终止循环

48.break语句-终止循环 文章目录48.break语句-终止循环1.循环控制2.break的作用3. 语法4. 实操练习5. 知识扩展&#xff1a;print的位置6. break语句循环图1.循环控制 在循环的过程中如果要退出循环&#xff0c;我们可以用break语句和continue语句。 2.break的作用 break [b…