【GO语言】实现一个简单的并发聊天室,初级练手项目

项目总览:

1.开发语言:GO语言
2.IDE:Goland
3.开发用时:半天
4.源码已上传到我的GitHub,链接:https://github.com/2394799692/IM-2022-3-13- 或点此跳转
5.项目主要用于练习go语言实现并发编程和网络编程。


以下是本篇文章正文内容,欢迎朋友们进行指正,一起探讨,共同进步。——来自考研路上的lwj。QQ:2394799692

一、项目开发日志

开发日志:
广播用户上线:
1.主go程中,创建监听套接字,记得defer
2.for循环监听客户端连接请求。Accept()函数
3.有一个客户端连接,创建新go程处理客户端数据 HandlerConnet(conn)defer
4.定义全局结构体类型 C,Name,Addr
5.创建全局map,channel
6.实现HandlerConnet,获取客户端IP+port——RemoteAddr().初始化新用户结构体信息。name==Addr
7.创建实现管理go程,在accept()之前。
8.实现Manager。初始化在线用户map。循环读取全局变量channel,如果无证据,阻塞。如果有证据,遍历在线用户map,将数据写到用户的C里
9.将新用户添加到 在线用户map中。key==IP+port value=新用户结构体
10.创建WriteMsgToClient go程,专门给当前用户写数据。——来源于用户自带的C中
11.创建WriteMsgToClient(clnt,conn)。遍历自带的C,读数据,conn.Write到客户端
12.HandlerConnet中,结束位置,组织用户上线信息写到全局channel——Manager的读就被激活(原来一直阻塞)
13.HandlerConnet中,结束加for{}

广播用户消息:
1.封装函数MakeMsg()来处理广播,用户消息
2.HandlerConnet中,创建匿名go程,读取用户socket上发送来的聊天内容,写到全局channel
3.for循环conn.Read n==0 err!=nil
4.写给全局message——后续的事,原来广播用户上线模块 完成。

查询在线用户:
1.将读取到的用户消息msg结尾的“\n”去掉
2.判断是否是“who”命令
3.如果是,遍历在线用户列表,组织显示信息,写到socket中、
4.如果不是,写给全局messge

修改用户名:
1.将读取到的用户消息msg判断是否包含“rename”
2.提取”|“后面的字符串,存入到Client的Name成员中
3.更新在线用户列表。onlineMap。Key——IP+prot
4.提示用户更新完成。conn.Write

用户退出:
1.在用户成功登陆之后,创建监听用户退出的channel——isQuit
2.当conn.Read==0,isQuit<-true
3.在HandlerConnet结尾for中,添加select监听<-isQuit
4.条件满足,将用户从在线列表移除。组织用户下线消息,写入message

超时强T:
1.在select中监听定时器。(time.After())计时到达。将用户从在线列表移除。组织用户下线消息,写入message(广播)
2.创建监听用户活跃的channel——hasData
3.只要用户在执行聊天,改名,who、任意操作,都在hasdata中写数据
4.在select中添加监听hasdata,条件满足,不做任何事情,目的是重置计时器


主go程(服务器):负责监听,接收用户(客户端)连接请求,建立通信关系。同时启动相应的go程处理任务

二、知识补充与整体框架图

1.TCP通信的基础知识

【GO语言】实现TCP—C/S设计模式的服务器server端与客户client端即时通信
【GO语言】实现TCP—C/S设计模式的服务器server端与客户client端【并发】通信

2.聊天室实现框架图

请添加图片描述

3.各个主要go程拆解

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

三、全部代码展示

package main

import (
	"fmt"
	"net"
	"strings"
	"time"
)

//创建用户结构体类型!
type Client struct {
	C    chan string
	Name string
	Addr string
}

//创建全局map,存储在线用户
var onlineMap map[string]Client

//创建全局channel,用于传递用户消息
var message = make(chan string)

func WriteMsgToClient(clnt Client, conn net.Conn) {
	//监听用户自带channel上是否有消息
	for msg := range clnt.C {
		conn.Write([]byte(msg + "\n"))
	}
}
func MakeMsg(clnt Client, msg string) (buf string) {
	buf = "[" + clnt.Addr + "]" + clnt.Name + ":" + msg
	return
}
func HandlerConnect(conn net.Conn) {
	defer conn.Close()
	//创建channel判断,用户是否活跃
	hasData := make(chan bool)
	//获取用户网络地址 ip+port
	netAddr := conn.RemoteAddr().String()
	//创建用户的结构体信息
	clnt := Client{make(chan string), netAddr, netAddr}
	//将新链接用户,添加到在线用户map中
	onlineMap[netAddr] = clnt
	//发送用户上线消息到全局channel中
	go WriteMsgToClient(clnt, conn)
	//创建专门用来给当前用户发送消息的go程
	message <- MakeMsg(clnt, "login")
	//创建一个channel,用来判断退出状态
	isQuit := make(chan bool)
	//创建一个匿名go程,专门处理用户发送的消息。
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := conn.Read(buf)
			if n == 0 {
				isQuit <- true
				fmt.Printf("检测到客户端:%s退出", clnt.Name)
				return
			}
			if err != nil {
				fmt.Println("conn.Read err:", err)
				return
			}
			//将读到的用户消息,写入到message中
			msg := string(buf[:n-1]) //减去1的意思是去掉斜杆
			//提取在线用户列表
			if msg == "who" && len(msg) == 3 {
				conn.Write([]byte("online user list:\n"))
				//遍历当前map、获取在线用户
				for _, user := range onlineMap {
					userInfo := user.Addr + ":" + user.Name + "\n"
					conn.Write([]byte(userInfo))
				}
			} else if len(msg) >= 8 && msg[:6] == "rename" {
				newName := strings.Split(msg, "|")[1]
				clnt.Name = newName       //修改结构体成员name
				onlineMap[netAddr] = clnt //更新onlineMap
				conn.Write([]byte("rename successful\n"))
			} else {
				//将读到的用户消息,写入到message中
				message <- MakeMsg(clnt, msg)
			}
			hasData <- true
		}
	}()
	//保证不退出
	for {
		//监听channel上的数据流动
		select {
		case <-isQuit:
			delete(onlineMap, clnt.Addr)       //将用户从online移除
			message <- MakeMsg(clnt, "logout") //写入用户退出消息到全局channel
			return
		case <-hasData:
			//什么都不做,目的是重置下面case的计时器。
		case <-time.After(time.Second * 10):
			delete(onlineMap, clnt.Addr)       //将用户从online移除
			message <- MakeMsg(clnt, "logout") //写入用户退出消息到全局channel
			return
		}
	}
}
func Manager() {
	//初始化map
	onlineMap = make(map[string]Client)
	for { //循环从message中读取
		//监听全局channel中是否有数据,有数据存储至msg,无数据阻塞
		msg := <-message
		//循环发送消息给所有在线用户
		for _, clnt := range onlineMap {
			clnt.C <- msg
		}
	}
}
func main() {
	//创建监听套接字
	listener, err := net.Listen("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Println("Listen err", err)
		return
	}
	defer listener.Close()
	//循环监听客户端连接请求
	//创建管理者go程,管理map和ch
	go Manager()
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("Accept err", err)
			return
		}
		//启动go程处理客户端请求
		go HandlerConnect(conn)
	}
}



四、效果展示:

1)启动服务器后,开启3个客户端,同时发送数据,测试都能接收到:
在这里插入图片描述

2)使用rename|+新名字,实现改名功能
在这里插入图片描述

3)使用exit命令退出聊天室
在这里插入图片描述
4)超时自动下线

在这里插入图片描述
5)who展示所有在线人员
在这里插入图片描述

  • 7
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
好的,我可以提供一些思路和代码示例,帮助你完成这个任务。 首先,我们需要选择一个Web框架构建这个程序。Go语言中比较流行的Web框架有 Gin 和 Beego,这里我选择 Gin 作为示例。 接下来,我们需要考虑如何实现并发售票功能。一种常见的方法是使用互斥锁(Mutex)来保护共享资源,防止多个协程同时访问导致数据错误。具体操作如下: 1. 定义一个 Ticket 结构体,包含票的编号和剩余数量: ```go type Ticket struct { ID int Amount int } ``` 2. 定义一个全局的互斥锁: ```go var mutex sync.Mutex ``` 3. 在处理售票请求的函数中,先加锁,然后检查剩余数量是否大于 0,如果是则将剩余数量减 1,并返回售票成功的消息;否则返回售票失败的消息。最后释放锁。 ```go func sellTicket(c *gin.Context) { mutex.Lock() defer mutex.Unlock() if ticket.Amount > 0 { ticket.Amount-- c.JSON(http.StatusOK, gin.H{"status": "success", "message": "Ticket sold."}) } else { c.JSON(http.StatusOK, gin.H{"status": "fail", "message": "Ticket sold out."}) } } ``` 4. 在程序启动时,初始化 Ticket 结构体: ```go ticket := Ticket{ID: 1, Amount: 100} ``` 5. 最后,在路由中绑定处理售票请求的函数: ```go router := gin.Default() router.GET("/sell", sellTicket) router.Run(":8080") ``` 完整代码示例如下: ```go package main import ( "net/http" "sync" "github.com/gin-gonic/gin" ) type Ticket struct { ID int Amount int } var mutex sync.Mutex var ticket Ticket func sellTicket(c *gin.Context) { mutex.Lock() defer mutex.Unlock() if ticket.Amount > 0 { ticket.Amount-- c.JSON(http.StatusOK, gin.H{"status": "success", "message": "Ticket sold."}) } else { c.JSON(http.StatusOK, gin.H{"status": "fail", "message": "Ticket sold out."}) } } func main() { ticket = Ticket{ID: 1, Amount: 100} router := gin.Default() router.GET("/sell", sellTicket) router.Run(":8080") } ``` 这样,我们就完成了一个提供并发售票功能的Web程序。用户可以通过访问 `/sell` 路由来购买票,程序会保证每个请求都是在一个协程中独立执行,避免了并发访问的问题。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

立志冲海大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值