最新要闻
- 世界讯息:日系车再受质疑 因发动机缺陷:日产召回超52万辆汽车
- 【环球新视野】3女生拎3斤米酒进站被拦一饮而尽!网友:王宝强听了都头疼
- 天天短讯!比亚迪海豚上演高速路“全自动驾驶” 车主躺后排睡觉
- 每日视点!击败《流浪地球2》!《满江红》成票房冠军:张艺谋大儿子出演 游客排长队打秦桧雕像免票逛岳飞庙
- 自建“出海舰队” 比亚迪花了50亿元买的船:长这模样
- 日本原装进口!雀巢黑咖啡大促:6毛8能泡一杯
- 当前焦点!夏普发布新款PV800UL激光投影仪:亮度高达8000ANSI流明
- 观点:还买啥车!美国人车贷都还不起了 拖欠率比金融危机峰值还高
- 中国反击!新增7项先进科技禁止/限制出口 合计达139项
- 每日短讯:真刺鸡战场!西安一景区设免费抓鸡活动:人鸡比例10:1
- AMD YES!来自小厂的迷你主机 把友商按在地上摩擦
- 世界看点:你敢坐吗?日产汽车联手日立:通过电动汽车为电梯临时供电
- 湖南以前叫什么名字?湖南旅游十大必去景区
- 南菱嫣盛霆旭是什么小说?2023年言情小说推荐
- 创造营2019全部成员有哪些?创造营2019出道成员
- 雪见是哪个电视剧的人物?雪见是哪个演员扮演的角色?
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
天天视点!IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)
本文由金蝶随手记技术团队丁同舟分享。
1、引言
跟移动端IM中追求数据传输效率、网络流量消耗等需求一样,随手记客户端与服务端交互的过程中,对部分数据的传输大小和效率也有较高的要求,普通的数据格式如 JSON 或者 XML 已经不能满足,因此决定采用 Google 推出的 Protocol Buffers 以达到数据高效传输。
本文将基于随手记团队的Protobuf应用实践,分享了Protobuf的技术原理、上手实战等(本篇要分享的是技术原理),希望对你有用。
(相关资料图)
学习交流:
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4114-1-1.html)
2、系列文章
本文是系列文章中的第8篇,本系列总目录如下:
- 《IM通讯协议专题学习(一):Protobuf从入门到精通,一篇就够!》
- 《IM通讯协议专题学习(二):快速理解Protobuf的背景、原理、使用、优缺点》
- 《IM通讯协议专题学习(三):由浅入深,从根上理解Protobuf的编解码原理》
- 《IM通讯协议专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码原理》
- 《IM通讯协议专题学习(五):Protobuf到底比JSON快几倍?全方位实测!》
- 《IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf》(稍后发布..)
- 《IM通讯协议专题学习(七):手把手教你如何在NodeJS中从零使用Protobuf》
- 《IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)》(* 本文)
- 《IM通讯协议专题学习(九):金蝶随手记团队的Protobuf应用实践(实战篇) 》(稍后发布..)
3、基本介绍
Protocol buffers为 Google 提出的一种跨平台、多语言支持且开源的序列化数据格式。相对于类似的 XML 和 JSON,Protocol buffers 更为小巧、快速和简单。其语法目前分为proto2和proto3两种格式。
相对于传统的 XML 和 JSON, Protocol buffers 的优势主要在于:更加小、更加快。
对于自定义的数据结构,Protobuf 可以通过生成器生成不同语言的源代码文件,读写操作都非常方便。
假设现在有下面 JSON 格式的数据:
{
"id":1,
"name":"jojo",
"email":"123@qq.com",
}
使用 JSON 进行编码,得出byte长度为43的的二进制数据:
7b226964 223a312c 226e616d 65223a22 6a6f6a6f 222c2265 6d61696c 223a2231 32334071 712e636f 6d227d
如果使用 Protobuf 进行编码,得到的二进制数据仅有20个字节:
0a046a6f 6a6f1001 1a0a3132 33407171 2e636f6d
4、编码原理
相对于基于纯文本的数据结构如 JSON、XML等,Protobuf 能够达到小巧、快速的最大原因在于其独特的编码方式。《Protobuf从入门到精通,一篇就够!》对 Protobuf 的 Encoding 作了很好的解析。
例如:对于int32类型的数字,如果很小的话,protubuf 因为采用了Varint方式,可以只用 1 个字节表示。
5、Varint原理
Varint 中每个字节的最高位 bit 表示此 byte 是否为最后一个 byte 。1 表示后续的 byte 也表示该数字,0 表示此 byte 为结束的 byte。
例如数字 300 用 Varint 表示为 1010 1100 0000 0010:
▲ 图片源自《Protobuf从入门到精通,一篇就够!》
注意:需要注意解析的时候会首先将两个 byte 位置互换,因为字节序采用了 little-endian 方式。
但 Varint 方式对于带符号数的编码效果比较差。因为带符号数通常在最高位表示符号,那么使用 Varint 表示一个带符号数无论大小就必须要 5 个 byte(最高位的符号位无法忽略,因此对于 -1 的 Varint 表示就变成了 010001)。
Protobuf 引入了 ZigZag 编码很好地解决了这个问题。
6、ZigZag编码
关于 ZigZag 的编码方式,博客园上的一篇博文《整数压缩编码 ZigZag》做出了详细的解释。
ZigZag 编码按照数字的绝对值进行升序排序,将整数通过一个 hash 函数h(n) = (n<<1)^(n>>31)(如果是 sint64 h(n) = (n<<1)^(n>>63))转换为递增的 32 位 bit 流。
关于为什么 64 的 ZigZag 为 80 01,《整数压缩编码 ZigZag》中有关于其编码唯一可译性的解释。
通过 ZigZag 编码,只要绝对值小的数字,都可以用较少位的 byte 表示。解决了负数的 Varint 位数会比较长的问题。
7、T-V and T-L-V
Protobuf 的消息结构是一系列序列化后的Tag-Value对。其中 Tag 由数据的 field 和 writetype组成,Value 为源数据编码后的二进制数据。
假设有这样一个消息:
message Person {
int32 id = 1;
string name = 2;
}
其中,id字段的field为1,writetype为int32类型对应的序号。编码后id对应的 Tag 为(field_number << 3) | wire_type = 0000 1000,其中低位的 3 位标识 writetype,其他位标识field。
每种类型的序号可以从这张表得到:
需要注意,对于string类型的数据(在上表中第三行),由于其长度是不定的,所以 T-V的消息结构是不能满足的,需要增加一个标识长度的Length字段,即T-L-V结构。
8、反射机制
Protobuf 本身具有很强的反射机制,可以通过 type name 构造具体的 Message 对象。陈硕的文章《一种自动反射消息类型的 Google Protobuf 网络传输方案》中对 GPB 的反射机制做了详细的分析和源码解读。这里通过 protobuf-objectivec 版本的源码,分析此版本的反射机制。
陈硕对 protobuf 的类结构做出了详细的分析 —— 其反射机制的关键类为Descriptor类:
每个具体 Message Type 对应一个 Descriptor 对象。尽管我们没有直接调用它的函数,但是Descriptor在“根据 type name 创建具体类型的 Message 对象”中扮演了重要的角色,起了桥梁作用。
同时,陈硕根据 GPB 的 C++ 版本源代码分析出其反射的具体机制:DescriptorPool类根据 type name 拿到一个 Descriptor的对象指针,在通过MessageFactory工厂类根据Descriptor实例构造出具体的Message对象。
示例代码如下:
Message* createMessage(conststd::string& typeName)
{
Message* message = NULL;
constDescriptor* descriptor = DescriptorPool::generated_pool()->FindMessageTypeByName(typeName);
if(descriptor)
{
constMessage* prototype = MessageFactory::generated_factory()->GetPrototype(descriptor);
if(prototype)
{
message = prototype->New();
}
}
returnmessage;
}
注意:
- 1)DescriptorPool 包含了程序编译的时候所链接的全部 protobuf Message types;
- 2)MessageFactory 能创建程序编译的时候所链接的全部 protobuf Message types。
9、以Protobuf-objectivec为例
在 OC 环境下,假设有一份 Message 数据结构如下:
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
解码此类型消息的二进制数据:
Person *newP = [[Person alloc] initWithData:data error:nil];
这里调用了:
- (instancetype)initWithData:(NSData*)data error:(NSError**)errorPtr {
return[selfinitWithData:data extensionRegistry:nilerror:errorPtr];
}
其内部调用了另一个构造器:
- (instancetype)initWithData:(NSData *)data
extensionRegistry:(GPBExtensionRegistry *)extensionRegistry
error:(NSError **)errorPtr {
if((self = [self init])) {
@try {
[self mergeFromData:data extensionRegistry:extensionRegistry];
//...
}
@catch (NSException *exception) {
//...
}
}
return self;
}
去掉一些防御代码和错误处理后,可以看到最终由mergeFromData:方法实现构造:
- (void)mergeFromData:(NSData*)data extensionRegistry:(GPBExtensionRegistry *)extensionRegistry {
GPBCodedInputStream *input = [[GPBCodedInputStream alloc] initWithData:data]; //根据传入的`data`构造出数据流对象
[selfmergeFromCodedInputStream:input extensionRegistry:extensionRegistry]; //通过数据流对象进行merge
[input checkLastTagWas:0]; //校检
[input release];
}
这个方法主要做了两件事:
- 1)通过传入的 data 构造GPBCodedInputStream对象实例;
- 2)通过上面构造的数据流对象进行 merge 操作。
GPBCodedInputStream负责的工作很简单,主要是把源数据缓存起来,并同时保存一系列的状态信息,例如size, lastTag等。
其数据结构非常简单:
typedef struct GPBCodedInputStreamState {
constuint8_t *bytes;
size_t bufferSize;
size_t bufferPos;
// For parsing subsections of an input stream you can put a hard limit on
// how much should be read. Normally the limit is the end of the stream,
// but you can adjust it to anywhere, and if you hit it you will be at the
// end of the stream, until you adjust the limit.
size_t currentLimit;
int32_t lastTag;
NSUIntegerrecursionDepth;
} GPBCodedInputStreamState;
@interface GPBCodedInputStream () {
@package
struct GPBCodedInputStreamState state_;
NSData *buffer_;
}
merge 操作内部实现比较复杂,首先会拿到一个当前 Message 对象的 Descriptor 实例,这个 Descriptor 实例主要保存 Message 的源文件 Descriptor 和每个 field 的 Descriptor,然后通过循环的方式对 Message 的每个 field 进行赋值。
Descriptor 简化定义如下:
@interfaceGPBDescriptor : NSObject
@property(nonatomic, readonly, strong, nullable) NSArray
*fields; @property(nonatomic, readonly, strong, nullable) NSArray
*oneofs; //用于 repeated 类型的 filed @property(nonatomic, readonly, assign) GPBFileDescriptor *file;
@end
其中GPBFieldDescriptor定义如下:
@interface GPBFieldDescriptor () {
@package
GPBMessageFieldDescription *description_;
GPB_UNSAFE_UNRETAINED GPBOneofDescriptor *containingOneof_;
SELgetSel_;
SELsetSel_;
SELhasOrCountSel_; // *Count for map<>/repeated fields, has* otherwise.
SELsetHasSel_;
}
其中GPBMessageFieldDescription保存了 field 的各种信息,如数据类型、filed 类型、filed id等。除此之外,getSel和setSel为这个 field 在对应类的属性的 setter 和 getter 方法。
mergeFromCodedInputStream:方法的简化版实现如下:
- (void)mergeFromCodedInputStream:(GPBCodedInputStream *)input
extensionRegistry:(GPBExtensionRegistry *)extensionRegistry {
GPBDescriptor *descriptor = [selfdescriptor]; //生成当前 Message 的`Descriptor`实例
GPBFileSyntax syntax = descriptor.file.syntax; //syntax 标识.proto文件的语法版本 (proto2/proto3)
NSUInteger startingIndex = 0; //当前位置
NSArray *fields = descriptor->fields_; //当前 Message 的所有 fileds
//循环解码
for(NSUIntegeri = 0; i < fields.count; ++i) {
//拿到当前位置的`FieldDescriptor`
GPBFieldDescriptor *fieldDescriptor = fields[startingIndex];
//判断当前field的类型
GPBFieldType fieldType = fieldDescriptor.fieldType;
if(fieldType == GPBFieldTypeSingle) {
//`MergeSingleFieldFromCodedInputStream` 函数中解码 Single 类型的 field 的数据
MergeSingleFieldFromCodedInputStream(self, fieldDescriptor, syntax, input, extensionRegistry);
//当前位置+1
startingIndex += 1;
} else if(fieldType == GPBFieldTypeRepeated) {
// ...
// Repeated 解码操作
} else{
// ...
// 其他类型解码操作
}
} // for(i < numFields)
}
可以看到,descriptor在这里是直接通过 Message 对象中的方法拿到的,而不是通过工厂构造:
GPBDescriptor *descriptor = [self descriptor];
//`desciptor`方法定义
- (GPBDescriptor *)descriptor {
return [[selfclass] descriptor];
}
这里的descriptor类方法实际上是由GPBMessage的子类具体实现的。
例如在Person这个消息结构中,其descriptor方法定义如下:
+ (GPBDescriptor *)descriptor {
static GPBDescriptor *descriptor = nil;
if(!descriptor) {
static GPBMessageFieldDescription fields[] = {
{
.name = "name",
.dataTypeSpecific.className = NULL,
.number = Person_FieldNumber_Name,
.hasIndex = 0,
.offset = (uint32_t)offsetof(Person__storage_, name),
.flags = GPBFieldOptional,
.dataType = GPBDataTypeString,
},
//...
//每个field都会在这里定义出`GPBMessageFieldDescription`
};
GPBDescriptor *localDescriptor = //这里会根据fileds和其他一系列参数构造出一个`Descriptor`对象
descriptor = localDescriptor;
}
return descriptor;
}
接下来,在构造出 Message 的 Descriptor 后,会对所有的 fields 进行遍历解码。解码时会根据不同的fieldType调用不同的解码函数。
例如对于fieldType == GPBFieldTypeSingle,会调用 Single 类型的解码函数:
MergeSingleFieldFromCodedInputStream(self, fieldDescriptor, syntax, input, extensionRegistry);
MergeSingleFieldFromCodedInputStream内部提供了一系列宏定义,针对不同的数据类型进行数据解码。
#define CASE_SINGLE_POD(NAME, TYPE, FUNC_TYPE) \
caseGPBDataType##NAME: { \
TYPE val = GPBCodedInputStreamRead##NAME(&input->state_); \
GPBSet##FUNC_TYPE##IvarWithFieldInternal(self, field, val, syntax); \
break; \
}
#define CASE_SINGLE_OBJECT(NAME) \
caseGPBDataType##NAME: { \
idval = GPBCodedInputStreamReadRetained##NAME(&input->state_); \
GPBSetRetainedObjectIvarWithFieldInternal(self, field, val, syntax); \
break; \
}
CASE_SINGLE_POD(Int32, int32_t, Int32)
...
#undef CASE_SINGLE_POD
#undef CASE_SINGLE_OBJECT
例如:对于int32类型的数据,最终会调用int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state);函数读取数据并赋值。
这里内部实现其实就是对于 Varint 编码的解码操作:
int32_t GPBCodedInputStreamReadInt32(GPBCodedInputStreamState *state) {
int32_t value = ReadRawVarint32(state);
return value;
}
在对数据解码完成后,拿到一个int32_t,此时会调用GPBSetInt32IvarWithFieldInternal进行赋值操作。
其简化实现如下:
void GPBSetInt32IvarWithFieldInternal(GPBMessage *self,
GPBFieldDescriptor *field,
int32_t value,
GPBFileSyntax syntax) {
//最终的赋值操作
//此处`self`为`GPBMessage`实例
uint8_t *storage = (uint8_t *)self->messageStorage_;
int32_t *typePtr = (int32_t *)&storage[field->description_->offset];
*typePtr = value;
}
其中typePtr为当前需要赋值的变量的指针。至此,单个 field 的赋值操作已经完成。
总结一下,在 protobuf-objectivec 版本中,反射机制中构建 Message 对象的流程大致为:
- 1)通过 Message 的具体子类构造其 Descriptor,Descriptor 中包含了所有 field 的 FieldDescriptor;
- 2)循环通过每个 FieldDescriptor 对当前 Message 对象的指定 field 赋值。
10、参考资料
[1]Protobuf 官方开发者指南(中文译版)
[2]Protobuf官方手册
[3]Why do we use Base64?
[4]The Base16, Base32, and Base64 Data Encodings
[5]Protobuf从入门到精通,一篇就够!
[5]如何选择即时通讯应用的数据传输格式
[7]强列建议将Protobuf作为你的即时通讯应用数据传输格式
[8]APP与后台通信数据格式的演进:从文本协议到二进制协议
[9]面试必考,史上最通俗大小端字节序详解
[10]移动端IM开发需要面对的技术问题(含通信协议选择)
[11]简述移动端IM开发的那些坑:架构设计、通信协议和客户端
[12]理论联系实际:一套典型的IM通信协议设计详解
[13]58到家实时消息系统的协议设计等技术实践分享
(本文已同步发布于:http://www.52im.net/thread-4114-1-1.html)
-
天天视点!IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)
本文将基于随手记团队的Protobuf应用实践,分享了Protobuf的技术原理、上手实战等(本篇要分享的是技术...
来源: 天天视点!IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)
世界热门:VS Code保存后自动格式化Vue代码
世界讯息:日系车再受质疑 因发动机缺陷:日产召回超52万辆汽车
世界速看:(笔记)运算放大器经典应用电路及工作原理
【网关开发】6.lua绑定委托(delegate)实现多播调用
【环球新视野】3女生拎3斤米酒进站被拦一饮而尽!网友:王宝强听了都头疼
天天短讯!比亚迪海豚上演高速路“全自动驾驶” 车主躺后排睡觉
每日视点!击败《流浪地球2》!《满江红》成票房冠军:张艺谋大儿子出演 游客排长队打秦桧雕像免票逛岳飞庙
自建“出海舰队” 比亚迪花了50亿元买的船:长这模样
日本原装进口!雀巢黑咖啡大促:6毛8能泡一杯
当前通讯!看个小说竟然像在照镜子!
数据库容灾等级
后端跨域问题导致java.io.IOException: UT010029: Stream is closed解决办法
当前焦点!夏普发布新款PV800UL激光投影仪:亮度高达8000ANSI流明
观点:还买啥车!美国人车贷都还不起了 拖欠率比金融危机峰值还高
中国反击!新增7项先进科技禁止/限制出口 合计达139项
MySQL索引底层探究
热头条丨感受 Vue3 的魔法力量
每日短讯:真刺鸡战场!西安一景区设免费抓鸡活动:人鸡比例10:1
AMD YES!来自小厂的迷你主机 把友商按在地上摩擦
世界看点:你敢坐吗?日产汽车联手日立:通过电动汽车为电梯临时供电
湖南以前叫什么名字?湖南旅游十大必去景区
南菱嫣盛霆旭是什么小说?2023年言情小说推荐
创造营2019全部成员有哪些?创造营2019出道成员
雪见是哪个电视剧的人物?雪见是哪个演员扮演的角色?
不要抛下绮绮是什么意思?王者荣耀流行梗有哪些?
大金空调是哪个国家的品牌?大金空调不制热怎么回事?
环球快消息!【如何提高IT运维效率】深度解读京东云基于NLP的运维日志异常检测AIOps落地实践
环球实时:别再写狗屎代码了,推荐这 5 款 IDEA 插件,让你的代码质量直接起飞!
将实体光盘制作成光盘映像iso文件
输入法切换不了是什么原因?输入法切换不了怎么解决?
小米6x什么时候发布的?小米6x详细参数
qq游戏大厅在哪里打开?qq游戏大厅怎么多开?
注册表编辑器是干什么的?注册表编辑器怎么恢复默认设置?
【当前热闻】《流浪地球2》周边众筹已超4500万!最初目标仅仅10万
中国“宁王”成功出海 宁德时代首座海外工厂投产
全球头条:springboot~logback按level添加不同的颜色
春节假期有车走应急车道 视频车拍照举报还得数百元红包奖励
环球百事通!最勤劳“小兔子”行驶1500米、数据940GB!玉兔二号传回新玉照
当前热讯:中国玩家的电子阳痿:被日本“老中医”彻底治好了
聚焦:读Java8函数式编程笔记03_高级集合类和收集器
【天天快播报】男子与女友吵完架开车2分钟扣22分 逆行、闯红灯等:网友看完害怕
最新:男子春节逆向旅游深圳承包整片沙滩:通过房价得出判断
中国春节档电影市场重焕活力:总票房破67亿 列历史第2
头条焦点:AX9000安装使用Docker
天天看点:理想L9高速“失灵” 追尾“自杀式并线”车引争议:车主自找的?
车企年度销量目标完成率:比亚迪一枝独秀 长城、长安惨不忍睹
今日热议:唯一/普通索引的选择?change buffer
焦点消息!AMD RX 400/500老显卡尴尬不能跑新游戏:同时代N卡却没问题
当前视点!暗物质:宇宙中最神秘的物质之一 已经逼疯科学家了
环球观天下!RabbitMQ介绍
图省钱去开电动汽车:在美国根本不存在
天天快资讯丨玩法BT!真人版《鱿鱼游戏》出意外:多人受伤
世界速看:MQ的相关概念
Python字符串
当前关注:女子过年练车坠河:一家3人不幸遇难 还是大学生
环球快播:学习笔记——安卓的下载路径;创建一个空的安卓project;Android中的日志工具划分
滚动:史上最好Windows系统!微软要对Win11首个正式版强制升级22H2了
【全球报资讯】马斯克称中国竞争对手最努力最聪明:最有可能仅次于特斯拉
每日快讯!Cybertruck又跳票了!特斯拉首款电动皮卡量产要等到2024年
2022手机战事骁龙精彩收官:新的好戏要开场了!
全球微头条丨Codeforces Round #601 (Div. 2) A-E
世界快报:敏感肌适用 露得清氨基酸洗面奶19.9元白菜价:3.3折狂促
全球要闻:POJ 1185 炮兵阵地
每日动态![概率论与数理统计]笔记:4.3 常用的统计分布
通讯!刘德华吴京《流浪地球2》电影里重回20岁 吴京:没有被年轻俊美吓到吧
【天天新要闻】2G/3G退网 怎就这么难
【环球速看料】[NOIP2016提高组] 愤怒的小鸟
焦点速看:33.98万元起买吗?理想L7内部空间图公布:感受一下到底有多能装
男子花20多万三亚度假遭遇节约型爸妈 网友热议:过度节约才是更大浪费
全球热点!操作系统的概念、功能和目标
今日热讯:阿里回应将在新加坡建“第一高楼”当全球总部:出生在杭州 生长在杭州 发展在杭州
中国团队开发液体机器人成功越狱:复刻《终结者2》T-1000名场面
密钥封装和公钥加密的联系和区别?
环球观热点:将awk脚本写在文件里:一种高效的awk循环循环方式
【世界播资讯】男子在鱼疗池睡着“生吞”小鱼 网友:鱼生从未体验的味道
要闻速递:国航航班颠簸下坠乘客录视频遗言以防不测:全飞机的人都在尖叫
学习笔记——redis数据类型(ZSet)
世界微速讯:这几个月的二手车 可能是最香的
Hexo 修改默认文章路径
【世界热闻】-53℃的漠河启动i9-13900K、RTX 4090!魔幻一幕出现
当前热点-男孩逛景区遇现实版“鹈鹕灌顶”:小心确实有攻击性
Educational Codeforces Round 1
动态焦点:女儿返程点千元外卖塞满父母冰箱:感恩双亲 过去他们把我行李箱装满
【世界新视野】顽皮狗总监:《神秘海域》不会再出了
环球速看:Windows开发的瑞士军刀,NewSPYLite发布
当前最新:学习笔记——redis中的数据类型(List、Set、Hash)
信息:【算法训练营day27】LeetCode39. 组合总和 LeetCode40. 组合总和II LeetCode131. 分割回文串
【环球新要闻】windows2003 的安装以及安装时遇到的问题
世界焦点!大规模实测199颗i9-13900KS:6GHz的秘密找到了!真神仙
天天信息:PS5 Edge手柄续航差原因找到了:电池容量缩水1/3
首款支持NVIDIA RTX I/O秒进游戏的大作终于来了!但是平均帧率降了10%
每日速看!家长带三胞胎爬五指山 三大三小全被困:21小时才救出来
全球微资讯!希捷搞定50+TB硬盘!但还得等3年
《流浪地球2》火爆 吴京恳请大家不要再宣传300亿票房:会觉得内疚
20年前 1个啤酒瓶能卖5毛钱 为什么在没人收了?
焦点速读:狗狗走丢一个月回家疯狂撞门:为啥土狗能找到回家的路 宠物狗却不能?
门店359元:鸿星尔克腾蛇2.0老爹鞋149元大促
读Java8函数式编程笔记02_流
苹果在三大战场向谷歌发起“无声战斗”:让iOS远离Android