Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go聊天室

基于Go语言实现简单网络聊天室(命令行模式)

作者:千年死缓

这篇文章主要为大家详细介绍了如何基于Go语言实现简单网络聊天室,文中的示例代码简洁易懂,有需要的小伙伴可以跟随小编一起学习一下

实战简介

网络聊天室(命令行模式)

要求:

基于tcp协议实现功能

服务器端

接受用户消息和循环转发

对功能命令进行处理

客户端

接受服务器发送的信息并处理

接受用户的输入处理后发往服务器

结构和示例

用户登录示例

功能行命令测试

发送消息广播测试

用户退出示例

服务器端

基本流程

1. 初始化

init() 函数被调用,用于初始化全局变量:

2. 主函数 main()

3. 处理客户端连接 handleConnection(conn)

对于每个客户端连接,首先增加在线用户计数 count。

调用 addUser(conn) 添加新用户到在线用户列表,并返回一个 client 结构体实例。

创建一个 quit 通道,用于在客户端断开连接时发送信号。

启动两个协程:

监听 quit 通道以检测客户端是否断开连接。

4. 添加新用户 addUser(conn)

创建一个新的 client 实例,其中包含一个用于消息的通道 userChannel、客户端连接 conn 和默认名称(客户端地址)。

将新用户添加到 onlineList 映射中。

5. 管理消息广播 manger()

从消息通道 message 中读取消息,并将消息广播给所有在线用户。

6. 写消息到客户端 writeMsgToClient(conn, quit)

从客户端的 userChannel 读取消息,使用 module.Encode 对消息进行编码,并通过客户端连接 conn 发送给客户端。

如果发生错误或客户端断开连接,关闭客户端连接并发送信号到 quit 通道。

7. 读取客户端消息 readClient(conn, quit)

从客户端读取消息,根据消息的内容执行不同的操作:

如果客户端断开连接,则发送信号到 quit 通道。

8. 处理客户端退出

当 quit 通道接收到信号时,从在线用户列表中删除该客户端,并减少在线用户计数。

如果所有客户端都已断开连接,则输出“等待用户连接中…”。

代码

package main

import (
	"bufio"         
	"chatRoom/chatRoom/module" // 消息的编码和解码模块
	"fmt"          
	"io"             
	"log"            
	"net"            
	"strconv"        
	"strings"       
	"sync"           
	"time"          
)

// 定义客户端结构体
type client struct {
	userChannel chan string // 用户的消息通道
	conn        net.Conn    // 网络连接
	name        string      // 用户名
	addr        string      // 客户端地址
}

// 定义在线用户计数器
var count int

// 定义互斥锁
var mu sync.Mutex

// 定义在线用户列表
var onlineList map[string]*client

// 定义消息广播通道
var message chan string

// 初始化函数
func init() {
	onlineList = make(map[string]*client) // 初始化在线用户列表
	message = make(chan string, 1024)     // 初始化消息广播通道
}

// 主函数
func main() {
	fmt.Println("端口监听中...")
	listener, err := net.Listen("tcp", "127.0.0.1:8080")
	if err != nil {
		log.Fatal(err) // 如果监听失败,记录错误并退出程序
	}
	defer listener.Close()
	time.Sleep(time.Second)
	fmt.Println("端口监听成功")

	// 启动管理消息广播的协程
	go manger()

	// 主循环,接受客户端连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		go handleConnection(conn)
	}
}

// 处理客户端连接的函数
func handleConnection(conn net.Conn) {
	defer conn.Close()

	count++ // 增加在线用户计数

	fmt.Println("有新用户连接服务,当前连接数:", count)

	// 添加新用户
	addUser(conn)

	// 创建退出信号通道
	var quit = make(chan bool)

	// 启动写消息到客户端的协程
	go writeMsgToClient(conn, quit)

	// 启动读取客户端消息的协程
	go readClient(conn, quit)

	// 监听退出信号
	select {
	case <-quit:
		// 用户下线处理
		connName := onlineList[conn.RemoteAddr().String()].name
		mu.Lock()
		close(onlineList[conn.RemoteAddr().String()].userChannel)
		mu.Unlock()
		mu.Lock()
		delete(onlineList, conn.RemoteAddr().String())
		mu.Unlock()
		count--
		message <- "< 系统消息 > [ " + connName + " ]" + "下线了 当前在线人数 " + strconv.Itoa(len(onlineList)) + " 人"
		fmt.Println("有用户下线了,当前连接数:", count)
		if count == 0 {
			fmt.Println("等待用户连接中...")
		}

		return
	}
}

// 修改用户名的方法
func (c *client) changeName(newUserName string) bool {
	mu.Lock()
	defer mu.Unlock()
	// 更新用户名
	c.name = newUserName
	return true
}

// 管理消息广播的函数
func manger() {
	fmt.Println("开始监听 message通道")
	defer fmt.Println("结束监听 message通道")
	for msg := range message {
		mu.Lock()
		for _, v := range onlineList {
			v.userChannel <- msg
		}
		mu.Unlock()
	}
}

// 写消息到客户端的协程
func writeMsgToClient(conn net.Conn, quit chan bool) {
	fmt.Println(onlineList[conn.RemoteAddr().String()].name, "的信息通道监听成功")
	defer fmt.Println(onlineList[conn.RemoteAddr().String()].name, "的信息通道监听结束")
	for msg := range onlineList[conn.RemoteAddr().String()].userChannel {
		king, err := module.Encode(msg + "\n")
		if err != nil {
			fmt.Println("发送消息失败")
			continue
		}
		_, err = conn.Write(king)
		if err != nil {
			fmt.Println("发送消息失败")
			quit <- true
		}
	}
	fmt.Println("函数writeMsgToClient函数结束")
}

// 添加新用户
func addUser(conn net.Conn) client {
	fmt.Println("开始使用添加新用户" + conn.RemoteAddr().String())
	newUser := client{
		make(chan string), // 创建用户消息通道
		conn,              // 网络连接
		conn.RemoteAddr().String(), // 用户名,初始化为客户端地址
		conn.RemoteAddr().String(), // 客户端地址
	}
	onlineList[conn.RemoteAddr().String()] = &newUser // 添加到在线用户列表

	fmt.Println("addUser函数结束,用户" + conn.RemoteAddr().String() + "添加成功")
	return newUser
}

// 读取客户端消息的协程
func readClient(conn net.Conn, quit chan bool) {
	fmt.Println("开始读取客户端发送的信息")
	defer fmt.Println("客户端发送信息读取结束")

	userChannel := onlineList[conn.RemoteAddr().String()].userChannel

	reader := bufio.NewReader(conn)
	for {
		msg, err := module.Decode(reader)
		if err == io.EOF {
			quit <- true
		}
		if err != nil {
			fmt.Println("decode msg failed, err:", err)
			quit <- true
		}
		if len(msg) == 0 {
			continue
		}
		fmt.Println("收到client发来的数据:", msg)

		// 处理客户端发送的不同类型的消息
		switch {
		case strings.HasPrefix(msg, "!@#$@!cd1changeName"):

			king := true
			oldName := onlineList[conn.RemoteAddr().String()].name
			newName := strings.TrimPrefix(msg, "!@#$@!cd1changeName")

			if strings.HasPrefix(msg, "!@#$@!cd1changeNameFirst") {
				newName = strings.TrimPrefix(msg, "!@#$@!cd1changeNameFirst")
			}

			if newName == "" {
				newName = conn.RemoteAddr().String()
			}

			for _, v := range onlineList {

				mapName := v.name
				if mapName == newName {

					king = false

					break
				}
			}

			if strings.HasPrefix(msg, "!@#$@!cd1changeNameFirst") && king == false {

				message <- "< 系统消息 > [ " + conn.RemoteAddr().String() + " ] [ " + oldName + " ] 上线了!"

				userChannel <- "< 系统消息 > [ " + onlineList[conn.RemoteAddr().String()].name + " ]" + "名字: " + newName + " 已存在,请更换一个名字尝试"

				userChannel <- "< 系统消息 > 你当前昵称为: " + oldName + " ( 输入cd2可进行名字修改 )"

				continue
			}

			if king == false {

				userChannel <- "< 系统消息 > 昵称修改失败!!!"

				userChannel <- "< 系统消息 > [ " + onlineList[conn.RemoteAddr().String()].name + " ]" + "名字: " + newName + " 已存在,请更换一个名字尝试"

				userChannel <- "< 系统消息 > 你当前昵称为: " + oldName

				continue

			}

			isSuccess := onlineList[conn.RemoteAddr().String()].changeName(newName)
			if isSuccess {

				userChannel <- "!@#$@!cd1changeName" + newName

				if strings.HasPrefix(msg, "!@#$@!cd1changeNameFirst") {

					message <- "< 系统消息 > [ " + conn.RemoteAddr().String() + " ] [ " + newName + " ] 上线了!"

					time.Sleep(time.Millisecond * 50)

					userChannel <- "< 系统消息 > 你当前的昵称为:" + newName

					continue
				}

				userChannel <- "< 系统消息 > 昵称修改成功 你当前昵称为: " + newName

			} else {

				userChannel <- "< 系统消息 > 昵称修改失败!!!"

			}

			message <- "< 系统消息 > [ " + conn.RemoteAddr().String() + " ]" + " 旧昵称为: " + oldName + " 新昵称为: " + newName

		case strings.HasPrefix(msg, "!@#$@!cd4exit"):
			fmt.Println("[ " + onlineList[conn.RemoteAddr().String()].name + " ] " + "下线了")

			quit <- true

			return
		case strings.HasPrefix(msg, "!@#$@!menu"):

			userChannel <- "< 系统消息 > \n * ./cd1 或 ./menu       功能菜单\n * ./cd2 或 ./changeName 更改昵称\n * ./cd3 或 ./online     在线用户数量查询\n * ../cd4 或 ./quit      退出聊天室"

		case strings.HasPrefix(msg, "!@#$@!cd3online"):

			fmt.Println("在线人数:", count)

			userChannel <- "< 系统消息 > 当前在线人数:" + strconv.Itoa(count)

		default:

			message <- "[ " + onlineList[conn.RemoteAddr().String()].name + " ]" + ": " + msg

			fmt.Println("信息广播成功")
		}
	}
}

客户端

基本流程

1. 主函数 main()

2. 获取用户输入 getUserInput(prompt string)

3. 读取消息 readMsg(conn)

4. 处理用户输入

代码

package main

import (
	"bufio"         
	"chatRoom/chatRoom/module" // 消息的编码和解码模块
	"fmt"            
	"io"            
	"net"            
	"os"            
	"strings"        
	"time"           
)

// 定义一个全局变量用于存储用户的昵称
var name string

// 主函数
func main() {
	// 尝试连接到服务器
	conn, err := net.Dial("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Println("服务器连接失败 err =", err)
		return
	}

	defer conn.Close()
	// 打印连接成功的消息
	fmt.Println("服务器连接成功")

	// 获取用户的昵称
	name = getUserInput("请输入你的昵称:")
	// 构建一条特殊的消息,用于通知服务器用户昵称
	data, err := module.Encode("!@#$@!cd1changeNameFirst" + name)
	if err != nil {
		
		fmt.Println("encode msg failed, err:", err)
		return
	}
	// 发送消息到服务器
	_, err = conn.Write(data)
	if err != nil {
		fmt.Println("发送数据失败1 err =", err)
	}

	// 创建一个通道,用于接收退出信号
	var exit = make(chan bool)
	// 确保在函数退出时关闭通道
	defer close(exit)

	// 显示欢迎信息和命令提示
	fmt.Println("--------------欢迎进入多人聊天室系统----------------")
	fmt.Println("       * ./cd1 或 ./menu       功能菜单")
	fmt.Println("       * ./cd2 或 ./changeName 更改昵称")
	fmt.Println("       * ./cd3 或 ./online     在线用户数量查询")
	fmt.Println("       * ./cd4 或 ./quit       退出聊天室")
	fmt.Println("---------------指令字母不区分大小写-----------------")

	// 启动一个协程用于读取消息
	go readMsg(conn)

	// 启动一个协程用于处理用户输入
	go func() {
		for {
			// 获取用户输入
			msg := getUserInput("")

			// 根据用户输入特殊消息处理命令
			if strings.EqualFold(msg, "./cd1") || strings.EqualFold(msg, "./menu") {
				msg = "!@#$@!menu"
			}
			if strings.EqualFold(msg, "./cd2") || strings.EqualFold(msg, "./changeName") {
				newMsg := getUserInput("请输入新的昵称:")
				msg = "!@#$@!cd1changeName" + newMsg
			}
			if strings.EqualFold(msg, "./cd3") || strings.EqualFold(msg, "./online") {
				msg = "!@#$@!cd3online"
			}
			if strings.EqualFold(msg, "./cd4") || strings.EqualFold(msg, "./quit") {
				msg = "!@#$@!cd4exit"
				// 编码并发送退出消息
				data, err := module.Encode(msg)
				if err != nil {
					fmt.Println("消息数据失败1, err:", err)
					return
				}
				_, err = conn.Write(data)
				if err != nil {
					// 如果发送失败,打印错误信息
					fmt.Println("发送数据失败2 err =", err)
				}
				// 打印退出信息
				fmt.Println("正在退出...")
				// 发送退出信号
				exit <- true
				return
			}

			// 编码并发送普通消息
			data, err := module.Encode(msg)
			if err != nil {
				// 如果消息编码失败,打印错误信息并退出协程
				fmt.Println("发送数据失败3, err:", err)
				return
			}
			_, err = conn.Write(data)
			if err != nil {
				// 如果发送失败,打印错误信息
				fmt.Println("发送数据失败4 err =", err)
			}
		}
	}()

	// 主循环,监听退出信号
	for {
		select {
		case <-exit:
			// 当收到退出信号时,打印退出成功并退出程序
			fmt.Println("退出成功")
			return
		}
	}
}

// getUserInput 函数用于获取用户输入
func getUserInput(prompt string) string {

	time.Sleep(time.Millisecond * 100)
	// 根据不同的提示信息显示相应的提示
	switch prompt {
	case "请输入你的昵称:":
		fmt.Print("请输入你的昵称:")
	case "请输入新的昵称:":
		fmt.Println("请输入新的昵称:")
	}
	// 创建一个标准输入的缓冲读取器
	reader := bufio.NewReader(os.Stdin)
	// 读取一行输入
	input, err := reader.ReadString('\n')
	if err != nil {
		// 如果读取失败,打印错误信息并返回错误信息
		fmt.Println("用户输入获取失败:err =", err)
		return "客户端信息读取错误"
	}
	// 返回去掉空格的输入字符串
	return strings.TrimSpace(input)
}

// readMsg 函数用于读取消息
func readMsg(conn net.Conn) {

	defer conn.Close()
	// 创建一个缓冲读取器来读取连接中的数据
	reader := bufio.NewReader(conn)
	for {
		// 解码消息
		msg, err := module.Decode(reader)
		if err == io.EOF {
			// 如果遇到EOF(文件结束),表示连接已断开
			fmt.Println("服务器连接已断开 ")
			// 终止程序
			os.Exit(1)
		}
		if err != nil {
		
			fmt.Println("服务器断开连接 2 err =", err)
			return
		}
		if msg == "" {
			// 如果消息为空,则跳过本次循环
			continue
		}
		if strings.HasPrefix(msg, "!@#$@!cd1changeName") {
			// 如果消息是更改昵称的通知
			msg1 := strings.TrimPrefix(msg, "!@#$@!cd1changeName")
			name = strings.TrimRight(msg1, "\n")
			// 更新昵称
			continue
		}
		// 打印消息的时间戳和内容
		fmt.Print("【 ", time.Now().Format("15:04"), " 】", msg)
	}
}

消息封包和解包的函数

作用:防止tcp粘包的情况影响消息的读取

1.为什么会出现粘包

主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。

“粘包"可发生在发送端也可发生在接收端:

2.解决办法

出现"粘包"的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。

封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入"包尾"内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

代码

package module

import (
	"bufio"
	"bytes"
	"encoding/binary"
)

func Encode(message string) ([]byte, error) {
	// 读取消息的长度,转换成int32类型(占4个字节)
	var length = int32(len(message))
	var pkg = new(bytes.Buffer)
	// 写入消息头
	err := binary.Write(pkg, binary.LittleEndian, length)
	if err != nil {
		return nil, err
	}
	// 写入消息实体
	err = binary.Write(pkg, binary.LittleEndian, []byte(message))
	if err != nil {
		return nil, err
	}
	return pkg.Bytes(), nil
}

// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
	// 读取消息的长度
	lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
	lengthBuff := bytes.NewBuffer(lengthByte)
	var length int32
	err := binary.Read(lengthBuff, binary.LittleEndian, &length)
	if err != nil {
		return "", err
	}
	// Buffered返回缓冲中现有的可读取的字节数。
	if int32(reader.Buffered()) < length+4 {
		return "", err
	}

	// 读取真正的消息数据
	pack := make([]byte, int(4+length))
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}
	return string(pack[4:]), nil
}

以上就是基于Go语言实现简单网络聊天室(命令行模式)的详细内容,更多关于Go聊天室的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文