GO网络编程(四):海量用户通信系统2:登录功能核心【重难点】

news2025/2/4 17:58:07

目录

    • 一、C/S详细通信流程图
    • 二、消息类型定义与json标签
      • 1. 消息类型定义
      • 2. JSON标签
      • 3.结构体示例及其 JSON 表示:
      • 4.完整代码与使用说明
    • 三、客户端发送消息
      • 1. 连接到服务器
      • 2. 准备发送消息
      • 3. 创建 LoginMes 并序列化
      • 4. 将序列化后的数据嵌入消息结构
      • 5. 序列化整个 Message 结构体
      • 6. 发送消息长度
      • 7. 发送消息内容与完整代码
      • 8.细节解释与总结
    • 四、服务端处理消息
      • 1. 创建缓冲区
      • 2. 循环读取客户端发送的数据
      • 3. 读取前 4 个字节
      • 4. 并发处理
      • 5. 总结与完整代码
    • 五、测试与注意事项
      • 1.取消安全提示
      • 2.命令行中的ctrl+c
      • 3.执行.exe与测试效果

一、C/S详细通信流程图

上一讲我们实现了最基本的登录功能,其实根本没涉及网络编程,这节开始才是网络编程,而且复杂,重要,有一定难度。
C/S通信可用以下两张图表示:
在这里插入图片描述
在这里插入图片描述
这节不涉及全部流程,但涉及三块重要的内容:类型定义与json标签,客户端发送消息,服务端处理消息。

二、消息类型定义与json标签

网络编程中通过消息(Message)结构体传递和处理不同类型的数据,例如登录请求和登录响应。通常,这些类型和消息结构体用于客户端与服务器之间的通信,消息被序列化为 JSON 格式,然后通过网络传输。在服务器端或客户端接收到消息后,会对其进行反序列化,以便处理不同类型的消息。
首先在根目录下建立message包,即建立文件夹message,在其中建立文件message.go,然后定义消息结构体类型:

package message

const (
	LoginMesType    = "LoginMes"
	LoginResMesType = "LoginResMes"
)

type Message struct {
	Type string `json:"type"` //消息类型
	Data string `json:"data"` //消息
}

1. 消息类型定义

消息封装:消息传输时,包含不同类型的信息(登录、登出、注册等),需要通过一个统一的结构来表示这些消息。这就是 Message 结构体的用途。
消息分类:通过 Message 中的 Type 字段,可以区分消息的类型,例如是登录消息(LoginMes)还是登录响应消息(LoginResMes),这样服务器和客户端可以根据 Type 做出不同的处理。
扩展性:未来如果需要添加更多的消息类型(例如注册、注销等),可以很方便地在已有的结构上扩展新的类型,而不需要重写消息传递的逻辑。

2. JSON标签

JSON 标签用于指定在序列化(将 Go 的结构体转换为 JSON 格式)和反序列化(将 JSON 格式的数据转换为 Go 结构体)时,结构体字段对应的 JSON 字段名。Go 中的字段名通常是驼峰命名,而 JSON 中的字段名习惯上是小写或下划线分隔,通过 json:“字段名” 来指定这个映射。
具体例子如下:
Type string \json:"type":在消息序列化为 JSON 时,这个字段会被转换为 “type”。例如,如果 Type 是 “LoginMes”,JSON 结果会包含:“type”: “LoginMes”。
Data string \json:"data":这个字段包含实际的消息数据,在 JSON 中也会被表示为 “data”。例如:
"data": "{\"userID\":123,\"userPwd\":\"password\"}",也就是具体的消息数据会序列化为字符串。

3.结构体示例及其 JSON 表示:

1.Message 结构体:

type Message struct {
    Type string `json:"type"` // 消息类型
    Data string `json:"data"` // 消息数据
}

JSON 格式:

{
    "type": "LoginMes",
    "data": "{\"userID\":123,\"userPwd\":\"password\",\"userName\":\"John\"}"
}

2.LoginMes 结构体:

type LoginMes struct {
    UserID   int    `json:"userID"`   // 用户ID
    UserPwd  string `json:"userPwd"`  // 用户密码
    UserName string `json:"userName"` // 用户名
}

JSON 格式:

{
    "userID": 123,
    "userPwd": "password",
    "userName": "John"
}

3.LoginResMes 结构体:

type LoginResMes struct {
    Code  int    `json:"code"`  // 状态码
    Error string `json:"error"` // 错误信息
}
{
    "code": 200,
    "error": ""
}

4.完整代码与使用说明

message.go的完整代码如下:

package message

const (
	LoginMesType    = "LoginMes"
	LoginResMesType = "LoginResMes"
)

type Message struct {
	Type string `json:"type"` //消息类型
	Data string `json:"data"` //消息
}

// 定义两个消息..后面需要再增加
type LoginMes struct {
	UserID   int    `json:"userID"`   //用户id
	UserPwd  string `json:"userPwd"`  //用户密码
	UserName string `json:"userName"` //用户名
}

type LoginResMes struct {
	Code  int    `json:"code"`  //返回状态码 500表示该用户未注册 200表示登录成功
	Error string `json:"error"` //返回错误信息
}

定义这些类型是为了处理客户端和服务器之间的消息传递,不同类型的消息可以通过 Message 结构体中的 Type 来识别。JSON 标签用于控制 Go 结构体字段与 JSON 字段的映射,使得消息在序列化和反序列化时能够正确转换。
在通信时,客户端发送登录请求,会将 LoginMes 转换为 JSON 字符串,并将其存放在 Message 结构体的 Data 字段中。服务器收到后,会先根据 Message 的 Type 来判断消息类型(例如 LoginMes),然后再将 Data 字符串反序列化为 LoginMes 结构体,并进行处理。服务器处理完登录请求后,可以通过 LoginResMes 结构体将结果返回给客户端。

三、客户端发送消息

接下来我们转到client包,完善login代码。
在登录操作中,客户端与服务器通过TCP协议通信,发送的是一个序列化后的消息。这是一个典型的网络编程操作流程,具体包括连接服务器、序列化数据、发送消息等步骤。

1. 连接到服务器

conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
    fmt.Println("net.Dial err=", err)
    return err
}
defer conn.Close()

这里使用 net.Dial 函数建立一个 TCP 连接,目标地址是localhost:8889,即本地的 8889 端口。
conn 是代表连接的对象,通过这个连接对象,客户端可以向服务器发送和接收数据。
defer conn.Close() 表示函数结束时自动关闭连接,避免资源泄漏。

2. 准备发送消息

var mes message.Message
mes.Type = message.LoginMesType

这里创建了一个 Message 结构体变量 mes,用于封装将要发送的消息。mes.Type 被设置为 message.LoginMesType,这表示消息类型是登录消息(LoginMes),后续服务器会根据消息类型来识别消息的具体用途。

3. 创建 LoginMes 并序列化

var loginMes message.LoginMes
loginMes.UserID = userID
loginMes.UserPwd = userPwd
data, err := json.Marshal(loginMes)
if err != nil {
    fmt.Println("json.Marshal err=", err)
    return err
}

loginMes 是一个 LoginMes 结构体,包含用户ID和用户密码的信息。
使用 json.Marshal 将 loginMes 转换为 JSON 字符串,便于传输。这是因为网络通信通常使用文本格式(如 JSON)来传输复杂数据结构。

4. 将序列化后的数据嵌入消息结构

mes.Data = string(data)

这里将序列化后的 loginMes 作为 JSON 字符串赋值给 mes.Data,即将登录信息封装到消息结构体 mes 中的 Data 字段。

5. 序列化整个 Message 结构体

data, err = json.Marshal(mes)
if err != nil {
    fmt.Println("json.Marshal err=", err)
    return err
}

再次使用 json.Marshal 将整个 Message 结构体序列化为 JSON 字符串,准备发送给服务器。这是因为 Message 结构体不仅包含数据,还包含类型信息。

6. 发送消息长度

var pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
n, err := conn.Write(buf[:])
if n != 4 || err != nil {
    fmt.Println("conn.Write(bytes[:]) fail err=", err)
    return err
}

网络通信中,为了保证数据的完整性,常常需要先发送消息的长度,这里通过 len(data) 获取序列化后的消息的长度,并通过 binary.BigEndian.PutUint32 函数将消息长度(pkgLen)转换为 4 个字节(大端序),存放在 buf 中。最后使用 conn.Write(buf[:]) 将消息长度发送给服务器。

7. 发送消息内容与完整代码

接下来会通过 conn.Write(data) 把实际的消息数据发送给服务器,这个操作将在下节讲。
login函数的完整代码如下:

func login(userID int, userPwd string) error {
	//下一个就要开始定协议
	// fmt.Printf("userId=%d pwd=%s\n", userId, pwd)
	// return nil
	//1.连接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	if err != nil {
		fmt.Println("net.Dial err=", err)
		return err
	}
	//延时关闭
	defer conn.Close()
	//2.准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.LoginMesType
	//3.创建一个LoginMes 结构体
	var loginMes message.LoginMes
	loginMes.UserID = userID
	loginMes.UserPwd = userPwd
	//4.将loginMes 序列化
	data, err := json.Marshal(loginMes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return err
	}
	//5.把data赋给mes.Data字段
	mes.Data = string(data)
	//6.将mes进行序列化
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return err
	}
	//7.到这个时候,data就是我们要发送的消息
	//7.1 先把data的长度发送给服务器
	// 先获取到data的长度->转成一个表示长度的byte切片
	var pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write(bytes[:]) fail err=", err)
		return err
	}
	fmt.Printf("客户端,发送消息的长度=%d\n内容:%s\n", len(data), string(data))
	return nil
}

8.细节解释与总结

(1)为什么使用大端序
在网络传输中,为了保证跨平台数据的一致性,通常会采用大端序(Big Endian)来传输数据。因为大端序的字节排列方式与人类阅读数字的顺序一致。高位字节在前,低位字节在后,和我们书写数值的方式相同。
举个例子,假设 pkgLen = 123456 (十六进制为 0x0001E240),则 binary.BigEndian.PutUint32(buf[0:4], pkgLen) 将执行以下操作:
buf[0] = 0x00
buf[1] = 0x01
buf[2] = 0xE2
buf[3] = 0x40
最终 buf[0:4] 就包含了 pkgLen 的大端序表示。
(2)为什么需要发送长度
在网络通信中,消息的长度是动态的,接收方通常不知道需要读取多少数据。如果直接发送数据,接收方可能无法正确判断消息的结束。
因此,发送方先发送一个固定长度的消息(如4个字节的消息长度),让接收方根据这个长度来接收完整的消息。
(3)客户端发消息其实就为四步
1.连接服务器
2.初始化并序列化消息变种
3.用消息变种初始化并序列化消息
4.发送消息的长度与内容

四、服务端处理消息

在server包下创建main.go,自定义process函数,其核心内容如下:

1. 创建缓冲区

buf := make([]byte, 1024*4) //准备缓冲区以读取数据

这里使用 make 函数创建了一个大小为 4KB(1024 * 4 字节)的缓冲区 buf,用于存放从客户端接收到的数据。

2. 循环读取客户端发送的数据

for {
    fmt.Println("读取客户端发送的数据")
    //...
}

使用 for 循环,持续监听并读取来自客户端的数据,直到出现错误或者连接断开。循环不断地执行读取操作,保证服务器可以持续处理客户端发送的消息。

3. 读取前 4 个字节

n, err := conn.Read(buf[:4])
if n != 4 || err != nil {
    fmt.Println("conn.Read err=", err)
    return
}

conn.Read(buf[:4]):从连接 conn 中读取最多 4 个字节,读取的数据存储在 buf 数组的前 4 个字节位置。
返回值 n:表示读取了多少个字节。理想情况下,n 应该为 4,表示完整读取了 4 个字节的数据。
接下来要判断连接是否正常:如果读取的字节数不是 4,即读取不完整,或发生了错误,则输出错误信息并退出 process 函数,终止该连接的处理。

4. 并发处理

在第二节,我讲了客户端与服务端的基本通信操作,在服务端,每个客户端连接都通过新的协程来处理,这使得服务器能够同时与多个客户端进行通信,而不会因为一个客户端的阻塞操作(如读取数据)而影响其他客户端。其中listen.Accept()是关键操作,它的作用是阻塞主协程并等待客户端连接,连接成功后返回一个用于与客户端通信的conn对象

5. 总结与完整代码

server包下的main.go要实现一个简单的并发 TCP 服务器,即
监听 TCP 连接,并发处理,错误处理,以及资源释放,即通过 defer 机制,服务器会在程序结束时正确释放资源(关闭监听器)。完整代码如下:

package main

import (
	"fmt"
	"net"
)

// 处理和客户端的通信
func process(conn net.Conn) {
	//这里需要延时关闭conn
	defer conn.Close()
	buf := make([]byte, 1024*4) //准备缓冲区以读取数据
	//循环读取客户端发送的信息
	for {
		fmt.Println("读取客户端发送的数据")
		n, err := conn.Read(buf[:4])
		if n != 4 || err != nil {
			fmt.Println("conn.Read err=", err)
			return
		}
		fmt.Println("读到的buf=", buf[:4])
	}
}
func main() {
	//提示信息
	fmt.Println("服务器在8889端口监听")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	if err != nil {
		fmt.Println("net.Listen err=", err)
		return
	}
	defer listen.Close()
	//一旦监听成功,就等待客户端来连接服务器
	for {
		fmt.Println("等待客户端来连接服务器......")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept err=", err)
		}
		//一旦连接成功,则启动一个协程和客户端保持通信
		go process(conn)
	}
}

五、测试与注意事项

1.取消安全提示

如果是windows操作系统,在执行server包的程序时,可能会弹出“是否允许公共网络访问”这样的提示,最直接的办法就是关闭防火墙,当然如果你网络编程的时间较长并且一直在上网,这是有风险的。还有种就是将server包下的服务器程序,比如server.exe加入信任列表。当然,前提是编译server包,即在项目根目录下执行如下命令:

go build ./server

然后在win底部菜单栏中(win10/11适用)输入“允许应用通过”,此时会弹出下图:
在这里插入图片描述
点击进入设置界面,再按下图操作:
在这里插入图片描述
然后浏览并添加即可。

2.命令行中的ctrl+c

这里的ctrl+c可不是复制,而是用于命令行中结束程序,一般的命令行比如windows cmd,一旦开启server.exe,只能通过右上角的关闭按钮结束,但如果用powershell执行程序,则用ctrl+c就能结束,这样就能保留命令行记录与操作(按上方向键能查看旧命令)。注意:如果你用的IDE是vscode,则内置终端默认就是powershell,而且可以启动多个终端并分屏查看。至于快捷键调出的外部命令行是无法使用ctrl+c的。

3.执行.exe与测试效果

打开两个powershell命令行,在当前根目录下执行两个编译好的程序client.exe和server.exe:

./server.exe
./client.exe

效果如图:
在这里插入图片描述

OK,以上就是登录模块的核心功能了,这也是网络编程的基础与重点,可以说,不掌握本节内容,是无法入门的。本节内容多,不简单,读者需要多次阅读和编程才能掌握。

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

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

相关文章

java 数据存储方式

1. 变量存储 这是最基本的数据存储方式,通过声明变量来存储数据。变量可以是基本数据类型(如int、float、char等),也可以是引用数据类型(如对象、数组等)。变量存储的数据通常存储在内存中,随着…

有状态(Session) VS 无状态(Token)

目录 概念 JWT Token在项目中使用 概念 有状态和无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理。 1、有状态服务 是指程序在执行过程中生成的中间数据,服务器端一般都要保存请求的相关信息,每个请求可以默认地使…

Hugging Face 任意大模型仓库劫持 - 无声的破坏

摘要 在这篇博客中,我们展示了攻击者如何攻击Hugging Face的Safetensors转换空间及其相关的服务机器人。这些服务是一个在Hugging Face上广受欢迎的服务,专门用于将不安全的机器学习模型转换为更安全的版本。我们随后展示了如何通过Hugging Face自身的服…

C0016.Clion中qDebug()打印输出中文时,都是问号??????的解决办法

问题描述 在clion中使用qDebug打印输出中文内容时,都是?????如下图: 注意:修改该文件的编码格式就行,该文件名为apr.cpp; 解决办法

矩阵求解复数(aniwoth求解串扰)

所以这种求解串扰的格式是因为,有串扰的共轭项在方程组中 复数共轭项的作用,但是这是二次方程,

vue2集成vuex实现网站统一数据管理

文章目录 前言安装配置过程1、安装vuex依赖2、在src目录下创建store文件夹,创建模块site.jsgetters.jsindex.js 3、在man.js中添加vuex vuex实战:存储与获取网站基础数据何时去存储数据?(路由前置获取数据)如何取数据&…

windows中下载、安装、配置JDK/JDK环境配置/Java配置环境变量/Linux中安装配置JDK环境

JDK下载(官网)、安装、配置(包括系统、idea、eclipse)一篇就够了 1、问题概述? Java开发者必须掌握的JDK下载、安装、配置过程。 包括在Eclipse及IDEA中的配置使用 2、下载JDK 【注册Oracle官网账号】 下载的前天是注册orcle官网账号,作为开发者,这个必须有,随时关注…

RTX4060安装nvidia显卡驱动

文章目录 nvidia drivers下载删除原有nvidia驱动安装nvidia驱动如果报错Unable to find the kernel source tree for the currently runningbuilding kernel modules解决方法 报错成功安装!!! nvidia drivers下载 https://www.nvidia.cn/geforce/drivers/#:~:textNVIDIA%20GeF…

ESP01 AT指令学习

一 、AT指令 测试指令:ATCWMODE? 参数及取值范围 cwmode(1-3) 查询指令: ATCWMODE? 当前cwmode的取值 3 设置指令: ATCWMODE3 设置当前的cwmode为 3 1、station 模式 连接到其他wifi 2、softA…

【源码+文档】基于SpringBoot+Vue的酒店管理系统

🚩如何选题? 如何选题、让题目的难度在可控范围,以及如何在选题过程以及整个毕设过程中如何与老师沟通,这些问题是需要大家在选题前需要考虑的,具体的方法我会在文末详细为你解答。 🚭如何快速熟悉一个项目…

如何从 Windows 照片库恢复已删除的照片

数据丢失的主要原因之一是人为错误。更糟糕的是,回收站中没有备份或删除的文件。在这种情况下,数据恢复或适用于 Windows 的专用图片恢复工具可以为您提供帮助,因为它们可以帮助恢复已删除的图片。 牢记这一点,我们将讨论从 Wind…

基于SSM的电影院订票系统设计与实现

文未可获取一份本项目的java源码和数据库参考。 开展本课题的意义及工作内容: 1.意义 系统管理也都将通过计算机进行整体智能化操作,对于电影院订票系统系统所牵扯的管理及数据保存都是非常多的,例如电影信息管理、订单管理等,…

04-SpringBootWeb案例(中)

3. 员工管理 完成了部门管理的功能开发之后,我们进入到下一环节员工管理功能的开发。 基于以上原型,我们可以把员工管理功能分为: 分页查询(今天完成)带条件的分页查询(今天完成)删除员工&am…

压摆率(Slew Rate)

1. 定义 压摆率(Slew Rate)也叫转换速率,是指运算放大器输出电压的最大变化速率,单位通常为伏特每微秒(V/s)。 如果输入信号的变化速率超过运算放大器的压摆率,输出信号将不能即时跟随输入信号…

多线程编程实例

代码&#xff1a; #include<stdio.h> #include<pthread.h> static int run-1; static int retvalue; void *start_routine(void *arg) {int *running arg;printf("child Thread initial over,pass in parameters:%d\n",*running);while(*running){print…

Prometheus与Grafana的完美结合:打造强大的监控与可视化平台

目录 一、Prometheus简介 1.1、Prometheus基本介绍 1.2、Prometheus监控原理 1.3、Prometheus 的局限性 二、部署Prometheus 2.1、使用apt或者yum安装 2.2、基于官方提供的二进制文件安装 2.3、基于docker镜像直接启动或通过docker-compose编排 2.4、基于Operator部署在Kuberne…

使用TensorBoard可视化模型

目录 TensorBoard简介 神经网络模型 可视化 轮次-损失曲线 轮次-准确率曲线 轮次-学习率曲线 迭代-评估准确率曲线 迭代-评估损失曲线 TensorBoard简介 TensorBoard是一款出色的交互式的模型可视化工具。安装TensorFlow时,会自动安装TensorBoard。如图: TensorFlow可…

TI 毫米波雷达——ADC Buffer的交错与非交错采样

TI 毫米波雷达——ADC Buffer的交错与非交错采样 写在前面ADC Buffer 数据存储形式交错采样 (Interleaved mode)1. 4 Rx 通道存储形式2. 3 Rx 通道存储形式3. 2 Rx 通道存储形式 —— RX0/RX1 Enable only4. 1 Rx 通道存储形式 —— RX0 Enable only 非交错采样 (Non-Interleav…

C++:STL常用算法随笔

主要的头文件#include <algorithm> < functional> <numeric> 遍历算法&#xff1a; for_each、transform(搬运容器到另一个容器中 ) void print1(int val) {cout << val <<" "; } for_each (v.begin(),v.end() , print1) 或者用仿…

NVIDIA网卡系列之ConnectX-6 DX规格信息(200G-PCIe 4.0x16-8PF1000VF-2019年发布)

背景 NVIDIA ConnectX-6是最大支持200G的产品&#xff0c;有DX LX等系列。LX一般是25G比较便宜。 核心关键点 200GbpsPCIe 4.0&#xff0c;最大lane: x16 (4.0的lane速 16GT/s * 16 256T/s&#xff0c;所以支持的是200G的网卡用PCIe4.0)QSFPPF&#xff0c;VF数量&#xff1…