Go语言学习笔记【7】 实现聊天系统[一](B站视频)

本文记录了使用Go语言实现聊天系统的过程,包括需求分析、登录注册功能的实现以及消息流程的详细解析。通过C-S架构,采用序列化的JSON数据传输,确保数据完整性。在登录过程中进行了消息长度校验和登录验证,注册功能涉及到Redis存储用户信息。
摘要由CSDN通过智能技术生成

【声明】

非完全原创,部分内容来自于学习其他人的理论和B站视频。如果有侵权,请联系我,可以立即删除掉。

一、聊天系统需求和分析

1、需求

实现一个海量用户的通讯系统,要求主界面有用户登录、注销用户、退出系统三个功能。其中,登录用户需要输入用户ID、密码,校验通过后才能进入聊天室,聊天室中用户可互相发送消息

2、分析

采用C-S架构,服务端死循环进行端口监听,一旦检测到有客户端连接时则开启线程处理;客户端需要在连接时,需要先登录上才能发消息

2.1、消息的流程

由于涉及到服务端与客户端的网络通讯,因此传输的数据使用序列化后的json串。通讯时,传输的数据消息需要划分为多种:用户登录的消息,用户发送聊天数据的消息;服务器响应用户操作的消息

分析登录消息传输流程
以客户端发送用户登录消息,服务端返回登录验证结果消息为例,分析整个流程:
(1)客户端接收用户输入的ID、用户名,并将其传给服务端
(2)服务端接收客户端的登录数据,用服务器存储的密码进行校验,返回登录验证的结果
(3)客户端接收服务端的登录验证结果,判断登录成功还是失败,并打开相应的界面
(4)考虑如何组织服务端和客户端间通讯的数据格式

设计消息协议
为了方便上述流程的顺利进行,可以将用户输入的ID、用户名封装为一个结构体LoginMsg,将服务端相应用户操作的数据封装为结构体LoginReturn。
为了确保接收侧能够正确解析当前数据,需要将服务端和客户侧的消息统一格式,定义一个消息结构体Message,类型表示指定待解析的结构体,数据表示服务端或者客户端序列化后的数据
为了确保数据的完整性,需要先对数据的长度进行校验。

客户端发送数据、服务端接收数据流程
按照上述的方法,客户端发送的流程为:
(1)创建一个LoginMsg结构体对象,用于接收用户的ID、密码,并将其序列化为数据
(2)创建一个消息结构体Message,类型为登录消息,数据为LoginMsg结构体对象序列化后的数据
(3)将Message结构体序列化,为了防止丢包,有两种办法:在Message结构体中添加数据长度,一次性发送结构体序列化后的数据,接受侧解析数据后根据长度字段来确定数据是否完整;先发送Message结构体序列化后数据的长度,再发送数据的内容,接收侧校验长度。本案例中选用方法2,先发长度,再发数据

服务端接收数据的流程:
(1)接收客户侧发送的消息数据长度,和消息数据本身
(2)校验消息数据的长度
(3)如果长度不等,则需要纠错协议
(4)长度相等,则对消息反序列化,根据Message消息类型,将数据反序列化出其对应的结构体
(5)如果是登录消息LoginMsg,则根据密码来校验
(6)根据校验结果,构建LoginReturn结构体,将其序列化为Message结构体的数据,再将Message结构体序列化的数据发送给客户端

3、实现登录、注册

3.1、登录消息长度校验

3.1.1、go文件

utils/msgdef.go

package utils
const (
	ClientLoginMsg  = "LoginMsg"
	ServerReturnMsg = "LoginReturn"
)
type Message struct {
   
	MsgType string
	MsgData string
}
type LoginMsg struct {
   
	UsrId  int
	UsrPwd string
}
type LoginReturn struct {
   
	ErrCode int
	ErrInfo string
}

server/server.go

func msgProcess(con net.Conn) {
   
	buf := make([]byte, 4)
	defer con.Close()
	lens, err := con.Read(buf)
	fmt.Printf("Server receive message from client[%s] ", con.RemoteAddr())
	if lens != 4 || err != nil {
   
		fmt.Println("failed, len = ", lens, "err = ", err)
		return
	}
	fmt.Printf("sucessful, len = %d, content = %v\n", lens, buf)
}

func main() {
   
	listen, err := net.Listen("tcp", "0.0.0.0:8088")
	if err != nil {
   
		fmt.Println("Server create listener failed, err = ", err)
		return
	}
	defer listen.Close()
	fmt.Println("Server[0.0.0.0:8088] continuously listening for connections")
	for {
   
		con, err := listen.Accept()
		if err != nil {
   
			fmt.Println("Server accept connection failed, err = ", err)
			return
		}
		go msgProcess(con)
	}
}

client/client.go

package main
import (
	"fmt"
)
func main() {
   
	//定义全局变量接收用户的序号选择、用户ID、密码
	var key, id int
	var pwd string
	for {
   
		fmt.Println("-------------欢迎来到简易及时通讯系统-------------")
		fmt.Println("\t\t\t 1. 用户登录")
		fmt.Println("\t\t\t 2. 注销用户")
		fmt.Println("\t\t\t 3. 退出系统")
		fmt.Printf("\t\t\t请选择(1~3): ")
		fmt.Scanln(&key)
		switch key {
   
		case 1:
			fmt.Printf("请输入用户ID: ")
			fmt.Scanln(&id)
			fmt.Printf("请输入用户密码: ")
			fmt.Scanln(&pwd)
			if err := Login(id, pwd); err != nil {
   
				fmt.Println("client login failed")
			} else {
   
				fmt.Println("client login successful")
			}
		case 2:
		case 3:
		default:
			fmt.Println("序号输入有误,请重新输入!")
		}
		if key == 1 || key == 2 || key == 3 {
   
			break
		}
	}
}

client/login.go

package main

import (
	"Test0/IMS/utils"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"net"
)

func Login(usrId int, usrPwd string) (err error) {
   
	//1. 连接到服务器
	con, err := net.Dial("tcp", "localhost:8088")
	if err != nil {
   
		fmt.Println("Client connect Server[localhost:8088] failed, err = ", err)
		return
	}
	defer con.Close()
	
	//2. 创建LoginMsg结构体对象并序列化
	loginbuf, err := json.Marshal(&utils.LoginMsg{
   UsrId: usrId, UsrPwd: usrPwd})
	if err != nil {
   
		fmt.Println("Client login data marshal failed, err = ", err)
		return
	}

	//3. 创建Message消息结构体,并序列化
	data, err := json.Marshal(&utils.Message{
   MsgType: utils.ClientLoginMsg, MsgData: string(loginbuf)})
	if err != nil {
   
		fmt.Println("Client message data marshal failed, err = ", err)
		return
	}

	//4. 发送序列化后的数据
	//4.1 使用大端的方式先发送消息数据的长度
	buf := make([]byte, 4) //4字节记录消息序列化后的长度
	binary.BigEndian.PutUint32(buf, uint32(len(data)))
	lens, err := con.Write(buf)
	fmt.Printf("Client send message length to Server[%s] ", con.RemoteAddr())
	if lens != 4 || err != nil {
   
		fmt.Println("failed, len = ", lens, "err = ", err)
		return
	}
	fmt.Printf("sucessful, len = %d, content = %+v\n", lens, buf)
	return nil
}
3.1.2、验证结果
PS Test0\IMS\server> go run .\server.go
Server[0.0.0.0:8088] continuously listening for connections
Server receive message from client[127.0.0.1:62402] sucessful, len = 4, content = [0 0 0 71]

Test0\IMS\client>go run client.go
-------------欢迎来到简易及时通讯系统-------------
                         1. 用户登录
                         2. 注销用户
                         3. 退出系统
                        请选择(1~3): 1
请输入用户ID: 1234
请输入用户密码: root
Client send message to Server[127.0.0.1:8088] sucessful, len = 4, content = [0 0 0 71]
client login successful

3.2、登录消息的校验

3.2.1、go文件修改点

client/login.go

//func Login(usrId int, usrPwd string) (err error)
	//4. 发送序列化后的数据
	//4.1 使用大端的方式先发送消息数据的长度
	buf := make([]byte, 4) //4字节记录消息序列化后的长度
	binary.BigEndian.PutUint32(buf, uint32(len(data)))
	lens, err := con.Write(buf)
	fmt.Printf("Client send message length to Server[%s] ", con.RemoteAddr())
	if lens != 4 || err != nil {
   
		fmt.Println("failed, len = ", lens, "err = ", err)
		return
	}
	fmt.Printf("sucessful, len = %d, content = %+v\n\n", lens, buf)

	//4.2 发送消息数据
	lens, err = con.Write(data)
	fmt.Printf("Client send message data to Server[%s] ", con.RemoteAddr())
	if err != nil {
   
		fmt.Println("failed, len = ", lens, "err = ", err)
		return
	}
	fmt.Printf("sucessful, len = %d, content = %+v\n", lens, data)
	time.Sleep(time.Second * 2)
	return nil

server/dealMsg.go

package main
import (
	"Test0/IMS/utils"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"net"
)
func readMsg(con net.Conn) (msg utils.Message, err error) {
   
	//1. 读取前4个字节,即数据长度
	buf := make([]byte, 8096)
	_, err = con.Read(buf[:4])
	if err != nil {
   
		fmt.Printf("Server receive message length from client[%s] failed, err = %v\n", con.RemoteAddr(), err)
		return
	}
	pkgLens := binary.BigEndian.Uint32(buf[:4])

	//2. 再读pkgLens个字节到buf中
	lens, err := con.Read(buf[:pkgLens])
	if lens != int(pkgLens) || err != nil {
   
		fmt.Printf("Server receive message data from client[%s] failed, err = %v", con.RemoteAddr(), err)
		return
	}

	//3. 读到的数据反序列化为Message结构体
	err = json.Unmarshal(buf[:pkgLens], &msg)
	if err != nil {
   
		fmt.Println("Server received message unmarshal failed, err = ", err)
		return
	}
	return
}

server/server.go

func msgProcess(con net.Conn) {
   
	/* //check message length is ok
	buf := make([]byte, 8096)
	defer con.Close()
	lens, err := con.Read(buf)
	fmt.Printf("Server receive message from client[%s] ", con.RemoteAddr())
	if lens != 4 || err != nil {
		fmt.Println("failed, len = ", lens, "err = ", err)
		return
	}
	fmt.Printf("sucessful, len = %d, content = %+v\n", lens, buf)*/
	//1. 获取客户端反序列化的消息结构体
	defer con.Close()
	for {
   
		msg, err := readMsg(con)
		if err != nil {
   
			if err == io.EOF {
   
				fmt.Printf("client[%s] closed, err = %v\n", con.RemoteAddr(), err)
			} else {
   
				fmt.Printf("Server receive message from client[%s] check length failed, err = %v\n", con.RemoteAddr()
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值