博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
protocol buffer安装及使用(非常详细)
阅读量:5857 次
发布时间:2019-06-19

本文共 15304 字,大约阅读时间需要 51 分钟。

hot3.png

这篇文章是我在研究protocol buffer如何时从网上搜刮了很多文章后整理而成的,虽然很多文章中的内容已经很全面了,但是我发现有些我遇到的问题不是看一片文章能解决的,在这里把这篇我整理的文档分享给大家,只是为了给和我遇到同样问题的人提供方便,别无他用。

Linux 下安装及编译

常规安装步骤如下所示: 

 tar -xzf protobuf-2.5.0.tar.gz //解压

 cd protobuf-2.5.0  //进入解压后的目录

//或者直接手动解压,进入解压后的文件夹

 bash ./configure --prefix=$INSTALL_DIR 

 make 

 make check 

 sudo make install

 

如果常规安装后找不到protobuf库则用下面方法安装

窗体顶端

安装步骤如下所示:

 tar -xzf protobuf-2.1.0.tar.gz 

 cd protobuf-2.1.0 

 ./configure --prefix=/usr/local/protobuf

 make 

 make check 

 make install 

 

 2 > sudo vim /etc/profile

 添加

export PATH=$PATH:/usr/local/protobuf/bin/

export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/

保存执行

source /etc/profile

 

同时 在~/.profile中添加上面两行代码,否则会出现登录用户找不到protoc命令

 

3 > 配置动态链接库路径

sudo vim /etc/ld.so.conf

插入:

/usr/local/protobuf/lib

 

4 > su  #root 权限

ldconfig

 

5> sudo  cp /usr/local/protobuf/lib/pkgconfig/protobuf.pc  /usr/lib/pkgconfig/

sudo  cp /usr/local/protobuf/lib/pkgconfig/protobuf_lite.pc  /usr/lib/pkgconfig/

窗体底端

 

编译.proto文件,比如当前目录下有个hello.proto文件

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/hello.proto

// SRC_DIR是源文件所在的目录  DST_DIR是目标文件所在的目录也即生成的.h.cc文件所在的目录

 

编译详解:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto

      这里将给出上述命令的参数解释。
      1. protocProtocol Buffer提供的命令行编译工具。
      2. --proto_path等同于-I选项,主要用于指定待编译的.proto消息定义文件所在的目录,该选项可以被同时指定多个。
      3. --cpp_out选项表示生成C++代码,--java_out表示生成Java代码,--python_out则表示生成Python代码,其后的目录为生成后的代码所存放的目录。
      4. path/to/file.proto表示待编译的消息定义文件。
      注:对于C++而言,通过Protocol Buffer编译工具,可以将每个.proto文件生成出一对.h.ccC++代码文件。生成后的文件可以直接加载到应用程序所在的工程项目中。如:MyMessage.proto生成的文件为MyMessage.pb.hMyMessage.pb.cc

 

本次程序使用当前目录,所以可以写成下面的格式:

protoc -I=./ --cpp_out=./ ./hello.proto

说明:./表示当前目录,也即生成的.h.cc文件都会在当前目录下

编译器将根据你在文件中描述的消息类型生成你所选语言的代码,包括设定和读取字段值、把你的消息序列化成输出流、以及从输入流中解析你的消息。

这里指定--cpp,表示c++文件

 

Windows下安装及编译:

1在解压后的文件夹中,打开vsprojects目录:

打开libprotobuf.vcproj(这个是vs的工程文件,打开前请确认安装了vs2005 vs2008vs2010等等),在打开工程中,   可能需要转换工程,直接转换就行了

打开后,vs的工程页面:

右键分别生成libprotobuflibprotobuf-litelibprotocprotoc这四个工程(编译模式为Release 模式):

编译完成

编译完成后,可以在vsprojects\Release下发现3lib和一个exe文件,他们分别是libprotobuf.liblibprotobuf-lite.liblibprotoc.libprotoc.exe 

2拷贝文件

拷贝文件windows目录下:

将得到的libprotobuf.liblibprotobuf-lite.liblibprotoc.libprotoc.exe拷贝到系统盘的windows目录下。如果你的系统安装在C盘,那么就拷贝到c:\windows目录下

拷贝文件到vs目录下

比如安装的是vs2010,则将protobuf目录下的src目录下的google文件夹拷贝到Microsoft Visual Studio 10.0\VC\include目录下,这样做的原因是,vs项目中包含的头文件都会从这个目录下找,不这样做的话,项目中包含的proto头文件将找不到。

3编译proto文件,生成c++文件

假设在E:\test目录下有一个名为addressbook.protoproto文件,那么打开CMD,输入下面的命令就可以生成C++源码了:

protoc.exe -I=e:\ --cpp_out=e:\ e:\\addressbook.proto

生成了addressbook.pb.haddressbook.pb.cc文件,将这个文件拷贝到需要用到的工程里,然后在工程文件的相应位置添加上.h文件和.cc文件即可

高级编译:

可以写一个bat文件,将proto文件拖到bat文件上自动编译,比如创建一个proto.bat,内容如下:

========================================================

protoc.exe -I=./ --cpp_out=./ ./%~nx1

 

pause

 

xcopy *.h ..\src\*.* /y/d/f

xcopy *.cc ..\src\*.* /y/d/f

del *.h

del *.cc

 

解释:

第一句就是proto的编译语句

pause是为了让CMD窗口暂停,方便看执行结果

xcopy *.h那句 是将当前目录下的.h文件拷贝到同级目录src目录下,其中..表示回到上一级目录

del *.h 是删除当前目录下的.h文件

可以根据自己需要修改其中内容,这样将.proto文件拖到.bat文件上,就自动生成.h.cc文件并且保存到指定的目录下了。

Protocol buffer 创建与字段规则:

编写.proto文件:如 hello.proto

package lm; 

 message helloworld 

 { 

    required int32     id = 1;  // ID 

    required string    str = 2;  // str 

    optional int32     opt = 3;  //optional field 

 }

 

package相当于是c++中的命名空间(namespace),作用就是为了防止不同项目中的类名冲突,具体使用  包名::类名 对象名

(如果包名中字段用.分隔,比如package a.b.c 则相当于创建了多层包,使用的时候需要命名空间作用域符::逐层访问 如 a::b::c::类名 对象名)

required 必须字段  optional 可选字段  repeated重复字段

详解:

required:一个格式规范的消息必须恰好有一个这样的字段。

optional:一个格式规范的消息可以有0个或1个这样的字段(但不超过1个)。
repeated:一个格式规范的消息里这个字段可以重复任意次(包括0次)。这些重复值的顺序将被保留。

required 将是永久的 你得很小心地定义 required字段。如果某时某刻你想停止写或者发送一个required字段,把字段改成optional字段会很麻烦,因为老读者会认为没有这个字段的消息是不完整的,然后可能无意中拒绝或丢弃掉它。

数据类型:

常用数据类型:

double 

float 
int32 使用可变长度编码。对负数编码的时候会相对低效,所以如果你的字段值有可能是负数,请使用sint32。 
int64 使用可变长编码。对负数编码的时候会相对低效,所以如果你的字段值有可能是负数,请使用sint64。 
uint32 使用可变长编码。 
uint64 使用可变长编码。 
sint32 使用可变长编码。带符号的int数值,它编码负数的时候比常规的int32s更加高效。 
sint64 使用可变长编码。带符号的int数值,它编码负数的时候比常规的int64s更加高效。 
fixed32 固定4个字节。若数值经常大于228时比uint32更加高效。 
fixed64 固定8个字节。若数值经常大于256时比uint64更加高效。 
sfixed32 固定4个字节。 
sfixed64 固定8个字节。 
bool 
string 字符串必须总包含utf8编码或7位的ASCII文本。 s
bytes 可以包含任意顺序的字节。 

.protoc++java类型对照表

.proto Type

C++ Type

Java Type

double

 double

 double

float

 float

 float

int32

 int32

 int

int64

 int64

 long

uint32

 uint32

 int

uint64

 uint64

 long

sint32

 int32

 int

sint64

 int64

 long

fixed32

 uint32

 int

fixed64

 uint64

 long

sfixed32

 int32

 int

sfixed64

 int64

 long

bool

 bool

 boolean

string

 string

 String

bytes

string

ByteString

 

枚举类型和嵌套类型

比如:

enum UserStatus {

          OFFLINE = 0;
          ONLINE = 1;
      }
message UserInfo {
          required int64 acctID = 1;
          required string name = 2;
          required UserStatus status = 3;
      }
 message LogonRespMessage {
          required LoginResult logonResult = 1;
          required UserInfo userInfo = 2;
      }

 

Protocol Buffer消息升级原则。

      在实际的开发中会存在这样一种应用场景,既消息格式因为某些需求的变化而不得不进行必要的升级,但是有些使用原有消息格式的应用程序暂时又不能被立刻升级,这便要求我们在升级消息格式时要遵守一定的规则,从而可以保证基于新老消息格式的新老程序同时运行。规则如下:

      1. 不要修改已经存在字段的标签号。

      2. 任何新添加的字段必须是optionalrepeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。

      3. 在原有的消息中,不能移除已经存在的required字段,optionalrepeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。

      4. int32uint32int64uint64bool等类型之间是兼容的,sint32sint64是兼容的,stringbytes是兼容的,fixed32sfixed32,以及fixed64sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类型兼容的类型,否则就将打破新老消息格式的兼容性。

      5. optionalrepeated限定符也是相互兼容的。

 

 

protocol buffer使用实例

使用说明

发送端:

 

创建一个对象

方法:包名::类名 对象名

初始化对象

方法:对象名.set_字段名(参数);

required字段必须初始化,可选字段可以不初始化,当消息被解析时,如果它没有包含可选元素,类中相应的字段将被设定成默认值。默认值可以在消息描述中指定如:optional int32 result_per_page = 3 [default = 10], 如果可选元素没有指定默认值,那将使用该类型的默认值:string类型会是个空字符串,bool类型会是false,数值类型会是0,枚举类型是枚举定义中的第一个值

序列化

方法:对象名.函数名(参数);

函数有:SerializeToArraySerializeToCodeStreamSerializeToFileDescriptorSerializeToOstreamSerializeToStringSerializeToZeroCopyStream

【常用序列化方法见最后】

如暂存到字符串中:对象名.SerializeToString(string*);

这样就把序列化后的结果暂时存到字符串中了,方便后面传输

 

接收端:

1  创建一个对象

方法:包名::类名 对象名

反序列化

方法:对象名.函数名(参数);

函数有:ParseFromArrayParseFromCodedStreamParseFromFileDescriptorParseFromIstreamParseFromStringParseFromZeroCopyStream

【常用反序列化方法见最后】

如反序列化一个字符串中的数据:对象名.ParseFromString(string);

使用数据

方法:对象名.字段名()

如 对象名.id()

     对象名.str()

 

代码中绿色字体代表是使用protocol buffer的关键代码

使用:客户端:hello_client.cpp

客户端的主要代码

#include <iostream>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <string>

#include <pthread.h>

#include "hello.pb.h"

#include <fstream>

 

using namespace std;

 

int main()

{

  lm::helloworld msg1;//创建helloworld对象

  msg1.set_id(101);

  msg1.set_str("hello");

  string buf;

  msg1.SerializeToString(&buf);//序列化,将msg1内容序列化为protocol buffer的二进制格式,存放到字符串变量buf

  int fd=socket(AF_INET,SOCK_STREAM,0);

if(fd==-1)perror("socket"),exit(-1);

struct sockaddr_in addr;

addr.sin_family=AF_INET;

addr.sin_port=htons(2222);

addr.sin_addr.s_addr=inet_addr("192.168.21.194");

int res=connect(fd,(struct sockaddr*)&addr,sizeof(addr));

if(res==-1)perror("connect"),exit(-1);

cout << "connect ok!" << endl;

char data[1024]={};

strcpy(data,buf.c_str());//将字符串内容放到字符数组中,方便传输

while(1){

write(fd,data,strlen(data));//发送字符数组

sleep(2);

}

close(fd);

  return 0;

}

 

 

使用:服务端:hello_server.cpp

服务端的主要代码

 

#include <iostream>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <string>

#include <fstream>

#include "hello.pb.h"

 

using namespace std;

 

//打印函数

void ListMsg(const lm::helloworld &msg)

{

  cout << msg.id() << endl;

  cout << msg.str() << endl;

}

 

 

int fd;

 

int main()

{

  lm::helloworld msg1;

  char data[1024]={};

  string buf;

  fd=socket(AF_INET,SOCK_STREAM,0);

if(fd==-1)perror("socket"),exit(-1);

struct sockaddr_in addr;

addr.sin_family=AF_INET;

addr.sin_port=htons(2222);

addr.sin_addr.s_addr=inet_addr("192.168.21.194");

int res=bind(fd,(struct sockaddr*)&addr,sizeof(addr));

if(res==-1)perror("bind"),exit(-1);

cout << "绑定成功,欢迎访问" << endl;

listen(fd,100);

while(1){

     struct sockaddr_in client;

socklen_t len=sizeof(client);

int sockfd=accept(fd,(struct sockaddr*)&client,&len);

if(sockfd==-1)perror("accept"),exit(-1);

cout << inet_ntoa(client.sin_addr) << "连接上来了" << endl;

while(1){

    res=read(sockfd,data,1024);

if(res==-1)perror("read"),exit(-1);

if(res==0)break;

buf=data;

msg1.ParseFromString(buf);//反序列化

 ListMsg(msg1); 

sleep(2);

}

}

  return 0;

}

 

Linux下编译链接:

g++ hello_client.cpp hello.pb.cc -o cl `pkg-config --cflags --libs protobuf`

 

g++ hello_server.cpp hello.pb.cc -o se `pkg-config --cflags --libs protobuf`

 

 

说明:hello_server.cpp是服务端c++文件

    hello.pb.cc .proto生成的文件

    -o se  是指定生成的可执行文件的名字

   `pkg-config --cflags --libs protobuf` 是指定protobuf库所在的位置

pkg-config 是通过库提供的一个 .pc 文件获得库的各种必要信息的,包括版本信息、编译和连接需要的参数等,.pc 文件的搜索路径是通过环境变量 PKG_CONFIG_PATH 来设置的,pkg-config 将按照设置路径的先后顺序进行搜索,直到找到指定的 .pc 文件为止

--cflags 参数可以给出在编译时所需要的选项

--libs 参数可以给出连接时的选项

 

 

 

【常用序列化和反序列化方法】

1C数组的序列化和反序列化API

1. //C数组的序列化和序列化API  

2. bool ParseFromArray(const void* data, int size);  

3. bool SerializeToArray(void* data, int size) const;  

4. //使用  

5. void set_people()

6. {  

7.     wp.set_name("sealyao");     

8.     wp.set_id(123456);          

9.     wp.set_email("sealyaog@gmail.com");  

10.     wp.SerializeToArray(parray,256);  

11. }  

12.   

13. void get_people()               

14. {  

15.     rap.ParseFromArray(parray,256);  

16.     cout << "Get People from Array:" << endl;  

17.     cout << "\t Name : " <<rap.name() << endl;  

18.     cout << "\t Id : " << rap.id() << endl;  

19.     cout << "\t email : " << rap.email() << endl;  

20. }  

 

2C++ String的序列化和反序列化API

1. //C++string序列化和序列化API  

2. bool SerializeToString(string* output) const;  

3. bool ParseFromString(const string& data);  

4. //使用:  

5. void set_people()               

6. {  

7.     wp.set_name("sealyao");     

8.     wp.set_id(123456);          

9.     wp.set_email("sealyaog@gmail.com");  

10.     wp.SerializeToString(&pstring);  

11. }  

12.   

13. void get_people()               

14. {  

15.     rsp.ParseFromString(pstring);    

16.     cout << "Get People from String:" << endl;  

17.     cout << "\t Name : " <<rsp.name() << endl;  

18.     cout << "\t Id : " << rsp.id() << endl;  

19.     cout << "\t email : " << rsp.email() << endl;  

20. }  

 

3、文件描述符序列化和反序列化API

1.  //文件描述符的序列化和序列化API  

2.  bool SerializeToFileDescriptor(int file_descriptor) const;  

3.  bool ParseFromFileDescriptor(int file_descriptor);  

4.   

5.  //使用:  

6. void set_people()  

7. {  

8.     fd = open(path,O_CREAT|O_TRUNC|O_RDWR,0644);  

9.     if(fd <= 0){  

10.         perror("open");  

11.         exit(0);   

12.     }     

13.     wp.set_name("sealyaog");  

14.     wp.set_id(123456);  

15.     wp.set_email("sealyaog@gmail.com");  

16.     wp.SerializeToFileDescriptor(fd);     

17.     close(fd);  

18. }  

19.   

20. void get_people()  

21. {  

22.     fd = open(path,O_RDONLY);  

23.     if(fd <= 0){  

24.         perror("open");  

25.         exit(0);  

26.     }  

27.     rp.ParseFromFileDescriptor(fd);  

28.     std::cout << "Get People from FD:" << endl;  

29.     std::cout << "\t Name : " <<rp.name() << endl;  

30.     std::cout << "\t Id : " << rp.id() << endl;  

31.     std::cout << "\t email : " << rp.email() << endl;  

32.     close(fd);  

33. }  

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

1. //C++ stream 序列化/反序列化API  

2. bool SerializeToOstream(ostream* output) const;  

3. bool ParseFromIstream(istream* input);  

4.   

5. //使用:  

6. void set_people()  

7. {  

8.     fstream fs(path,ios::out|ios::trunc|ios::binary);  

9.     wp.set_name("sealyaog");  

10.     wp.set_id(123456);  

11.     wp.set_email("sealyaog@gmail.com");  

12.     wp.SerializeToOstream(&fs);      

13.     fs.close();  

14.     fs.clear();  

15. }  

16.   

17. void get_people()  

18. {  

19.     fstream fs(path,ios::in|ios::binary);  

20.     rp.ParseFromIstream(&fs);  

21.     std::cout << "\t Name : " <<rp.name() << endl;  

22.     std::cout << "\t Id : " << rp.id() << endl;   

23.     std::cout << "\t email : " << rp.email() << endl;     

24.     fs.close();  

25.     fs.clear();  

26. }  

 

 

Repeated字段的使用

例子:

Proto:

Package tt_basic;

Message tt_user_list{

repeated string user_account = 1;

repeated int32 sex = 2;

};

 

typedef tt_basic::tt_user_list    tt_b_user_list;

tt_b_user_list   user_list_tt;

添加字段:

user_list_tt.add_user_acccount(xxxx);

user_list_tt.add_sex(xxxx);

 

序列化。。。。。

 

取出字段:

Boost::shared_ptr<tt_b_user_list>user_list_ptr= boost::make_shared<tt_b_user_list>();

User_list_ptr->ParseFromString(recv_str);//反序列化。。。。

For(int k = 0; k < user_list_ptr->user_account_size(); k++)

{

Cout << user_list_ptr->user_account(k) <<    << user_list_ptr->sex(k) << endl;

}

 

 

嵌套类型的使用

1、普通嵌套类型

enum UserStatus { 

OFFLINE = 0; 
ONLINE = 1; 
enum LoginResult { 
LOGON_RESULT_SUCCESS = 0; 
LOGON_RESULT_NOTEXIST = 1; 
LOGON_RESULT_ERROR_PASSWD = 2; 
LOGON_RESULT_ALREADY_LOGON = 3; 
LOGON_RESULT_SERVER_ERROR = 4; 
message UserInfo { 
required int64 acctID = 1; 
required string name = 2; 
required UserStatus status = 3; 
message LogonRespMessage { 
required LoginResult logonResult = 1; 
required UserInfo userInfo = 2; //这里嵌套了UserInfo消息。 

添加字段及序列化

LogonRespMessage logonResp; 

logonResp.set_logonresult(LOGON_RESULT_SUCCESS); 
//如上所述,通过mutable_userinfo函数返回userInfo字段的指针,之后再初始化该对象指针。 
UserInfo* userInfo = logonResp.mutable_userinfo(); 
userInfo->set_acctid(200); 
userInfo->set_name("Tester"); 
userInfo->set_status(OFFLINE); 
int length = logonResp.ByteSize(); 
char* buf = new char[length]; 
logonResp.SerializeToArray(buf,length); 

反序列化及取出字段

logonResp2.ParseFromArray(buf,length); 

printf("LogonResult = %d, UserInfo->acctID = %I64d, UserInfo->name = %s, UserInfo->status = %d\n" 
,logonResp2.logonresult(),logonResp2.userinfo().acctid(),logonResp2.userinfo().name().c_str(),logonResp2.userinfo().status()); 

 

2、repeated嵌套message

message BuddyInfo { 
required UserInfo userInfo = 1; 
required int32 groupID = 2; 
message RetrieveBuddiesResp { 
required int32 buddiesCnt = 1; 
repeated BuddyInfo buddiesInfo = 2; 

 

添加字段及序列化

RetrieveBuddiesResp retrieveResp; 

retrieveResp.set_buddiescnt(2); 
BuddyInfo* buddyInfo = retrieveResp.add_buddiesinfo(); 
buddyInfo->set_groupid(20); 
UserInfo* userInfo = buddyInfo->mutable_userinfo(); 
userInfo->set_acctid(200); 
userInfo->set_name("user1"); 
userInfo->set_status(OFFLINE); 
buddyInfo = retrieveResp.add_buddiesinfo(); 
buddyInfo->set_groupid(21); 
userInfo = buddyInfo->mutable_userinfo(); 
userInfo->set_acctid(201); 
userInfo->set_name("user2"); 
userInfo->set_status(ONLINE); 
int length = retrieveResp.ByteSize(); 
char* buf = new char[length]; 
retrieveResp.SerializeToArray(buf,length); 

反序列化及取出字段

RetrieveBuddiesResp retrieveResp2; 

retrieveResp2.ParseFromArray(buf,length); 
printf("BuddiesCount = %d\n",retrieveResp2.buddiescnt()); 
printf("Repeated Size = %d\n",retrieveResp2.buddiesinfo_size()); 
//这里仅提供了通过容器迭代器的方式遍历数组元素的测试代码。 
//事实上,通过buddiesinfo_sizebuddiesinfo函数亦可循环遍历。 
google::protobuf:: RepeatedPtrField<BuddyInfo>* buddiesInfo = retrieveResp2.mutable_buddiesinfo(); 
google::protobuf:: RepeatedPtrField<BuddyInfo>::iterator it = buddiesInfo->begin(); 
for (; it != buddiesInfo->end(); ++it) { 
printf("BuddyInfo->groupID = %d\n", it->groupid()); 
printf("UserInfo->acctID = %I64d, UserInfo->name = %s, UserInfo->status = %d\n" 
, it->userinfo().acctid(), it->userinfo().name().c_str(),it->userinfo().status()); 

Import的使用

使用import可以引入别的proto文件,这样在该文件中就可以使用别的文件中的类作为自己的数据类型来使用了,比如:

窗体顶端

BaseMessage.proto

Package message

message MessageBase
{
    required int32 opcode = 1;
    // other: sendMgrId, sendId, recvMgrId, recvId, ...
}
message BaseMessage
{
    required MessageBase msgbase = 1;
}
BaseMessage.proto是其它消息proto文件的基础,以容器模块的C2S_GetContainerInfo为例:
ContainerMessage.proto

Package message

import "BaseMessage.proto";
message C2SGetContainerInfoMsg
{
    required MessageBase msgbase = 1;
    optional int32 containerType = 2;
}
.proto文件编写规则
1)所有消息都需要包含msgbase这项,并编号都为1,即:
  required MessageBase msgbase = 1;
2)除了msgbase这项写成required外,其它所有项都写成optional
编译 .proto 文件
protoc -I=. --cpp_out=. ContainerMessage.proto

窗体底端

需要注意的是,要使用import,如果package不一致,使用的时候格式 package.message,如果一致则可直接使用message类型

转载于:https://my.oschina.net/ifraincoat/blog/406339

你可能感兴趣的文章
Shell基础之-grep命令
查看>>
PostgreSQL backup and recovery - online logical backup & recovery
查看>>
C++玫瑰
查看>>
如何在Element UI 对话框里面加载高德地图
查看>>
深入理解编译注解(三)依赖关系 apt/annotationProcessor与Provided的区别
查看>>
类似微信首页弹性滚动和惯性滚动效果的实现——OverScroll
查看>>
试读angular源码第一章:开场与platformBrowserDynamic
查看>>
android插件自定义之多渠道打包插件(支持微信资源混淆andResGuard)
查看>>
Win实用好用软件清单推荐
查看>>
一道关于面向对象面试题
查看>>
程序员的商业模式
查看>>
Vue双向绑定实现
查看>>
阿里数据库内核月报:2017年06月
查看>>
使用html5做的环形进度条
查看>>
如何写一份更好的简历
查看>>
web端播放m3u8视频流注意事项
查看>>
http浅析
查看>>
算法基础-希尔排序
查看>>
面试官: css3动画了解吗? 我: .......
查看>>
使用ConcurrentHashMap一定线程安全?
查看>>