go读取excel游戏配置
1.背景
游戏服务器,配置数据一般采用csv/excel来作为载体,这种方式,策划同学配置方便,服务器解析也方便。在jforgame框架里,我们使用以下的excel配置格式。

然后可以非常方便的进行数据检索,例如:

本文使用go实现类似的功能。
2.读取excel
2.1.使用github.com/tealeg/xlsx 库
github.com/tealeg/xlsx 是一个流行的 Go 语言库,用于读取和写入 Excel 文件。
定义数据读取接口,既可以选择excel格式,也可以拓展成csv等格式。
package dataimport "io"type DataReader interface {Read(io.Reader, interface{}) ([]interface{}, error)
}
Excel实现
package dataimport ("encoding/json""fmt""reflect""strconv""strings""github.com/tealeg/xlsx"
)type ExcelDataReader struct {ignoreUnknownFields bool
}func NewExcelDataReader(ignoreUnknownFields bool) *ExcelDataReader {return &ExcelDataReader{ignoreUnknownFields: ignoreUnknownFields,}
}func (r *ExcelDataReader) Read(filePath string, clazz interface{}) ([]interface{}, error) {// 使用 xlsx.OpenFile 打开 Excel 文件xlFile, err := xlsx.OpenFile(filePath)if err != nil {return nil, fmt.Errorf("failed to open Excel file: %v", err)}sheet := xlFile.Sheets[0]rows := sheet.Rowsvar headers []CellHeadervar records [][]CellColumn// 遍历每一行for _, row := range rows {firstCell := getCellValue(row.Cells[0])if firstCell == "HEADER" {headers, err = r.readHeader(clazz, row.Cells)if err != nil {return nil, err}continue}if len(headers) == 0 {continue}record := r.readExcelRow(headers, row)records = append(records, record)if firstCell == "end" {break}}return r.readRecords(clazz, records)
}func (r *ExcelDataReader) readRecords(clazz interface{}, rows [][]CellColumn) ([]interface{}, error) {var records []interface{}clazzType := reflect.TypeOf(clazz).Elem()for _, row := range rows {obj := reflect.New(clazzType).Elem()for _, column := range row {colName := column.Header.Columnif colName == "" {continue}// 根据 Tag 查找字段field, err := findFieldByTag(obj, colName)if err != nil {if !r.ignoreUnknownFields {return nil, err}continue}fieldVal, err := convertValue(column.Value, field.Type())if err != nil {return nil, err}field.Set(reflect.ValueOf(fieldVal))}records = append(records, obj.Interface())}return records, nil
}func (r *ExcelDataReader) readHeader(clazz interface{}, cells []*xlsx.Cell) ([]CellHeader, error) {var headers []CellHeaderfor _, cell := range cells {cellValue := getCellValue(cell)if cellValue == "HEADER" {continue}header := CellHeader{Column: cellValue,}headers = append(headers, header)}return headers, nil
}func getCellValue(cell *xlsx.Cell) string {if cell == nil {return ""}return cell.String()
}func (r *ExcelDataReader) readExcelRow(headers []CellHeader, row *xlsx.Row) []CellColumn {var columns []CellColumnfor i, cell := range row.Cells {// 忽略 header 所在的第一列if i == 0 {continue}if i >= len(headers) {break}cellValue := getCellValue(cell)column := CellColumn{// headers 从 0 开始,所以这里 -1Header: headers[i-1],Value: cellValue,}columns = append(columns, column)}return columns
}func convertValue(value string, fieldType reflect.Type) (interface{}, error) {switch fieldType.Kind() {case reflect.String:return value, nilcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:return strconv.ParseInt(value, 10, 64)case reflect.Float32, reflect.Float64:return strconv.ParseFloat(value, 64)case reflect.Bool:return strconv.ParseBool(value)case reflect.Slice, reflect.Struct:// 处理嵌套的 JSON 对象fieldVal := reflect.New(fieldType).Interface()if err := json.Unmarshal([]byte(value), &fieldVal); err != nil {return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)}return reflect.ValueOf(fieldVal).Elem().Interface(), nildefault:return nil, fmt.Errorf("unsupported type: %v", fieldType.Kind())}
}// 根据 Tag 查找字段
func findFieldByTag(obj reflect.Value, tagValue string) (reflect.Value, error) {objType := obj.Type()for i := 0; i < objType.NumField(); i++ {field := objType.Field(i)tag := field.Tag.Get("excel") // 获取 Tag 值if strings.EqualFold(tag, tagValue) { // 忽略大小写匹配return obj.Field(i), nil}}return reflect.Value{}, fmt.Errorf("field with tag %s not found", tagValue)
}type CellHeader struct {Column stringField reflect.Value
}type CellColumn struct {Header CellHeaderValue string
}
2.2.主要技术点
这里有几个需要注意的点
2.2.1.go结构体变量与excel字段分离
go使用首字母大写来标识一个变量是否包外可见,如果直接使用go的反射api,需要将excel的字段定义成大写,两者强绑定在一起,不方便。为了支持代码与配置命名的分离,可以使用go的tag定义,通过把excel的字段名称,写在struct的tag注释。有点类似于java的注解。
type Item struct {Id int64 `json:"id" excel:"id"`Name string `json:"name" excel:"name"`Quality int64 `json:"quality" excel:"quality"`Tips string `json:"tips" excel:"tips"`Icon string `json:"icon" excel:"icon"`
}
代码片段
// 根据 Tag 查找字段
func findFieldByTag(obj reflect.Value, tagValue string) (reflect.Value, error) {objType := obj.Type()for i := 0; i < objType.NumField(); i++ {field := objType.Field(i)tag := field.Tag.Get("excel") // 获取 Tag 值if strings.EqualFold(tag, tagValue) { // 忽略大小写匹配return obj.Field(i), nil}}return reflect.Value{}, fmt.Errorf("field with tag %s not found", tagValue)
}
2.2.2.exce支持嵌套结构
程序员很喜欢配置直接使用json格式,这样代码具有很高的拓展性,当策划改配置,只要不添加新类型,都可以无需程序介入。(其实大部分策划很讨厌json格式,配置容易出错,而且excel的自动公式无法很智能地工作)
例如下面的配置

结构体定义
type RewardDef struct {Type string `json:"type" excel:"type"`Value string `json:"value" excel:"value"`
}type ConsumeDef struct {Type string `json:"type" excel:"type"`Value string `json:"value" excel:"value"`
}type Mall struct {Id int64 `json:"id" excel:"id"`Type int64 `json:"type" excel:"type"`Name string `json:"name" excel:"name"`Rewards []RewardDef `json:"rewards" excel:"rewards"`Consumes []ConsumeDef `json:"consumes" excel:"consumes"`
}
主要代码
func convertValue(value string, fieldType reflect.Type) (interface{}, error) {switch fieldType.Kind() {case reflect.String:return value, nilcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:return strconv.ParseInt(value, 10, 64)case reflect.Float32, reflect.Float64:return strconv.ParseFloat(value, 64)case reflect.Bool:return strconv.ParseBool(value)case reflect.Slice, reflect.Struct:// 处理嵌套的 JSON 对象fieldVal := reflect.New(fieldType).Interface()if err := json.Unmarshal([]byte(value), &fieldVal); err != nil {return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)}return reflect.ValueOf(fieldVal).Elem().Interface(), nildefault:return nil, fmt.Errorf("unsupported type: %v", fieldType.Kind())}
}
2.3.单元测试用例
由于go只有main包能使用main函数,为了对我们的工具进行测试,我们可以直接使用类的单元测试。
新建一个文件excel_test.go(必须以_test结尾)
package dataimport ("fmt""io/github/gforgame/logger""testing"
)func TestExcelReader(t *testing.T) {// 创建 ExcelDataReader 实例reader := NewExcelDataReader(true)type RewardDef struct {Type string `json:"type" excel:"type"`Value string `json:"value" excel:"value"`}type ConsumeDef struct {Type string `json:"type" excel:"type"`Value string `json:"value" excel:"value"`}type Name struct {Id int64 `json:"id" excel:"id"`Name string `json:"type" excel:"name"`Rewards []RewardDef `json:"rewards" excel:"rewards"`Consumes []ConsumeDef `json:"consumes" excel:"consumes"`}// 读取 Excel 文件result, err := reader.Read("mall.xlsx", &Name{})if err != nil {logger.Error(fmt.Errorf("session.Send: %v", err))}// 打印结果for _, item := range result {fmt.Printf("%+v\n", item)}
}
3.数据载体
3.1.数据容器定义
读取excel文件,得到的是一个记录数组,我们还需要进一步进行封装,方便业务代码使用。
所以我们还需要把这批数据塞入到一个容器里,并且容器应该提供至少以下API。
// GetRecord 根据 ID 获取单个记录
func (c *Container[K, V]) GetRecord(id K) (V, bool) {}// GetAllRecords 获取所有记录
func (c *Container[K, V]) GetAllRecords() []V {}// GetRecordsBy 根据索引名称和索引值获取记录
func (c *Container[K, V]) GetRecordsBy(name string, index interface{}) []V {}
该容器必须支持泛型,适配不同的表定义。代码如下:
package dataimport "fmt"// Container 是一个通用的数据容器,支持按 ID 查询、按索引查询和查询所有记录
type Container[K comparable, V any] struct {data map[K]V // 存储 ID 到记录的映射indexMapper map[string][]V // 存储索引到记录的映射
}// NewContainer 创建一个新的 Container 实例
func NewContainer[K comparable, V any]() *Container[K, V] {return &Container[K, V]{data: make(map[K]V),indexMapper: make(map[string][]V),}
}// Inject 将数据注入容器,并构建索引
func (c *Container[K, V]) Inject(records []V, getIdFunc func(V) K, indexFuncs map[string]func(V) interface{}) {for _, record := range records {id := getIdFunc(record)c.data[id] = record// 构建索引for name, indexFunc := range indexFuncs {indexValue := indexFunc(record)key := indexKey(name, indexValue)c.indexMapper[key] = append(c.indexMapper[key], record)}}
}// GetRecord 根据 ID 获取单个记录
func (c *Container[K, V]) GetRecord(id K) (V, bool) {record, exists := c.data[id]return record, exists
}// GetAllRecords 获取所有记录
func (c *Container[K, V]) GetAllRecords() []V {records := make([]V, 0, len(c.data))for _, record := range c.data {records = append(records, record)}return records
}// GetRecordsBy 根据索引名称和索引值获取记录
func (c *Container[K, V]) GetRecordsBy(name string, index interface{}) []V {key := indexKey(name, index)return c.indexMapper[key]
}// indexKey 生成索引键
func indexKey(name string, index interface{}) string {return fmt.Sprintf("%s@%v", name, index)
}
对于java版本的游戏服务器框架,配置表定义格式如下:
/*** 成就表*/
@Setter
@Getter
@DataTable(name = "achievement")
public class AchievementData {@Idprivate int id;/*** 名字*/private String name;/*** 排序*/private int rank;/*** 类型*/@Indexprivate int type;/*** 条件,每个类型自行定义配置结构*/private String target;}
通过@Id注解定义主键,通过@Index注解定义索引。程序业务代码示例:
// 查询单条记录
AchievementData achievementData = GameContext.dataManager.queryById(AchievementData.class, 1);
// 查询指定索引的所有记录
List<AchievementData> records = GameContext.dataManager.queryByIndex(AchievementData.class, "type", type);
由于go目前不支持注解,无法通过注解让程序自动识别哪一个字段为主键,所以对于每一个容器,需要定义一个函数,手动标识应该取哪一个字段。
// 定义 ID 获取函数和索引函数getIdFunc := func(record Mall) int64 {return record.Id}
按索引取记录的逻辑也是同样的道理。
// 将记录注入容器nameRecords := make([]Mall, len(records))for i, record := range records {nameRecords[i] = record.(Mall)}
单元测试代码
func TestDataContainer(t *testing.T) {// 创建 ExcelDataReaderreader := NewExcelDataReader(true)// 读取 Excel 文件records, err := reader.Read("mall.xlsx", &Mall{})if err != nil {fmt.Println("Failed to read Excel file:", err)return}// 创建 Containercontainer := NewContainer[int64, Mall]()// 定义 ID 获取函数和索引函数getIdFunc := func(record Mall) int64 {return record.Id}indexFuncs := map[string]func(Mall) interface{}{"type": func(record Mall) interface{} {return record.Type},}// 将记录注入容器nameRecords := make([]Mall, len(records))for i, record := range records {nameRecords[i] = record.(Mall)}container.Inject(nameRecords, getIdFunc, indexFuncs)// 查询记录fmt.Println("All records:", container.GetAllRecords())target, _ := container.GetRecord(1)fmt.Println("Record with ID 1:", target)fmt.Println("Records with type 2:", container.GetRecordsBy("type", 2))
}
3.2.适配不同的表配置
从上面的代码可以看出,对于一份excel配置,每次都要复制一段非常相似的代码,无疑非常繁琐。所以我们对以上的代码进一步封装。
首先,定义各种表的元信息(java可通过注解定义)
type TableMeta struct {TableName string // 表名IDField string // ID 字段名IndexFuncs map[string]string // 索引字段名 -> 索引名称RecordType reflect.Type // 记录类型
}
将excel配置注入容器
func ProcessTable(reader *ExcelDataReader, filePath string, config TableMeta) (*Container[int64, interface{}], error) {// 读取 Excel 文件records, err := reader.Read(filePath, reflect.New(config.RecordType).Interface())if err != nil {return nil, fmt.Errorf("failed to read table %s: %v", config.TableName, err)}// 创建 Containercontainer := NewContainer[int64, interface{}]()// 定义 ID 获取函数getIdFunc := func(record interface{}) int64 {val := reflect.ValueOf(record)// 如果 record 是指针,则调用 Elem() 获取实际值if val.Kind() == reflect.Ptr {val = val.Elem()}field := val.FieldByName(config.IDField)return field.Int()}// 定义索引函数indexFuncs := make(map[string]func(interface{}) interface{})if config.IndexFuncs != nil {for indexName, fieldName := range config.IndexFuncs {indexFuncs[indexName] = func(record interface{}) interface{} {val := reflect.ValueOf(record)// 如果 record 是指针,则调用 Elem() 获取实际值if val.Kind() == reflect.Ptr {val = val.Elem()}field := val.FieldByName(fieldName)return field.Interface()}}}// 将记录注入容器container.Inject(records, getIdFunc, indexFuncs)return container, nil
}
在jforgame的版本实现,利用java的类扫描,可以非常方便把所有配置容器一次性扫描并注册,如下:
public void init() {if (!StringUtils.isEmpty(properties.getContainerScanPath())) {Set<Class<?>> containers = ClassScanner.listAllSubclasses(properties.getContainerScanPath(), Container.class);containers.forEach(c -> {// container命名必须以配置文件名+Container,例如配置表为common.csv,则对应的Container命名为CommonContainerString name = c.getSimpleName().replace("Container", "").toLowerCase();containerDefinitions.put(name, (Class<? extends Container>) c);});}Set<Class<?>> classSet = ClassScanner.listClassesWithAnnotation(properties.getTableScanPath(), DataTable.class);classSet.forEach(this::registerContainer);}
go目前不支持类扫描这种元编程,我们只能通过手动注册。
// 定义表配置tableConfigs := []TableMeta{// 商城表{TableName: "mall",IDField: "Id",IndexFuncs: map[string]string{"type": "Type"},RecordType: reflect.TypeOf(Mall{}),},// 道具表{TableName: "item",IDField: "Id",RecordType: reflect.TypeOf(Item{}),},}
3.3.单元测试用例
func TestMultiDataContainer(t *testing.T) {// 创建 ExcelDataReaderreader := NewExcelDataReader(true)// 定义表配置tableConfigs := []TableMeta{// 商城表{TableName: "mall",IDField: "Id",IndexFuncs: map[string]string{"type": "Type"},RecordType: reflect.TypeOf(Mall{}),},// 道具表{TableName: "item",IDField: "Id",RecordType: reflect.TypeOf(Item{}),},}// 处理每张表containers := make(map[string]*Container[int64, interface{}])for _, config := range tableConfigs {container, err := ProcessTable(reader, config.TableName+".xlsx", config)if err != nil {fmt.Printf("Failed to process table %s: %v\n", config.TableName, err)continue}containers[config.TableName] = container}// 查询商城记录mallContainer := containers["mall"]fmt.Println("All records in Mall table:", mallContainer.GetAllRecords())target, _ := mallContainer.GetRecord(1)fmt.Println("Record with ID 1:", target)fmt.Println("Records with type 2 in Mall table:", mallContainer.GetRecordsBy("type", 2))// 查询商城记录itemContainer := containers["item"]fmt.Println("All records in Mall table:", itemContainer.GetAllRecords())target2, _ := itemContainer.GetRecord(1)fmt.Println("Record with ID 1:", target2)
}
完整代码请移步:
--> go游戏服务器
相关文章:
go读取excel游戏配置
1.背景 游戏服务器,配置数据一般采用csv/excel来作为载体,这种方式,策划同学配置方便,服务器解析也方便。在jforgame框架里,我们使用以下的excel配置格式。 然后可以非常方便的进行数据检索,例如ÿ…...
特殊类设计
[本节目标] 掌握常见特殊类的设计方式 1.请设计一个类,不能被拷贝 拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。 C98 将拷贝构…...
图像去雾数据集的下载和预处理操作
前言 目前,因为要做对比实验,收集了一下去雾数据集,并且建立了一个数据集的预处理工程。 这是以前我写的一个小仓库,我决定还是把它用起来,下面将展示下载的路径和数据处理的方法。 下面的代码均可以在此找到。Auo…...
【LeetCode】--- MySQL刷题集合
1.组合两个表(外连接) select p.firstName,p.lastName,a.city,a.state from Person p left join Address a on p.personId a.personId; 以左边表为基准,去连接右边的表。取两表的交集和左表的全集 2.第二高的薪水 (子查询、if…...
基于Python的多元医疗知识图谱构建与应用研究(上)
一、引言 1.1 研究背景与意义 在当今数智化时代,医疗数据呈爆发式增长,如何高效管理和利用这些数据,成为提升医疗服务质量的关键。传统医疗数据管理方式存在数据孤岛、信息整合困难等问题,难以满足现代医疗对精准诊断和个性化治疗的需求。知识图谱作为一种知识表示和管理…...
小哆啦解题记:如何计算除自身以外数组的乘积
小哆啦开始力扣每日一题的第十二天 https://leetcode.cn/problems/product-of-array-except-self/description/ 《小哆啦解题记:如何计算除自身以外数组的乘积》 在一个清晨的阳光下,小哆啦坐在书桌前,思索着一道困扰已久的题目:…...
渐进式图片的实现原理
渐进式图片(Progressive JPEG)的实现原理与传统的基线 JPEG(Baseline JPEG)不同。它通过改变图片的编码和加载方式,使得图片在加载时能够逐步显示从模糊到清晰的图像。 1. 传统基线 JPEG 的加载方式 在传统的基线 JP…...
SQL刷题快速入门(三)
其他章节: SQL刷题快速入门(一) SQL刷题快速入门(二) 承接前两个章节,本系列第三章节主要讲SQL中where和having的作用和区别、 GROUP BY和ORDER BY作用和区别、表与表之间的连接操作(重点&…...
mybatis(19/134)
大致了解了一下工具类,自己手敲了一边,java的封装还是真的省去了很多麻烦,封装成一个工具类就可以不用写很多重复的步骤,一个工厂对应一个数据库一个environment就好了。 mybatis中调用sql中的delete占位符里面需要有字符…...
sqlmap 自动注入 -01
1: 先看一下sqlmap 的help: 在kali-linux 系统里面,可以sqlmap -h看一下: Target: At least one of these options has to be provided to define the target(s) -u URL, --urlURL Target URL (e.g. "Salesforce Platform for Application Development | Sa…...
3.8.Trie树
Trie树 Trie 树,又称字典树或前缀树,是一种用于高效存储和检索字符串数据的数据结构,以下是关于它的详细介绍: 定义与原理 定义:Trie 树是一种树形结构,每个节点可以包含多个子节点,用于存储…...
day 21
进程、线程、协程的区别 进程:操作系统分配资源的最小单位,其中可以包含一个或者多个线程,进程之间是独立的,可以通过进程间通信机制(管道,消息队列,共享内存,信号量,信…...
基于模板方法模式-消息队列发送
基于模板方法模式-消息队列发送 消息队列广泛应用于现代分布式系统中,作为解耦、异步处理和流量控制的重要工具。在消息队列的使用中,发送消息是常见的操作。不同的消息队列可能有不同的实现方式,例如,RabbitMQ、Kafka、RocketMQ…...
俄语画外音的特点
随着全球媒体消费的增加,语音服务呈指数级增长。作为视听翻译和本地化的一个关键方面,画外音在确保来自不同语言和文化背景的观众能够以一种真实和可访问的方式参与内容方面发挥着重要作用。说到俄语,画外音有其独特的特点、挑战和复杂性&…...
PyTorch使用教程(10)-torchinfo.summary网络结构可视化详细说明
1、基本介绍 torchinfo是一个为PyTorch用户量身定做的开源工具,其核心功能之一是summary函数。这个函数旨在简化模型的开发与调试流程,让模型架构一目了然。通过torchinfo的summary函数,用户可以快速获取模型的详细结构和统计信息࿰…...
亚博microros小车-原生ubuntu支持系列:5-姿态检测
MediaPipe 介绍参见:亚博microros小车-原生ubuntu支持系列:4-手部检测-CSDN博客 本篇继续迁移姿态检测。 一 背景知识 以下来自亚博官网 MediaPipe Pose是⼀个⽤于⾼保真⾝体姿势跟踪的ML解决⽅案,利⽤BlazePose研究,从RGB视频…...
C语言之高校学生信息快速查询系统的实现
🌟 嗨,我是LucianaiB! 🌍 总有人间一两风,填我十万八千梦。 🚀 路漫漫其修远兮,吾将上下而求索。 C语言之高校学生信息快速查询系统的实现 目录 任务陈述与分析 问题陈述问题分析 数据结构设…...
WPF基础 | WPF 基础概念全解析:布局、控件与事件
WPF基础 | WPF 基础概念全解析:布局、控件与事件 一、前言二、WPF 布局系统2.1 布局的重要性与基本原理2.2 常见布局面板2.3 布局的测量与排列过程 三、WPF 控件3.1 控件概述与分类3.2 常见控件的属性、方法与事件3.3 自定义控件 四、WPF 事件4.1 路由事件概述4.2 事…...
迷宫1.2
先发一下上次的代码 #include<bits/stdc.h> #include<windows.h> #include <conio.h> using namespace std; char a[1005][1005]{ " ", "################", "# # *#", "# # # #&qu…...
RabbitMQ---应用问题
(一)幂等性介绍 幂等性是本身是数学中的运算性质,他们可以被多次应用,但是不会改变初始应用的结果 1.应用程序的幂等性介绍 包括很多,有数据库幂等性,接口幂等性以及网络通信幂等性等 就比如数据库的sel…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...
uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
