目录
2. 重新理解read、write、recv、send和tcp为什么支持全双工
1. 应用层
我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。
1.1 再谈"协议"
协议是一种"约定",socket api的接口,在读写数据时,都是按"字符串"的方式来发送接收的,如果我们要传输一些"结构化的数据"怎么办呢?
为什么要传输"结构化的数据"?
假如在微信的群聊中聊天的时候,你在群里发了一条消息,这条消息包含了你的头像,你的昵称,你要发送的信息,那么你是愿意这三个字段一个一个的发,还是打包成一个字段发,我想当然都是愿意打包成一个字段发了,当然,在你这里肯定知道这个字段中哪个是头像,哪个是昵称,哪个是你要发送的信息,那么在对方那里当然也是知道哪个是头像,哪个是昵称,哪个是你要发送的消息,这个就是协议,双方约定号的,而这个结构化数据就是C++中的结构体或者是类,在这个类中包含三个字段,头像的路径,昵称和消息。
其实,协议就是双方约定好的结构化的数据。
1.2 网络版计算器
例如,我们需要实现一个服务器版本的加法器,我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。
约定方案一:
客户端发送一个形如"1+1"的字符串
这个字符串中有两个操作数,都是整型
两个数字之间会有一个字符是运算符,运算符只能是+
数字和运算符之间没有空格
....
客户端和服务器使用一个结构化数据,客户端发,服务端提取,这种做法确实是可以的,但是它的要求特别的高,比如说客户端和服务器有可能客户端有多种版本,新,老版本都有,32位,64位的都有,也有可能双方结构体类型一样,但是对齐方式不一样,也有可能客户端和服务器认为的data大小都不一样,也有可能服务器是用C/C++写的,客户端时用别的语言写的,构建的结构体不一样,也就是说这种方案堆客户端和服务器的要求都比较高。
如果将来你的平台换了,以前是在32位,现在直接变成64位,但是市场是同时存在32位和64位,而且对齐方式也不一样,结构体的大小,客户端和服务器不一样,这个时候就出问题了,所以直接以二进制的方式传结构体的做法是可以的,但是要求有点高,要考虑内存对齐的问题,要考虑双方客户端和服务器兼容性的问题。
但是操作系统底层定协议就是传的就是这种结构化的二进制, 因为是操作系统,操作系统的语言都是一样的,都是C语言,平台都可以规定好,有严格的平台化的定义,所以可以直接用这种方案,但是一般我们在应用层不这么干。
约定方案二:
定义结构体来表示我们需要交互的信息
发哦是那个数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体
这个过程叫做"序列化"和"反序列化"
1.3 序列化和反序列化
通过序列化,把信息由多变一,然后通过网络发送,再通过反序列化,把信息由一变多。
为什么要有序列化?
为了方便网络发送,其实就是把三个字符串打包成一个字符串,这就叫序列化,信息由多变一,方便网络发送。
为什么要有反序列化?
1、因为曾经序列化了,2、因为上层要用的是结构化数据,所以必须把字符串一变多,方便上层处理。
为什么上层喜欢用这种结构化的数据?
其实我们这个世界,包括计算机世界,都是对象化的,只要对象化了就方便处理,再服务器内部收到的消息要被管理,就必须先描述再组织,因为服务器要对收到的多条消息进行管理处理,注定了先描述再组织,注定了收到的消息必须结构化的,注定了结构化的消息是对你最友好的。
上层软件不管是发送还是接收,它都要对消息做管理,上层软件本质就是一套管理体系。
无论我们采用方案一,还是方案二,还是其他的方案,只要保证,一端发送时构造和数据,在另一端能够正确的进行解析,就是ok的,这种约定,就是应用层协议。
但是,为了让我们深刻理解协议,我们打算自定义实现以下协议的过程。
我们采用方案2,我们也要体现协议定制的细节
我们要引入序列化和反序列化,只不过我们博客直接采用现成的方案 - jsoncpp库
我们要对socket进行字节流的读取处理
2. 重新理解read、write、recv、send和tcp为什么支持全双工
当我们在创建一个TCPsockfd的时候,而在操作系统内部,TCP套接字本质就是文件,在TCP套接字内部会帮我们在TCP层给该套接字创建两个缓冲区,一个叫做发送缓冲器,一个叫做接收缓冲区,在操作系统内部,操作系统内核会基于我们的文件来给我们当前对应的TCP创建一个发送缓冲器和接收缓冲区,对于主机A如此,对于主机B写代码时也是创建套接字,也是创建TCP套接字的话,也有发送缓冲器和接收缓冲器。
在我们进行写入的时候,曾经学文件时,一个文件都要有自己的文件缓冲区,用户层定好的字符串,把该字符串通过文件描述符传递给操作系统,其实是把用户层数据拷贝给操作系统,然后再由操作系统基于TCP/IP向下封装,然后给我们发送,发送数据包含send、write,本质根本就不是发,而是把我们要发送的数据拷贝到我们对应的TCP所对应的发送缓冲区当中,创建10个TCP套接字,就有10对发送接收缓冲区,一个套接字底层要创建一个发送缓冲区,我们发送数据本质是拷贝。之前用的send、write本质就不是把数据发送到网络中,而是把数据拷贝到TCP的发送缓冲区中,而数据什么时候刷新,写到网络中,完全由操作系统自主决定,由TCP自己决定,就好比我们往文件中写数据,我们写的数据先到文件缓冲区,啥时候刷新的文件中完全由操作系统自主决定的。所以对于TCP来讲也是同样如此。
作为主机B要接收数据,对方机器发送数据本质是把TCP发送缓冲区的数据,经过网络,拷贝到对方的接收缓冲区,因为对方也有一对发送和接收缓冲区,我们发送数据把数据发送给对方的接收缓冲区,而对方一旦接收缓冲区中没数据,read就会阻塞,所以接收缓冲区为空,read就会阻塞,应用层进程就会阻塞住,就会卡住,而一旦有数据,操作系统TCP接收缓冲区当中的数据通过sockfd拷贝到buffer中,拷贝到buffer中,我们的read此时就读到了对应的数据,所以read读取,本质也是拷贝,所以操作系统把数据发送到网络当中,而对应,接收方从网络中读到自己的操作系统内部,本质上也是拷贝,所以万事万物皆是拷贝,主机B给主机A发送消息也是同样的道理。
为什么TCP支持全双工,因为我们再进行收发的时候我们的TCP套接字底层对应了一对发送和接收缓冲区,发的时候修改的是发送,收的时候读的是接收是。在通信时,我的发送对你的接收,在我们发送和接收进行拷贝时,对方的发送和接收也可以同样进行工作,因为时两对发送接收缓冲区,所以TCP支持全双工。
操作系统内可能会存在大量的报文,因为我们的客户端不止一台,有的报文在向上交付,有的报文在网络层。同样的,作为主机A,作为客户端,上面也有大量的APP,请求,响应回来,操作系统内也会有大量的报文,如果操作系统内存在大量的报文时,有哪些报文时新建的,哪些报文时新增的,哪些报文正在网络层,哪些报文正在传输层,哪些报文正在被交付,所以既然操作系统内存在大量的报文,报文里面有协议的报头和对应的数据,操作系统要管理报文,先描述,再组织,网络层的报文将来一定是一个用结构体描述起来的一个报文对象,再内核当中,这个结构体叫做sk_buff,sk就是socket,buff就是缓冲区。
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
struct sock *sk;
struct skb_timeval tstamp;
struct net_device *dev;
struct net_device *input_dev;
union {
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct ipv6hdr *ipv6h;
unsigned char *raw;
} h;
union {
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
unsigned char *raw;
} nh;
union {
unsigned char *raw;
} mac;
struct dst_entry *dst;
struct sec_path *sp;
/*
* This is the control buffer. It is free to use for every
* layer. Please put your private variables there. If you
* want to keep them across layers you have to do a skb_clone()
* first. This is owned by whoever has the skb queued ATM.
*/
char cb[48];
unsigned int len,
data_len,
mac_len,
csum;
__u32 priority;
__u8 local_df:1,
cloned:1,
ip_summed:2,
nohdr:1,
nfctinfo:3;
__u8 pkt_type:3,
fclone:2,
ipvs_property:1;
__be16 protocol;
void (*destructor)(struct sk_buff *skb);
#ifdef CONFIG_NETFILTER
struct nf_conntrack *nfct;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct sk_buff *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge;
#endif
__u32 nfmark;
#endif /* CONFIG_NETFILTER */
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
#ifdef CONFIG_NET_DMA
dma_cookie_t dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
/* These elements must be at the end, see alloc_skb() for details. */
unsigned int truesize;
atomic_t users;
unsigned char *head,
*data,
*tail,
*end;
};
操作系统内有很多的报文,我们可以用链表管理起来,对报文的管理就变成了对链表的增删查改。
TCP协议,它的名字叫传输控制协议,TCP这一层属于操作系统,TCP能做到传输控制,因为用户把数据拷贝到TCP,放到队列时,其实已经属于操作系统了,剩下的工作,数据什么时候发?怎么发?出错了怎么办?完全由于操作系统,严格说由TCP自己控制,所以,天然的TCP已经有了传输控制的前置条件了,因为怎么发由它说了算,所以我们把TCP叫传输控制协议,它有这个能力进行传输控制。
假设我今天要发一个"你好啊 20xx-yy-zz aa:bb:cc 新时代好青年",我把这个数据拷贝到了发送缓冲区,操作系统发送我这部分数据时,不关心我这里面的数据是多少字节,也不关心每个字段什么含义,操作系统只知道收到了一批数据,然后,操作系统要给对方发,假设我要发送的数据这个字符串"你好啊 20xx-yy-zz aa:bb:cc 新时代好青年"是20个字节,对方接收缓冲区假设只剩10个字节的空间,TCP要进行各种控制,如果要发送的时候,对方只剩10个字节的空间,TCP以一定的方式会知道对方只剩10个字节了,所以TCP也就只能发送10个字节过去,因为TCP要进行流量控制,所以TCP就把这个报文的一部分发给对方了,比如发的是"你好啊 20xx-yy",可是接收方的上层有可能很快就把这些数据处理走了,然后再读"你好啊 20xx-yy",此时read读到了"你好啊 20xx-yy"就返回了,可是read只是读取了原始报文的一部分,此时,这个读取就算出问题了。
TCP发多少我们控制不了,TCP按字节发,发10个也是发,发100个也是发,反正它只按照自己的原则,然后把我们所收到的这一批字节信息按照自己的真实需求,按照自己的流量控制,各种要求,然后给对方发,至于有没有发全,TCP不操心,所以TCP叫做面向字节流,TCP就是这么设计的,UDP叫面向数据报,不是因为叫面向数据报如何如何,而是因为UDP规定,凡是发出去的报文必须是完整的,所以才叫UDP,所以UDP如果要发"你好啊 20xx-yy-zz aa:bb:cc 新时代好青年"这个报文,把报文交给操作系统,操作系统就必须把这个报文整体打包,发送给对方,这个是必须这么干的,所以UDP并不存在所说的发一半的问题,所以UDP叫做面向数据报,UDP特别像快递,发快递只能发一个,收快递也只能收一个,快递公司规定,不能发半个快递给用户,TCP就像自来水,自来水公司和自来水水管,只负责把自来水通到你家,至于自来水你想怎么接,完全由用户自己决定,自来水公司只负责把数据以流式的方式交给你,能交就交,交不了就等一等交,基于TCP,读到的报文,如果拿到了一半,那么我们应用层来保证自己报文的完整性。
所以消息发一半过来,read到了要处理,如果发现报文不完整,就别处理,下一次再读一部分,拼接到它后面,由用户,由上层自己来决定把这个报文有没有读全,所以TCP因为是面向字节流,不对报文进行任何对应的完整性的处理,所以我们对应的TCP需要由应用层来保证报文的完整性。
文件也是面向字节流的,读取时老是担心报文完整性的问题。按行写的,按行读,做的就是序列化和反序列化,有了序列化和反序列化的方案,而且不怕读不完,因为会读到文件结尾。写的时候整数,浮点数随便写,读的时候很难受,读的时候发现分不开了,其实一直担心数据多读或少读。
如果我们自己做的话我们可以把数据和数据之间用特殊字符隔开。但是也不是很好。
我们可以设计成这样的:
head_length不是表示整个报文的长度,表示的是有效载荷的长度,也就是后面的数据的长度,head_length也是数字,和前面的数字区分就是通过\n,加\n是前面和后面做区分,方便增加可读性,因为报文可能粘再一起,为了打印,调试,给后面也加\n,所有的\n是约定好的,不作为实际报文长度的统计。
读取的时候必须读到第一个回车行'\n',读到回车行就知道前面读到的合起来一定是个数字,表示的是有效载荷的长度,接下来就从head_length+1就可以把整个报文的完整性的读上来了,现在我们要保证报文的完整性,我们只需要保证head_length是完整的就可以,head_length保证完整是因为它是个数字,不可能出现其他字符,保证head_length的完整性我们只需要读到\n就可以,因为head_length再读到\n之前前面不会出现\n,一直读,直到读到\n把数字读完,读不完就再等一等,下次读的时候继续把这个报文读完整,所以"x""open""y"叫做有效载荷,head_length我们自己定的报头。
如果我们要定协议,把协议定好,也就是把结构化字段定好,还要兼容考虑获取报文的完整性,序列化和反序列化由成熟的方案解决,协议定好,保证报文的完整性。
我们的解决方案就是head_length\n+json串。
如果我要发图片和视频呢?
图片和视频直接往head_length后面跟,也是一样的。而且不用关心里面的特殊字符,有了长度,后面随便跟,想发什么就发什么。
3. 代码实现
Linux: This repository is specifically designed to store Linux code - Gitee.comhttps://gitee.com/Axurea/linux/tree/master/2025_05_30_NetCalLinux: This repository is specifically designed to store Linux code - Gitee.com
https://gitee.com/Axurea/linux/tree/master/2025_06_03_NetCal
附录
Jsoncpp
Jsoncpp是一个用于处理JSON数据的C++库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。
特性
简单易用:Jsoncpp提供了直观的API,使得处理JSON数据变得简单。
高性能:Jsoncpp的性能经过优化,能够高效的处理大量JSON数据。
全面支持:支持JSON标准中的所有数据类型,包括对象、数据、字符串、数字、布尔值和null。
错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,方便开发者调试。
当使用Jsoncpp库进行JSON的序列化和反序列化时,确实存在不同的做法和工具类可供选择。以下是对Jsoncpp中序列化和反序列化操作的详细介绍:
Ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
形成的可执行文件的连接详情
aurora@wanghao:~/Linux/2025_05_30_NetCal$ ldd a.out
linux-vdso.so.1 (0x00007ffdeedb4000)
libjsoncpp.so.1 => /lib/x86_64-linux-gnu/libjsoncpp.so.1 (0x00007fa99ab7c000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fa99a90e000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fa99a8e9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa99a6f7000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa99a5a8000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa99abc0000)
aurora@wanghao:~/Linux/2025_05_30_NetCal$ ll /lib/x86_64-linux-gnu/libjsoncpp.so
lrwxrwxrwx 1 root root 15 Mar 21 2020 /lib/x86_64-linux-gnu/libjsoncpp.so -> libjsoncpp.so.1
aurora@wanghao:~/Linux/2025_05_30_NetCal$ ll /lib/x86_64-linux-gnu/libjsoncpp.so*
lrwxrwxrwx 1 root root 15 Mar 21 2020 /lib/x86_64-linux-gnu/libjsoncpp.so -> libjsoncpp.so.1
lrwxrwxrwx 1 root root 19 Mar 21 2020 /lib/x86_64-linux-gnu/libjsoncpp.so.1 -> libjsoncpp.so.1.7.4
-rw-r--r-- 1 root root 215840 Mar 21 2020 /lib/x86_64-linux-gnu/libjsoncpp.so.1.7.4
序列化
序列化指的是将数据结构或对象转换为一种格式,以便在网路上传输或存储到文件中。Jsoncpp提供了多种方法进行序列化:
1. 使用Json::Value的toStyledString方法:
- 优点:将Json::Value对象直接转换为格式化的JSON字符串。
- 示例:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
std::string s = root.toStyledString();
std::cout << s << std::endl;
return 0;
}
aurora@wanghao:~/Linux/2025_05_30_JsonTest$ ./json
{
"name" : "joe",
"sex" : "男"
}
2. 使用Json::StreamWrite
- 优点:提供了更多的定制选项,如缩进,换行符等。
- 示例:
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::StreamWriterBuilder wbuilder; // StreamWrite的工厂
std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root,&ss);
std::cout << ss.str() << std::endl;
return 0;
}
aurora@wanghao:~/Linux/2025_05_30_JsonTest$ ./json
{
"name" : "joe",
"sex" : "男"
}
3. 使用Json::FastWrite
- 优点:比StyledWrite更快,因为它不添加额外的空格和换行。
- 示例:
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
aurora@wanghao:~/Linux/2025_05_30_JsonTest$ ./json
{"name":"joe","sex":"男"}
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
// Json::FastWriter writer;
Json::StyledWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
aurora@wanghao:~/Linux/2025_05_30_JsonTest$ ./json
{
"name" : "joe",
"sex" : "男"
}
反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp提供了以下方法进行反序列化:
1. 使用Json::reader
- 优点:提供详细的错误信息和位置,方便调试。
- 示例:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
// JSON字符串
std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";
// 解析JSON字符串
Json::Reader reader;
Json::Value root;
// 从字符串中读取JSON数据
bool parsingSuccessful = reader.parse(json_string,root);
if(!parsingSuccessful)
{
// 解析失败,输出错误信息
std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
return 1;
}
// 访问JSON数据
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();
//输出结果
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "City: " << city << std::endl;
return 0;
}
aurora@wanghao:~/Linux/2025_05_30_JsonTest$ ./json
Name: 张三
Age: 30
City: 北京
2. 使用Json::CharReader的派生类(不推荐了,上面的就够了)。
- 在某些情况下,你可能需要更精细地控制解析过程,可以直接使用Json::CharReader的派生类。
- 但通常情况下,使用Json::parseFromStream或Json::Reader的parse方法就足够了。
总结
toStyledString、StreamWriter和FastWriter提供了不同的序列化选项,你可以根据具体需求选择使用。
Json::Reader和parseFromStream函数是Jsoncpp中主要的反序列化工具,它们提供了强大的错误处理机制。
在进行序列化和反序列化时,请确保处理所有可能的错误情况,并验证输入和输出的有效性。
Json::Value
Json::Value是Jsoncpp库中的一个重要类,用于表示和操作JSON数据结构。以下是一些常用的Json::Value操作列表:
构造函数
Json::Value():默认构造函数,创建一个空的Json::Value对象。
Json::Value(ValueType type,bool allocated = false):根据给定的ValueType(如nullValue,intValue,stringValue等)创建一个Json::Value对象。
访问元素
Json::Value& operator[](const char* key):通过键(字符串)访问对象中的元素。如果键不存在,则创建一个新的元素。
Json::Value& operator[](const std::string& key): 同上,但使用std::string类型的键。
Json::Value&operator:通过索引访问数组中的元素。如果索引超出范围,则创建一个新的元素。
Json::Value&at(constchar*key):通过键访问对象中的元素,如果键不存在则抛出异常。
Json::Value& at(const std::string& key):同上,但使用std::string类型的键。
类型检查
boo isNull():检查值是否为null。
boo isBool():检查值是否为布尔类型。
boo isInt():检查值是否为整数类型。
boo isInt64():检查值是否为64 位整数类型。
boo isUInt():检查值是否为无符号整数类型。
boo isUInt64():检查值是否为64位无符号整数类型。
boo isIntegral():检查值是否为整数或可转换为整数的浮点数。
boo isDouble():检查值是否为双精度浮点数。
boo isNumeric():检查值是否为数字(整数或浮点数)。
boo isString():检查值是否为字符串。
boo isArray():检查值是否为数组。
boo isObject():检查值是否为对象(即键值对的集合)。
赋值和类型转换
Json::Value& operator=(bool value):将布尔值赋给Json::Value对象。
Json::Value& operator=(int value): 将整数赋给Json::Value对象。
Json::Value& operator=(unsigned int value):将无符号整数赋给Json::Value对象。
Json::Value& operator=(Int64 value):将 64 位整数赋给Json::Value对象。
Json::Value& operator=(UInt64 value):将64位无符号整数赋给Json::Value对象。
Json::Value& operator=(double value):将双精度浮点数赋给Json::Value对象。
Json::Value& operator=(const char*value): 将C字符串赋给Json::Value对象。
Json::Value& operator=(const std::string& value): 将std::string赋给Json::Value对象。
bool asBool():将值转换为布尔类型(如果可能)。
int asInt():将值转换为整数类型(如果可能)。
Int64 asInt64():将值转换为64位整数类型(如果可能)。
unsigned int asUInt():将值转换为无符号整数类型(如果可能)。
UInt64 asUInt64():将值转换为64位无符号整数类型(如果可能)。
double asDouble():将值转换为双精度浮点数类型(如果可能)。
std::string asString():将值转换为字符串类型(如果可能)。
数组和对象操作
size_t size():返回数组或对象中的元素数量。
bool empty():检查数组或对象是否为空。
void resize(ArrayIndex newSize):调整数组的大小。
void clear():删除数组或对象中的所有元素。
void append(const Json::Value& value):在数组末尾添加一个新元素。
Json::Value& operator[](const char* key, const Json::Value& defaultValue=Json::nullValue):在对象中插入或访问一个元素,如果键不存在则使用默认值。
Json::Value& operator[](const std::string& key, const Json::Value& defaultValue=Json::nullValue):同上,但使用std::string类型的