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

news2024/12/24 8:11:21

欢迎来到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)
		}
	}
}

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

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

相关文章

【JavaSE】java对象的比较

文章目录 元素的比较基本类型的比较对象的比较 如何进行对象比较重写equals方法基于Comparble.compareTo的比较基于Comparator.compare的比较区分Comparator和Comparable 在PriorityQueue中比较的体现 元素的比较 基本类型的比较 在Java中,基本类型可以直接进行大小的比较 //…

多SpringBoot项目同进程下统一启动

1.背景 使用SpringBoot技术栈进行REST HTTP接口开发服务时&#xff0c;一般来说如果模块较多或者涉及多人协作开发&#xff0c;大家会不自觉的将每个模块独立成一个单独的项目进行开发&#xff0c;部署时则将每个服务进行单独部署和运行。服务间的调用则通过FeignClients&…

lvgl

lvgl 目录 lvgl Lvgl移植到STM32 -- 1、下载LVGL源码 -- 2、将必要文件复制到工程目录 -- 3、修改配置文件 将lvgl与底层屏幕结合到一块 -- lvgl也需要有定时器,专门给自己做了一个函数,告诉lvgl经过了多长时间(ms(毫秒)级别) 编写代码 lvgl的中文教程手册网站…

使用WebAssembly优化Web应用性能

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用WebAssembly优化Web应用性能 引言 WebAssembly 简介 安装工具 创建 WebAssembly 项目 编写 WebAssembly 代码 编译 WebAssem…

【docker】docker 环境配置及安装

本文介绍基于 官方存储库 docker 的环境配置、安装、代理配置、卸载等相关内容。 官方安装文档说明&#xff1a;https://docs.docker.com/engine/install/ubuntu/ 主机环境 宿主机环境 Ubuntu 20.04.6 LTS 安装步骤 添加相关依赖 sudo apt-get update sudo apt-get install…

【Linux】网络编程:初识协议,序列化与反序列化——基于json串实现,网络通信计算器中简单协议的实现、手写序列化与反序列化

目录 一、什么是协议&#xff1f; 二、为什么需要有协议呢&#xff1f; 三、协议的应用 四、序列化与反序列化的引入 什么是序列化和反序列化&#xff1f; 为什么需要序列化和反序列化&#xff1f; 五、序列化推荐格式之一&#xff1a;JSON介绍 六、网络版计算器编程逻…

基于MATLAB的加噪语音信号的滤波

一&#xff0e;滤波器的简述 在MATLAB环境下IIR数字滤波器和FIR数字滤波器的设计方 法即实现方法&#xff0c;并进行图形用户界面设计&#xff0c;以显示所介绍迷你滤波器的设计特性。 在无线脉冲响应&#xff08;IIR&#xff09;数字滤波器设计中&#xff0c;先进行模拟滤波器…

Java项目实战II基于Spring Boot的智能家居系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 一、前言 随着物联网技术的快速发展和普及&#…

基于Spring Boot+Vue的助农销售平台(协同过滤算法、限流算法、支付宝沙盒支付、实时聊天、图形化分析)

&#x1f388;系统亮点&#xff1a;协同过滤算法、节流算法、支付宝沙盒支付、图形化分析、实时聊天&#xff1b; 一.系统开发工具与环境搭建 1.系统设计开发工具 后端使用Java编程语言的Spring boot框架 项目架构&#xff1a;B/S架构 运行环境&#xff1a;win10/win11、jdk1…

【C++笔记】容器适配器及deque和仿函数

【C笔记】容器适配器及deque和仿函数 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;C笔记 文章目录 【C笔记】容器适配器及deque和仿函数前言一.容器适配器1.1什么是容器适配器1.2 STL标准库中stack和queue的底层结构 二.stack2.1stack类模…

软考:中间件

中间件 中间件是一类位于操作系统软件与用户应用软件之间的计算机软件&#xff0c;它包括一组服务&#xff0c;以便于运行在一台或多台机器上的多个软件通过网络进行交互。 中间件的主要功能包括通信支持和应用支持。 通信支持为应用软件提供平台化的运行环境&#xff0c;屏蔽…

统信UOS设备驱动开发-常见问题

包含linux设备驱动开发的基础知识及统信UOS设备驱动的总体架构,常用的设备驱动开发调试优化手段及在环境搭建和代码编写过程中常见问题处理等。 文章目录 环境搭建如何编译驱动代码编写如何实现同源异构环境搭建 如何编译内核 下载并解压内核源码包,进入源码根目录,内核的编…

JS 异步 Promise、Async、await详解

目录 一、JS里的同步异步 二、Promise 1、状态 2、all()、race()、any() 3、简单案例 4、异步执行案例 5、解决异步嵌套繁琐的场景 三、async和await 1、async返回类型 2、async与await结合使用的简单案例 3、解决异步嵌套问题 4、批量请求优化 一、JS里的同步异步…

【Vue3】Vue3相比Vue2有哪些新特性?全面解析与应用指南

&#x1f9d1;‍&#x1f4bc; 一名茫茫大海中沉浮的小小程序员&#x1f36c; &#x1f449; 你的一键四连 (关注 点赞收藏评论)是我更新的最大动力❤️&#xff01; &#x1f4d1; 目录 &#x1f53d; 前言1️⃣ 响应式系统的改进2️⃣ Composition API的引入3️⃣ 更好的Type…

Vue 事件阻止 e.preventDefault();click.prevent

Vue 事件阻止 Vue 事件阻止 e.preventDefault(); click.prevent修饰符

基于vue3和elementPlus的el-tree组件,实现树结构穿梭框,支持数据回显和懒加载

一、功能 功能描述 数据双向穿梭&#xff1a;支持从左侧向右侧转移数据&#xff0c;以及从右侧向左侧转移数据。懒加载支持&#xff1a;支持懒加载数据&#xff0c;适用于大数据量的情况。多种展示形式&#xff1a;右侧列表支持以树形结构或列表形式展示。全选与反选&#xf…

leetcode-21-合并两个有序链表

题解&#xff1a; 1、初始化哑节点dum 2、 3、 代码&#xff1a; 参考&#xff1a;leetcode-88-合并两个有序数组

WPF怎么通过RestSharp向后端发请求

1.下载RestSharpNuGet包 2.请求类和响应类 public class ApiRequest {/// <summary>/// 请求地址/// </summary>public string Route { get; set; }/// <summary>/// 请求方式/// </summary>public Method Method { get; set; }/// <summary>//…

指派问题的求解

实验类型&#xff1a;◆验证性实验 ◇综合性实验 ◇设计性实验 实验目的&#xff1a;学会使用Matlab求解指派问题。 实验内容&#xff1a;利用Matlab编程实现枚举法求解指派问题。 实验例题&#xff1a;有5人分别对应完成5项工作&#xff0c;其各自的耗费如下表所示&#…

vue3 gsap 基于侦听器的动画

1、gsap实现动画 https://gsap.com/ .以上来自baidu ai 2、代码&#xff1a; 安装gsap&#xff1a;pnpm install gsap <script setup> import { ref, reactive, watch } from vue import gsap from gsapconst number ref(0) const tweened reactive({number: 0 })wat…