Unix标准IO库相关函数总结之打开关闭流(一)

Unix标准IO类型FILE

在Unix相关的文件IO中几乎所有的函数都用到了文件描述符,文件描述符是打开一个文件时返回的一个可用的最小的文件描述标识。相应的在Unix标准IO相关的函数中,几乎每个函数都用到了FILE数据类型。本小结简单介绍一下FILE结构体的内容。

FILE实际上是一个struct的typedef,可以在/usr/include/stdio.h中找到它的定义为:

1
typedef _IO_FILE FILE;

_IO_FILE_的定义在文件/usr/include/libio.h中,我们可以看到它的具体定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

从上面的struct中可以观察到FILE中是有文件描述符标志的,即为fileno参数。

打开标准IO流

打开标准文件IO流的方法有如下三个:

1
2
3
4
5
FILE* fopen(const char* restrict pathname, const char* restrict type);
FILE* freopen(const char* restrict pathname, const char* restrict type, FILE* restrict fp);
FILE* fdopen(int fd, const char* type);

上述三个函数区别以及使用场景说明:

  • fopen的作用是打开一个指定文件路径的文件流。
  • freopen的作用是在一个指定的留上打开一个指定文件。如果当前流已经打开,则先关闭该流;若该流被重定向,则清除重定向。该函数常用于将一个指定的文件打开为一个默认的流,如若想使用printf函数将打印出来的内容输出到一个指定文件中,此时可以讲stdout重定向到指定的文件路径并指定打开模式。
  • fdopen的作用是将一个已经打开的文件描述符(该文件描述符可能是open,dup,socket等获取的)绑定到一个标准IO流上。此函数常用于由管道和网络通信通道函数返回的文件描述符,因为这些无法显式的指定文件。

参数说明:

  • pathname: 文件路径,相对或绝对
  • type: 打开模式(r,w,a,r+,w+,a+以及所有后面加b)

打开模式说明:

  • r : 读模式
  • w : 写模式,在写之前将原有文件内容全部清楚
  • a : 追加模式,offset为文件结尾
  • r+ : 读写模式(不删除文件原有内容,offset初始为文件开头)。如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”abc456”。
  • w+ : 读写模式(删除文件原有内容,offset为文件开头).如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”abc”。
  • a+ : 读写模式(offset为文件结尾).如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”123456abc”。

上述所有的模式后面都加上b表示对二进制文件的操作(rb,wb,ab,rb+,wb+,ab+)。

注意:对于fdopen函数由于文件已经由文件描述符打开,此时w模式时将不清除文件内容,追加模式不常见文件。

当以读写模式打开文件时候,将有一些限制。如果中间没有fflush,fseek,fsetpos以及rewind函数,标注输出之后不能直接进行输入;如果没有fseek,fsetpos或者rewind,或者一个输入没有到达文件尾,则输入操作之后不能跟输出操作。

关闭标准文件流

一般情况下在关闭文件流之前通常先使用fflush刷新缓冲区防止数据丢失,关闭标准文件流使用的函数为:

1
2
#include <stdio.h>
int fclose(FILE* fp);

Comment and share

TCP状态转换图

TCP涉及连接建立和连接终止的操作可以用状态转换图来说明。

TCP为一个连接定义了11中状态,并且规定了如何基于当前状态以及该状态下接收的分节(TCP报文段)从一个状态转换为另一个状态。其状态转换图如下所示:

具体转化过程和转换条件如上图所示。

Comment and share

TCP的连接与建立

最常用的传输层协议包括TCP和UDP两种,当然除此之外还有其他协议。UDP是一种既不面向连接有不可靠的传输层协议,而TCP是一种面向连接的可靠的传输层协议,为了达到这个目的,其在设计上使用了差错检测,重传,累计确认,定时器以及用于序号和确认好的字段等等。

TCP的特点

TCP协议有以下特点:

  • 面向连接:TCP的连接不是一条电路的或者虚电路,其连接状完全保留在两个端系统中。
  • 可靠传输:TCP连接在传输消息的过程中保证了数据的可靠性,即保证数据在传输过程中不会丢失。
  • 点对点:一条TCP连接只能连接两个端点。
  • 全双工:如果一台主机上的进程A和另一台主机上的进程B建立了一条TCP连接,那么应用层数据就可以从A进程发送到B进程的同时,B进程也可以发送数据到A进程。

TCP报文段结构

TCP报文段是由首部字段和一个数据字段组成的,数据字段中包含应用程序需要发送的数据。通常报文段中通过MSS(max segment size)来限制报文段数据字段的最大长度。报文段的结构如下:

说明:

  • 源端口号与目的端口号指的是客户端和服务器端应用程序分别使用的端口号。用于识别主机上某一个特定的应用程序。
  • 序号(seq)为TCP报文段中数据其实字节的序号。
  • 确认号字段为(ACK)期待接收的下一个TCP报文段中数据的序号。
  • 首部长度:首部长度表示当前TCP报文的首部的长度,一般为20,即可以看到表中报文首部的长度为20个字节。但是有时选项中有一些内容,这个时候首部长度大于20。
  • 保留字:NULL
  • 标志位:当某一些标志位被设置的时候表达一些特定的含义。
    • UGR标识报文段中存在着紧急数据。其中紧急数据的指针字段存放在后面16位的紧急数据指针中。
    • ACK标识确认号字段生效,一般连接建立之后的内个报文段的ACK被会被设置
    • PSH:不常用
    • SYN:连接建立时的客户端发送的服务端的第一个报文段和服务端响应客户端的报文段中被设置,标识当前为建立连接的过程。
    • FIN:与SYN类似,它是在断开连接时被设置。
  • 互联网检验和:用于错误检测
  • 紧急数据指针:即为上述当UGR标志被设置时标识紧急数据的指针。
  • 选项:应用程序中自定义的首部的其他内容。
  • 数据:应用程序发送的真正数据。

TCP建立连接(三次握手)

TCP连接的过程可以简单描述为以下几个过程:

  • 第一次握手:客户端发送请求连接报文段到服务器。此报文段中SYN被设置为1,同时随机或者指定一个起始序号x。此时客户端进入SYN_SENT(同步已发送)状态。
  • 第二次握手:服务端收到请求报文段之后,向客户端发送确认报文段。确认报文段中ACK设置为1,SYN设置为1,确认号为x+1,同时为自己生成一个序号y。此时服务端进入SYN_RECV状态(同步接受到)。
  • 第三次握手:客户端收到服务端的确认报文段之后,还要给服务端发送一个确认报文段。这个报文段中ACK被设置为1,确认号为y+1。此报文段可以携带数据。

经过上述三个步骤之后,TCP连接建立成功。客户端进入连接建立状态(ESTABLISHED)。然后就可以相互发送数据了。

为什么要经过三次握手?

三次握手的目的是为了防止失效的报文段突然传送到服务端而出现问题。

上述已经失效的报文段是指:如果客户端在发送第一次连接请求的过程中,由于网络原因导致此报文段在某个网络节点滞留较长时间,这个时候TCP传输协议会视为此报文段已经丢失,于是重传。若此滞留的报文段在连接断开之后才到达服务器,这个时候就会出现问题。

若不是使用三次握手,服务器收到失效的报文段之后会建立连接,故之后无法释放TCP资源。导致资源浪费以致于长期会使服务器宕机。

三次握手过程中服务器和客户端程序的行为

  • 服务器必须准备好接受外来的连接,调用socket(),bind()以及listen三个函数来完成,我们称为被动打开。
  • 客户端通过调用connect发起主动打开连接,向服务器发送SYN(同步)包,connect函数阻塞。
  • 服务器通过accept接收到客户端来送的TCP包,需要进行确认同事发送自己的SYN包,此时accept函数阻塞,
  • 客户端收到服务器的SYN包,connect函数返回,并向服务器发送确认。服务器接收到确认之后accept函数返回。

TCP连接断开(四次挥手)

由于TCP连接是全双工的,因此每个方向都必须单独的关闭,也就是发送方和接收方都需要FIN和ACK。客户端和服务器都可以首先主动发送连接终止的报文。当其中一方发送完数据之后即可向另一方发出连接断开的请求。当收到FIN意味着这一方向上没有数据流动,但是一方收到FIN之后仍然可以发送数据。四次挥手的具体过程如下:

  1. 此时TCP连接两端都处于ESTABLISHED的状态,客户端停止发送数据,并发出一个FIN报文段。首部FIN设置为1,序号seq=u(u为客户端传输数据的字后一个字节的序号加1)。客户端进入FIN_WAIT-1状态。
  2. 服务端回复确认报文段,确认号为ack=u+1,序号为seq=v(v为服务端传输数据的最后一个字节序号加1),服务端进入close_wait状态。现在TCP连接处于半关闭状态,服务端如果继续发送数据,客户端依然接收。
  3. 客户端收到确认报文段,进入FIN_WAIT-2状态,服务端发送完数据之后,发出FIN报文段,FIN被置位1,确认号为ack=u+1,然后进入LAST_ACK状态。
  4. 客户端回复确认报文段。ACK=1,确认号为sck=w+1(w为半开半闭时收到的最后一个字节数据的编号),序号为seq=u+1,然后进入TIME_WAIT状态。

一段时间(大约4分钟)之后等待状态结束,连接两端进入CLOSED状态。

参考链接:

Comment and share

rapidjson Stream(待更新)

本系列文章以例子的方式进行呈现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include "rapidjson/rapidjson.h"
#include "rapidjson/document.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#include "rapidjson/filereadstream.h"
#include "cstdio"
using namespace std;
using namespace rapidjson;
int main() {
const char json[] = "[1, 2, 3]";
StringStream s(json);
Document document;
document.ParseStream(s);
for (int i = 0; i < 3; i++)
std::cout << document[i].GetInt() << std::endl;
StringBuffer SB;
Writer<StringBuffer> writer(SB);
document.Accept(writer);
std::cout << SB.GetString() << std::endl;
FILE* fp = fopen("test.json", "rb");
char readBuffer[65535];
FileReadStream is(fp, readBuffer, sizeof(readBuffer));
document.ParseStream(is);
fclose(fp);
for (Value::ConstMemberIterator it = document.MemberBegin();
it != document.MemberEnd(); it++)
std::cout << (it->name).GetString() << std::endl;
}

Comment and share

rapidjson指针

本系列文章以例子的方式进行呈现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include "rapidjson/document.h"
#include "rapidjson/pointer.h"
#include <iostream>
using namespace rapidjson;
/*
* Json Pointer
* 一个Json Pointer有一串(0至多个)token所组成,每个token之前都有/前缀。每个token
* 可以是字符串或者数字。例如,给定一个json:{
* "foo":["bar", "baz"],
* "pi":3.414
* }
* JsonPointer解析为:
* 1. "/foo"->["bar", "baz"]
* 2."/foo/0"->"bar"
* 3."/foo/1"->"baz"
* 4."pi"->3.414
* 特别注意:一个空Json Pointer""解析为整个json
*/
int main()
{
Document d;
/* 创建、修改、访问dom元素
* 创建dom元素的方法: (两种都可以自动生成父值,可以同时创建几级)
* 1.Pointer("/Key").Set(document, "value"); key对应值为value
* 2.Pointer("/Key").Create(document). 此时创建出来的key对应的值为null
* 获取Dom元素的pointer:
* 1.Value* vptr = Pointer("/Key").Get(document)
* 2.Value& vref = Pointer("/Key").GetWithDefault(document, "defaultValue");
* 注意返回的是引用,如值不存在,则创建此键并深拷贝默认值。
* 交换:
* Pointer("/Key").Swap(document, value); //注意交换的是Value,不能为其他类型
* 删除:
* Pointer("/Key").Erase(document);
*/
/* 针对上述函数的辅助函数
* SetValueByPointer(document, "/Key", "KeyValue");
* CreateValueByPointer(document, "/Key");
* GetValueByPointer(document, "/Key");
* GetValueByPointerWithDefault(document, "/default", "defaultValue");
* SwapValueByPointer(document, "/Key", Value); //必须为Value
* EraseValueByPointer(document, "/Key");
*/
/* Pointer相关函数说明
* 1.Pointer::Get() 或 GetValueByPointer() 函数并不修改 DOM。
* 若那些 token 不能匹配 DOM 里的值,这些函数便返回 nullptr。
* 使用者可利用这个方法来检查一个值是否存在。
* 2. Create()、GetWithDefault()、Set()、Swap()都会改变Dom.
* 若一些父值不存在,就会创建它们。若父值类型不匹配 token,
* 也会强行改变其类型。改变类型也意味着完全移除其 DOM 子树的内容
*/
/*
* Pointer中负号解析
* 1.在Get中,负号永远都是Key的名字
* 2.在Set中,若负号在数组的后面,则表示数组最后一个元素的下一个
* 否则表示数组名。例:
* d.Parse("{\"foo\":[123]}");
* SetValueByPointer(d, "/foo/-", 456); // { "foo" : [123, 456] }
* SetValueByPointer(d, "/-", 789); // { "foo" : [123, 456], "-" : 789 }
*/
/* 字符串化
* 可以将一个Pointe字符串化,存储于字符串或者其他输出流。
* 如:
* Pointer p(...);
* StringBuffer sb;
* p.StringFy(sb);
* std::cout << sb.GetString() << std::endl;
*/
Pointer("/project").Set(d, "RapidJson");
Pointer("/stars").Set(d, 10);
std::cout << d["project"].GetString() << std::endl;
std::cout << d["stars"].GetInt() << std::endl;
if (Value* stars = Pointer("/stars").Get(d))
{
stars->SetInt(stars->GetInt() + 1);
}
std::cout << d["stars"].GetInt() << std::endl;
Pointer("/default").GetWithDefault(d, "defaultValue");
if (d.HasMember("default"))
std::cout << d["default"].GetString() << std::endl;
Value swapValue("SwapValue");
Pointer("/default").Swap(d, swapValue);
std::cout << d["default"].GetString() << std::endl;
Pointer("/default").Erase(d);
if (!d.HasMember("default"))
std::cout << "d Erase default successful" << std::endl;
}

Comment and share

rapidjson Schema

本系列文章以例子的方式进行呈现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* JsonSchema本质上是一个json,其作用是用于校验Json,使用schema对json进行校验,
* 可以让代码安全的去当问DOM,而不需要去检查类型或者键值的存在等等。这也能确保输
* 出的json符合特定的schema。
*/
#include "rapidjson/schema.h"
#include "rapidjson/stringbuffer.h"
#include <stdio.h>
#include <string>
#include <string.h>
using namespace std;
using namespace rapidjson;
/*使用JsonSchema校校验json格式的流程:
* 1. 将schema(模板)解析成一个Document
* 2. 然后将Document编译成一个SchemaDocument
* 3. 通过上述SchemaDocument创建一个SchemaValidator。
* 4. 然后通过document.Accept(validator)去校验一个json,获取校验结果。
*注意:
* 1. 一个SchemaDocument能被多个SchemaValidator引用,他不会被SchemaValidator修改。
* 2. 可以重复使用SchemaValidator校验多个文件。在校验其他文件之前,必须先调用validator.Reset()。
*JsonSchema的格式:
* 1. JsonSchema实质上是一个json数据。
* 2. JsonSchema与其他json数据不同的是它在每一个对象和元素中定义了他们的类型type,属性(如object的properties,以及integer的minimum等等),
* 同时,我们可以通过JsonSchema对json的值的类型以及取值范围等进行限定。schema中必须的值需要显示的声明在required中。
*/
bool ValidateJson() {
string testSchema = "{\"type\":\"object\", \"properties\":{\"code\":{\"type\":\"string\"}, \"int\":{\"type\":\"integer\", \"minimum\":0}}, \"required\":[\"int\",\"code\"]}";
Document sd;
if (sd.Parse(testSchema.c_str()).HasParseError()) {
printf("jsonSchema is not valid : %s", testSchema.c_str());
return false;
}
//创建SchemaDocument
SchemaDocument schema(sd);
Document d;
if (d.Parse("{\"code\":123, \"int\":1}").HasParseError()) {
printf("Document is not a valid json");
return false;
}
SchemaValidator validator(schema);
if (!d.Accept(validator)) {
StringBuffer sb;
validator.GetInvalidSchemaPointer().StringifyUriFragment(sb);
printf("Invalid schema: %s\n", sb.GetString());
printf("Invalid keyword: %s\n", validator.GetInvalidSchemaKeyword());
sb.Clear();
validator.GetInvalidDocumentPointer().StringifyUriFragment(sb);
printf("Invalid document:%s\n ", sb.GetString());
return false;
} else {
printf("符合schemas模式\n");
return true;
}
}
int main() {
ValidateJson();
}

Comment and share

rapidjson查询操作基本用法

本系列文章以例子的方式进行呈现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#include "rapidjson/document.h"
#include <iostream>
int main()
{
const char* test = "{\"num\":123, \"hello\":null, \"type\": \"object\",\"properties\": {\"oid\": {\"type\": \"string\"}, \"username\": {\"type\": \"string\"},\"creid\": {\"type\": \"string\"}},\"required\": [\"oid\", \"username\",\"creid\"]}";
rapidjson::Document document;
//Parse(const char*). 从c-string解析为Document json格式
document.Parse(test);
//GetString().从Document json中取出string
std::cout << "type = " << document["type"].GetString() << std::endl;
/* 函数原型 函数功能
* IsNull() 判断当前键对应的值是不是null
* IsNumber() 判断当前键对应的值是不是number
* IsInt() 判断当前键对应的值是不是int
* IsDouble() 判断当前键对应的值是不是double
* IsString() 判断当前键对应的值是不是string
* IsBool() 判断当前键对应的值是不是bool
* IsArray() 判断当前键对应的值是不是array
* ...
*/
std::cout << "hello is " << (document["hello"].IsNull() ? "null" : "not null") << std::endl;
std::cout << "num is " << (document["num"].IsNumber() ? "number" : "not number") << std::endl;
std::cout << "required is " << (document["required"].IsArray()? "array" : "not array") << std::endl;
/* 访问Array的两种方法
* 1. 通过下标访问 //使用引用来连续访问,方便之余还更高效。
* 2. 通过迭代器访问
* 注意问题
* 1.索引使用SizeType类型,而不是size_t类型,缺省情况下,SizeType是unsigned的typedef
* 2.对于String类型使用GetInt是非法的,这个时候会导致程序崩溃
*/
const rapidjson::Value &a = document["required"];
assert(a.IsArray());
for (rapidjson::SizeType i = 0; i < a.Size(); i++)
std::cout << a[i].GetString() << std::endl;
for (rapidjson::Value::ConstValueIterator it = a.Begin(); it != a.End(); it++) {
std::cout << it->GetString() << std::endl;
std::cout << it->GetStringLength() << std::endl;
}
/* 访问object的方法
* 1.使用迭代器进行访问
* 2.使用键对应的下标进行当问 如:document["required"]
* 注意问题
* 1.在使用下标访问之前,最好先调用HasMember()检查一下当前键是否存在,若存在再往下继续。否则会出现段错误。
*/
static const char* kTypeNames[] = {"Null", "False", "True", "Object", "Array", "String", "Number"};
for (rapidjson::Value::ConstMemberIterator it = document.MemberBegin();
it != document.MemberEnd(); it++) {
std::cout << it->name.GetString() << " is type : " << kTypeNames[it->value.GetType()] << std::endl;;
}
/* 查询Number
* 说明:Json只提供一种数值类型---Number。Number可以是实数也可以是整数
* Dom提供了5中数值类型。包括unsigned,int,uint_64,int_64,double
* 类型检查 数值提取
* IsUint() GetUint()
* IsInt() GetInt()
* IsUint64() GetUint64()
* IsInt64() GetInt64()
* IsDouble() GetDouble()
*
* 查询String
* 说明:除了getString(), Value类也有一个GetStringLength();原因是rapidjson的String需要支持Unicode字符,如\u0000.问题是C/C++字符串是
* 空字符结尾的字符串,,这种字符串会把'\0'作为结束符号。为了符合RFC4627,若要处理这些带有unicode的字符串,
* 需要使用GetStringLength获取正确的字符串长度
* 函数 功能
* const char* getString() 获取C字符串
* SizeType GetStringLength()const 获取字符串的长度,
*/
rapidjson::Document d1;
d1.Parse("{\"s\":\"a\\u0000b\", \"num\":123}");
std::cout << d1["s"].GetString() << std::endl;
std::cout << d1["s"].GetStringLength() << std::endl;
std::cout << strlen(d1["s"].GetString()) << std::endl;
/*
* 比较两个Value的值
* 1.使用==和!=比较两Value的值,当且仅当两个Value的类型和内容相等才算相等。
* 2.可以将Value与原始值进行比较
* 3.若被比较的两个Value的类型不同,直接返回false。
* 4.可以比较不同Object的值
*/
std::cout << (document["num"] == document["hello"]) << std::endl;
std::cout << (document["num"] == d1["num"]) << std::endl;
/*
* 创建/修改值
* 1.改变Value的类型:当使用默认构造函数创建一个Value或者Document的时候,它的类型默认为
* NULL,要改变其类型,使用SetXXX()。如
* rapidjsons::Document d;
* d.SetObject(); //将d的类型修改为Object
* rapidjson::Value v;
* v.SetInt(1); //或者v = 1;
* 2.构造函数的重载
* rapidjson::Value b(false);
* rapidjson::Value n(10);
*
* 3.特别注意Value的转移语义。 Value重载operator=()时使用了转移语义。
* rapidjson::Value a(123);
* rapidjson::Value b(456);
* a = b; //此时b变为null,a为456
* 4.使用move()函数实现函数参数中的转移语义
* rapidjson::Value a(0);
* test(a.move());
*
*/
rapidjson::Value testa(10);
rapidjson::Value testb(10);
testa = testb;
std::cout << testa.GetInt() << std::endl;
if (testb.IsNull())
{
std::cout << "testb is moved to a, and now testb is null" << std::endl;
}
else
{
std::cout << testb.GetInt() << std::endl;
}
/*
* 创建String
* rapidjson提供两种string的存储策略:
* 1.copy-string(深拷贝):分配缓冲区,然后把来源数据复制至它
* 2.const-string(浅拷贝):简单的存储字符串指针
* 说明:
* 1.copy-string总是安全的,因为它拥有数据的克隆
* 2.当数据源发生改变,const-string中的数据也会受到影响
* 上面两种字符串的创建方式:
* 1.copy-string创建方式需要给API传递一个Allocator参数,这个做法避免了给每一个
* Value都保存一个Allocator。另外还需要一个length参数,保存长度信息,故
* 此函数可以处理带有空字符的字符串。
* 2.const-string创建方式不需要长度信息和Allocator参数,它默认字符串以\0结束.
* 一般用于常量字符串或者具有安全生存周期的字符串上
*/
rapidjson::Value autor;
char buffer[50];
int len = sprintf(buffer, "%s %s", "Milo", "Yip");
autor.SetString(buffer,len, document.GetAllocator());
memset(buffer, 0x0, sizeof(buffer));
std::cout << autor.GetString() << std::endl;
rapidjson::Value testString;
testString = "testString";
std::cout << testString.GetString() << std::endl;
/** 修改Array
* 函数 功能
* clear() 清空Array中的内容
* Reserve(SizeType, Allocator&) 申请指定大小空间,单不插入数据
* Value& PushBack(Value&, Allocator&) 添加元素
* template <typename T> GenericValue& PushBack(T, Allocator&) 添加元素
* Value& PopBack() 删除最后一个元素
* ValueIterator Erase(ConstValueIterator pos) 删除指定位置元素
* ValueIterator Erase(ConstValueIterator first, ConstValueIterator last) 删除指定范围元素
*/
rapidjson::Value testArray(rapidjson::kArrayType);
rapidjson::Document::AllocatorType& testAllocator = document.GetAllocator();
for (int i = 0; i < 5; i++)
{
testArray.PushBack(i, testAllocator);
}
testArray.PushBack("lua", testAllocator).PushBack("Mio", testAllocator);
for (rapidjson::SizeType i = 0;i < 5; i++)
std::cout << testArray[i].GetInt() << std::endl;
std::cout << testArray[5].GetString() << std::endl;
std::cout << testArray[6].GetString() << std::endl;
/* 修改Object
* 说明:每个Object都是键值对的集合。每个键必须为String。
* 添加成员的函数:
* 1.Value& AddMember(Value&, Value&, Allocator& allocator)
* 2.Value& AddMember(StringRefType, Value&, Allocator&)
* 3.template <typename T> Value& AddMember(StringRefType, T value, Allocator&)
*
* 补充说明:1.使用StingRefType作为name参数的重载版本与字符串的SetString()类似。这些重载是为了避免复制name字符串
* 因为jsonObject中经常会使常数键名。
* 2.如果你需要从非常数字符串或者生命周期不足的字符串创建键名,需要使用copy-string API。
* 为了避免中间变量,可以使用临时值
*
* 移除成员函数:
* 1.bool RemoveMember(const Ch* name):使用键名来移除成员(线性时间复杂度)。
* 2.bool RemoveMember(const Value& name):除了 name 是一个 Value,和上一行相同。
* 3.MemberIterator RemoveMember(MemberIterator):使用迭代器移除成员(_ 常数 _ 时间复杂度)。
* 4.MemberIterator EraseMember(MemberIterator):和上行相似但维持成员次序(线性时间复杂度)。
* 5.MemberIterator EraseMember(MemberIterator first, MemberIterator last):移除一个范围内的成员,维持次序(线性时间复杂度)。
*/
rapidjson::Value contact(rapidjson::kObjectType);
contact.AddMember("name", "milo", document.GetAllocator());
contact.AddMember("married", false, document.GetAllocator());
std::cout << "contact[name] is:"<<contact["name"].GetString() << std::endl;
std::cout << "contact[married] is:" << contact["married"].GetBool() << std::endl;
contact.AddMember(rapidjson::Value("copy", document.GetAllocator()).Move(), // copy string
rapidjson::Value().Move(), // null value
document.GetAllocator());
/* 深复制Value
* 方法:
* 1.含有Allocator的构造函数
* 2.含有Allocator的CopyFrom
* 交换Value
* 方法: Swap() 无论两颗Dom树多复杂,交换时间为常数
*/
rapidjson::Value testCopy1(123);
rapidjson::Value testCopy2(testCopy1, document.GetAllocator());
rapidjson::Value testCopy3;
testCopy3.CopyFrom(testCopy1, document.GetAllocator());
testCopy1.Swap(testCopy3);
}
`

Comment and share

protobuffer学习总结

protobuffer是google开发的一种数据描述语言,它能够将结构化的数据序列化,并切可以将序列化的数据进行反序列化恢复原有的数据结构。一般用于数据存储以及通信协议方面。

如果是第一次使用protobuffer,我们可以将其与json或者xml进行类比,其实它与json或xml类似都可以作为数据的存储方式,不同的是json和xml是文本格式,而protobuffer是二进制格式。二进制格式不利于使用者直观的阅读,但是与json以及xml相比它有更多的优点。

protoBuffer相比于xml的优点

  • 更加简介
  • 体积小:消息大小只需要xml的1/10~1/3
  • 解析速度快:解析速度比xml快20~100倍
  • 使用proto Buffer的编译器,可以生成方便在编程中使用的数据访问代码.
  • 具有更好的兼容性,很好的支持向上或向下兼容的特性
  • 提供多种序列化的出口和入口,如文件流,string流,array流等等

protobuffer语法

消息类型实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Package example;
message Person{
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType{
mobile = 1;
home = 2;
work = 3;
}
message PhoneNumber{
required string number = 1;
optional PhoneType type = 2;
}
repeated PhoneNumber phone = 4;
}

指定字段规则

protobuffer中字段规则包括一下三种:

  • required:实例中必须包含的字段
  • optional:实例中可以选择性包含的字段,若实例没有指定,则为默认值,若没有设置该字段的默认值,其值是该类型的默认值。如string默认值为””,bool默认值为false,整数默认值为0。
  • repeated: 可以有多个值的字段,这类变量类似于vector,可以存储此类型的多个值。

由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。
一般情况下慎重使用required字段,当此字段一定是必要的时候才使用。

repeated使用实例:

1
2
3
4
5
6
7
8
message Person {
required int32 age = 1;
required string name = 2;
}
message Family {
repeated Person person = 1;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int main(int argc, char* argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
Family family;
Person* person;
// 添加一个家庭成员,John
person = family.add_person();
person->set_age(25);
person->set_name("John");
// 添加一个家庭成员,Lucy
person = family.add_person();
person->set_age(23);
person->set_name("Lucy");
// 添加一个家庭成员,Tony
person = family.add_person();
person->set_age(2);
person->set_name("Tony");
// 显示所有家庭成员
int size = family.person_size();
cout << "这个家庭有 " << size << " 个成员,如下:" << endl;
for(int i=0; i<size; i++)
{
Person psn = family.person(i);
cout << i+1 << ". " << psn.name() << ", 年龄 " << psn.age() << endl;
}
getchar();
return 0;
}

数据类型

protobuffer中的数据类型与C++数据类型之间的关联如下图:

protobuffer类型 C++类型
double double
float float
int32 int32
int64 int64
uint32 uint32
uint64 uint64
sint32 int32
sint64 int64
fixed32 uint32
fixed64 uint64
sfixed32 uint32
sfixed64 uint64
bool bool
string string
bytes string

枚举

当需要定义一个消息类型的时候,我们可能想为某一个字段指定预定义列表中的值。这个时候就需要用到枚举

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}

变量标识号

在proto数据结构中,每一个变量都有唯一的数字标识。这些标识符的作用是在二进制格式中识别各个字段的,一旦开始使用就不可再改变。

此处需要注意的是1-15之内的标号在存储的时候只占一个字节,而大于15到162047之间的需要占两个字符,所以我们尽量为频繁使用的字段分配1-15内的标识号
。另外19000-19999之内的标识号已经被预留,不可用。最大标识号为2^29-1。

嵌套

protobuffer中的消息可以嵌套消息,也就是在一个message中定义另一个message。如上面实例可以看出。

扩展

我们可以通过扩展对proto文件进行扩展,而不需要直接区编辑原文件。

例如有原文件:

1
2
3
4
message Foo{
//...
extensions 100 to 199;
}

上述extensions 100 to 199表示此范围内的标识号被保留为扩展用。我们在扩展文件中就可以使用这些标识号了。

1
2
3
extend Foo{
optional int32 bar = 126;
}

上述为扩展。当用户的Foo消息被编码的时候,数据的传输格式与用户在Foo里定义新字段的效果是完全一样的。然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同——生成的数据访问代码为扩展准备了特殊的访问函数来访问它。例如,下面是如何在C++中设置bar的值:

1
2
Foo foo;
foo.SetExtentions(bar, 15);

注释

与c++注释风格相同。双斜杠

向上且向下兼容更新消息

当在需求不断增加的过程中,数据结构也会不断变化,这个时候就需要我们去更新消息。怎么才能做到更新消息不会影响之前的数据和代码。这个时候我们更新消息需要遵循以下几个原则:

  • 不要更改任何已有的字段的数值标识
  • 所添加的字段必须是optional或者repeated。

包名称解析

为了防止消息明明冲突,我们往往会在文件的开始出生命包,包的作用相当于命名空间。在编译成C++代码时也是namespace。例如:

1
2
3
4
package foo.bar;
message open{
///...
}

在C++对open进行访问的时候的访问方式为:

1
foo::bar::open test;

C++程序使用protobuffer

按照上面的规则我们可以设计出合理的protobuffer类型。然后下一步就是将proto文件生成C++头文件和实现文件,将.proto文件编译成C接口的方法如下:

1
protoc -I=SOURCE_DIR --cpp_out=DIST_DIR test.proto

使用proto生成的头文件进行编译时需要链接protobuffer库。具体为:

1
g++ main.cpp test.pb.cc -lprotobuf

protobuffer编译为C++代码的常用接口

对于C++来说,编译器会为每个.proto文件生成一个.h文件和.cc文件。.proto文件中的每一个消息对应一个类。
protobuffer中常用的函数:

  • has_name() :判断是否有当前成员
  • clear_name() :清空该成员变量值
  • name() :获取成员的变量值
  • set_name(string) :设置变量值
  • set_name(const char*):设置变量值
  • set_name(int) :设置变量值
  • clear() :清空所有元素为空状态
  • void CopyFrom(person):从给定的对象复制。
  • mutable_name() :获取变量name的指针
  • add_name() :为repeated变量增加值
  • ByteSize() :获取变量所占的字节数
    若有元素data属性为repeated,其行为类似于vector,则此时则可用下列函数:
  • add_data() : 添加data元素,返回值为Date*类型。
  • data_size() : 获取repeated元素size,即元素的个数。
  • data(i) : 获取data中地i个元素。
  • ByteSize() : 获取序列化之后的protobuff对象的长度。
  • CopyFrom(const ProtoType&): 从一个protobuf对象拷贝到另一个

常用的序列化方法

C数组的序列化与反序列化的API

如果想将其序列为char并通过socket进行传输,这是使用SerializeToArray来达到目的。
*除了下述的SerializeToArray方法之外,还有方法SerializePartialToArray,两者用法相同,其中唯一的区别在于SerializePartialToArray允许忽略required字段,而前者不允许

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void* parray = (char*)malloc(256);
//API
bool ParseFromArray(const void* data, int size);
bool SerializeToArray(void* data. int size);
void set_people()
{
wp.set_name("sealyao");
wp.set_id(123456);
wp.set_email("sealyaog@gmail.com");
wp.SerializeToArray(parray,256);
}
void get_people()
{
rap.ParseFromArray(parray,256);
cout << "Get People from Array:" << endl;
cout << "\t Name : " <<rap.name() << endl;
cout << "\t Id : " << rap.id() << endl;
cout << "\t email : " << rap.email() << endl;
}

C++ String的序列化与反序列化API

除了下述的SerializeToString方法之外,还有方法SerializePartialToString,两者用法相同,其中唯一的区别在于SerializePartialToString允许忽略required字段,而前者不允许

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//C++string序列化和序列化API
bool SerializeToString(string* output) const;
bool ParseFromString(const string& data);
//使用:
void set_people()
{
wp.set_name("sealyao");
wp.set_id(123456);
wp.set_email("sealyaog@gmail.com");
wp.SerializeToString(&pstring);
}
void get_people()
{
rsp.ParseFromString(pstring);
cout << "Get People from String:" << endl;
cout << "\t Name : " <<rsp.name() << endl;
cout << "\t Id : " << rsp.id() << endl;
cout << "\t email : " << rsp.email() << endl;
}

文件描述符序列化与反序列化API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//文件描述符的序列化和序列化API
bool SerializeToFileDescriptor(int file_descriptor) const;
bool ParseFromFileDescriptor(int file_descriptor);
//使用:
void set_people()
{
fd = open(path,O_CREAT|O_TRUNC|O_RDWR,0644);
if(fd <= 0){
perror("open");
exit(0);
}
wp.set_name("sealyaog");
wp.set_id(123456);
wp.set_email("sealyaog@gmail.com");
wp.SerializeToFileDescriptor(fd);
close(fd);
}
void get_people()
{
fd = open(path,O_RDONLY);
if(fd <= 0){
perror("open");
exit(0);
}
rp.ParseFromFileDescriptor(fd);
std::cout << "Get People from FD:" << endl;
std::cout << "\t Name : " <<rp.name() << endl;
std::cout << "\t Id : " << rp.id() << endl;
std::cout << "\t email : " << rp.email() << endl;
close(fd);
}

C++ stream 序列化和反序列化API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//C++ stream 序列化/反序列化API
bool SerializeToOstream(ostream* output) const;
bool ParseFromIstream(istream* input);
//使用:
void set_people()
{
fstream fs(path,ios::out|ios::trunc|ios::binary);
wp.set_name("sealyaog");
wp.set_id(123456);
wp.set_email("sealyaog@gmail.com");
wp.SerializeToOstream(&fs);
fs.close();
fs.clear();
}
void get_people()
{
fstream fs(path,ios::in|ios::binary);
rp.ParseFromIstream(&fs);
std::cout << "\t Name : " <<rp.name() << endl;
std::cout << "\t Id : " << rp.id() << endl;
std::cout << "\t email : " << rp.email() << endl;
fs.close();
fs.clear();
}

参考链接:

http://blog.csdn.net/mycwq/article/details/19622571
http://colobu.com/2015/01/07/Protobuf-language-guide/
https://worktile.com/tech/share/prototol-buffers
http://tech.meituan.com/serialization_vs_deserialization.html
http://blog.csdn.net/weiwangchao_/article/details/16797763

Comment and share

makefile常用相关函数

makefile中函数的用法与变量类似,变量的用法是在变量前面加上$,函数的用法一样,也是使用$开头,$之后加一个括号,括号中的第一个参数是函数名,后面紧跟此函数需要的参数,用逗号分隔。下面介绍几个常用的函数。

wildcard函数

  • 参数: 一个正则表达式

wildcard的中文意思是通配符,它的功能类似于正则表达式,用于展开一列所有符合其参数描述的文件名,文件之间用空格分割。

实例:

1
SOURCE=$(wildcard *.cpp)

此时SOURCE的值为所有的以.cpp为后缀的文件集合,以空格隔开。

patsubst函数

其功能是一个匹配替换的函数(pattern substitute)。

  • 参数:第一个是需要匹配的样式,第二个是表示用什么替换它,第三个被处理的以空格隔开的字符串。

实例:

1
TARGET = $(patsubst *.cpp, *.o, $(SOURCE))

TARGET表示与SOURCE中同名的目标文件.

$@,$<, $^

上述几个变量的含义:

  • $@ 表示目标的文件名
  • $< 表示依赖中的第一个文件名
  • $^ 表示依赖中所有的文件名

实例:

1
all: library.cpp main.cpp

其中$@标识all, $<表示library.cpp ,$^表示library.cpp main.cpp

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China