手把手教你实现自定义的应用层协议

1.简述

  • 互联网上充斥着各种各样的网络服务,在对外提供网络服务时,服务端和客户端需要遵循同一套数据通讯协议,才能正常的进行通讯;就好像你跟台湾人沟通用闽南语,跟广东人沟通就用粤语一样。
  • 实现自己的应用功能时,已知的知名协议(http,smtp,ftp等)在安全性、可扩展性等方面不能满足需求,从而需要设计并实现自己的应用层协议。

2.协议分类

2.1按编码方式

  • 二进制协议
    比如网络通信运输层中的tcp协议。
  • 明文的文本协议
    比如应用层的http、redis协议。
    -混合协议(二进制+明文)
    比如苹果公司早期的APNs推送协议。

2.2按协议边界

  • 固定边界协议
    能够明确得知一个协议报文的长度,这样的协议易于解析,比如tcp协议。
  • 模糊边界协议
    无法明确得知一个协议报文的长度,这样的协议解析较为复杂,通常需要通过某些特定的字节来界定报文是否结束,比如http协议。

3.协议优劣的基本评判标准

  • 高效的
    快速的打包解包减少对cpu的占用,高数据压缩率降低对网络带宽的占用。
  • 简单的
    易于人的理解、程序的解析。
  • 易于扩展的
    对可预知的变更,有足够的弹性用于扩展。
  • 容易兼容的
  • 向前兼容,对于旧协议发出的报文,能使用新协议进行解析,只是新协议支持的新功能不能使用。
  • 向后兼容,对于新协议发出的报文,能使用旧协议进行解析,只是新协议支持的新功能不能使用。

4.自定义应用层协议的优缺点

4.1优点

-非知名协议,数据通信更安全,黑客如果要分析协议的漏洞就必须先破译你的通讯协议。
-扩展性更好,可以根据业务需求和发展扩展自己的协议,而已知的知名协议不好扩展。

4.2缺点

-设计难度高,协议需要易扩展,最好能向后向前兼容。
-实现繁琐,需要自己实现序列化和反序列化。

5.动手前的预备知识

5.1大小端

计算机系统在存储数据时起始地址是高地址还是低地址。

  • 大端
    从高地址开始存储。
  • 小端
    从低地址开始存储。
  • 图解
大小端.jpg
  • 判断
    这里以c/c++语言代码为例,使用了c语言中联合体的特性。
#include <stdint.h>
#include <iostream>
using namespace std;
bool bigCheck()
{
    union Check
    {
        char a;
        uint32_t data;
    };
    Check c;
    c.data = 1;
    if (1 == c.a)
    {
        return false;
    }
    return true;
}
int main()
{
    if (bigCheck())
    {
        cout << "big" << endl;
    }
    else
    {
        cout << "small" << endl;
    }
    return 0;
}

5.2网络字节序

顾名思义就是数据在网络传送的字节流中的起始地址的高低,为了避免在网络通信中引入其他复杂性,网络字节序统一是大端的。

5.3本地字节序

本地操作系统的大小端,不同操作系统可能采用不同的字节序。

5.4内存对象与布局

任何变量,不管是堆变量还是栈变量都对应着操作系统中的一块内存,由于内存对齐的要求程序中的变量并不是紧凑存储的,例如一个c语言的结构体Test在内存中的布局可能如下图所示。

struct Test
{
    char a;
    char b;
    int32_t c;
};
内存对象与布局.jpg

5.5序列化与反序列化

  • 将计算机语言中的内存对象转换为网络字节流,例如把c语言中的结构体Test转化成uint8_t data[6]字节流。
  • 将网络字节流转换为计算机语言中的内存对象,例如把uint8_t data[6]字节流转化成c语言中的结构体Test。
序列化与反序列化.jpg

6.一个例子

6.1 协议设计

本协议采用固定边界+混合编码策略。

  • 协议头
    8字节的定长协议头。支持版本号,基于魔数的快速校验,不同服务的复用。定长协议头使协议易于解析且高效。
  • 协议体
    变长json作为协议体。json使用明文文本编码,可读性强、易于扩展、前后兼容、通用的编解码算法。json协议体为协议提供了良好的扩展性和兼容性。
  • 协议可视化图
协议可视化.jpg

6.2 协议实现

talk is easy,just code it,使用c/c++语言来实现。

6.2.1c/c++语言实现

  • 使用结构体MyProtoHead来存储协议头
/*
  协议头
*/
struct MyProtoHead
{
  uint8_t version;  //协议版本号
  uint8_t magic;   //协议魔数
  uint16_t server;  //协议复用的服务号,标识协议之上的不同服务
  uint32_t len;    //协议长度(协议头长度+变长json协议体长度)
};
/*
  协议消息体
*/
struct MyProtoMsg
{
  MyProtoHead head;  //协议头
  Json::Value body;  //协议体
};
  • 打包类
/*
  MyProto打包类
*/
class MyProtoEnCode
{
public:
  //协议消息体打包函数
  uint8_t * encode(MyProtoMsg * pMsg, uint32_t & len);
private:
  //协议头打包函数
  void headEncode(uint8_t * pData, MyProtoMsg * pMsg);
};
  • 解包类
typedef enum MyProtoParserStatus
{
    ON_PARSER_INIT = 0,
    ON_PARSER_HAED = 1,
    ON_PARSER_BODY = 2,
}MyProtoParserStatus;
/*
    MyProto解包类
 */
class MyProtoDeCode
{
public:
    void init();
    void clear();
    bool parser(void * data, size_t len);
    bool empty();
    MyProtoMsg * front();
    void pop();
private:
    bool parserHead(uint8_t ** curData, uint32_t & curLen, 
        uint32_t & parserLen, bool & parserBreak);
    bool parserBody(uint8_t ** curData, uint32_t & curLen, 
        uint32_t & parserLen, bool & parserBreak);
private:
    MyProtoMsg mCurMsg;                     //当前解析中的协议消息体
    queue<MyProtoMsg *> mMsgQ;              //解析好的协议消息队列
    vector<uint8_t> mCurReserved;           //未解析的网络字节流
    MyProtoParserStatus mCurParserStatus;   //当前解析状态
};

6.2.2打包(序列化)

void MyProtoEnCode::headEncode(uint8_t * pData, MyProtoMsg * pMsg)
{
    //设置协议头版本号为1
    *pData = 1; 
    ++pData;
    //设置协议头魔数
    *pData = MY_PROTO_MAGIC;
    ++pData;
    //设置协议服务号,把head.server本地字节序转换为网络字节序
    *(uint16_t *)pData = htons(pMsg->head.server);
    pData += 2;
    //设置协议总长度,把head.len本地字节序转换为网络字节序
    *(uint32_t *)pData = htonl(pMsg->head.len);
}
uint8_t * MyProtoEnCode::encode(MyProtoMsg * pMsg, uint32_t & len)
{
    uint8_t * pData = NULL;
    Json::FastWriter fWriter;
    //协议json体序列化
    string bodyStr = fWriter.write(pMsg->body);
    //计算协议消息序列化后的总长度
    len = MY_PROTO_HEAD_SIZE + (uint32_t)bodyStr.size();
    pMsg->head.len = len;
    //申请协议消息序列化需要的空间
    pData = new uint8_t[len];
    //打包协议头
    headEncode(pData, pMsg);
    //打包协议体
    memcpy(pData + MY_PROTO_HEAD_SIZE, bodyStr.data(), bodyStr.size());
    return pData;
}

6.2.3解包(反序列化)

bool MyProtoDeCode::parserHead(uint8_t ** curData, uint32_t & curLen, 
    uint32_t & parserLen, bool & parserBreak)
{
    parserBreak = false;
    if (curLen < MY_PROTO_HEAD_SIZE)
    {
        parserBreak = true; //终止解析
        return true;
    }
    uint8_t * pData = *curData;
    //解析版本号
    mCurMsg.head.version = *pData;
    pData++;
    //解析魔数
    mCurMsg.head.magic = *pData;
    pData++;
    //魔数不一致,则返回解析失败
    if (MY_PROTO_MAGIC != mCurMsg.head.magic)
    {
        return false;
    }
    //解析服务号
    mCurMsg.head.server = ntohs(*(uint16_t*)pData);
    pData+=2;
    //解析协议消息体的长度
    mCurMsg.head.len = ntohl(*(uint32_t*)pData);
    //异常大包,则返回解析失败
    if (mCurMsg.head.len > MY_PROTO_MAX_SIZE)
    {
        return false;
    }
    //解析指针向前移动MY_PROTO_HEAD_SIZE字节
    (*curData) += MY_PROTO_HEAD_SIZE;
    curLen -= MY_PROTO_HEAD_SIZE;
    parserLen += MY_PROTO_HEAD_SIZE;
    mCurParserStatus = ON_PARSER_HAED;
    return true;
}
bool MyProtoDeCode::parserBody(uint8_t ** curData, uint32_t & curLen, 
    uint32_t & parserLen, bool & parserBreak)
{
    parserBreak = false;
    uint32_t jsonSize = mCurMsg.head.len - MY_PROTO_HEAD_SIZE;
    if (curLen < jsonSize)
    {
        parserBreak = true; //终止解析
        return true;
    }
    Json::Reader reader;    //json解析类
    if (!reader.parse((char *)(*curData), 
        (char *)((*curData) + jsonSize), mCurMsg.body, false))
    {
        return false;
    }
    //解析指针向前移动jsonSize字节
    (*curData) += jsonSize;
    curLen -= jsonSize;
    parserLen += jsonSize;
    mCurParserStatus = ON_PARSER_BODY;
    return true;
}
bool MyProtoDeCode::parser(void * data, size_t len)
{
    if (len <= 0)
    {
        return false;
    }
    uint32_t curLen = 0;
    uint32_t parserLen = 0;
    uint8_t * curData = NULL;
    curData = (uint8_t *)data;
    //把当前要解析的网络字节流写入未解析完字节流之后
    while (len--)
    {
        mCurReserved.push_back(*curData);
        ++curData;
    }
    curLen = mCurReserved.size();
    curData = (uint8_t *)&mCurReserved[0];
    //只要还有未解析的网络字节流,就持续解析
    while (curLen > 0)
    {
        bool parserBreak = false;
        //解析协议头
        if (ON_PARSER_INIT == mCurParserStatus ||
            ON_PARSER_BODY == mCurParserStatus)
        {
            if (!parserHead(&curData, curLen, parserLen, parserBreak))
            {
                return false;
            }
            if (parserBreak) break;
        }
        //解析完协议头,解析协议体
        if (ON_PARSER_HAED == mCurParserStatus)
        {
            if (!parserBody(&curData, curLen, parserLen, parserBreak))
            {
                return false;
            }
            if (parserBreak) break;
        }
        if (ON_PARSER_BODY == mCurParserStatus)
        {
            //拷贝解析完的消息体放入队列中
            MyProtoMsg * pMsg = NULL;
            pMsg = new MyProtoMsg;
            *pMsg = mCurMsg;
            mMsgQ.push(pMsg);
        }
    }
    if (parserLen > 0)
    {
        //删除已经被解析的网络字节流
        mCurReserved.erase(mCurReserved.begin(), mCurReserved.begin() + parserLen);
    }
    return true;
}

7.完整源码与测试

code is easy,just run it.

7.1源码

#include <stdint.h>
#include <stdio.h>
#include <queue>
#include <vector>
#include <iostream>
#include <string.h>
#include <json/json.h>
#include <arpa/inet.h>
using namespace std;

const uint8_t MY_PROTO_MAGIC = 88;
const uint32_t MY_PROTO_MAX_SIZE = 10 * 1024 * 1024; //10M
const uint32_t MY_PROTO_HEAD_SIZE = 8;

typedef enum MyProtoParserStatus
{
  ON_PARSER_INIT = 0,
  ON_PARSER_HAED = 1,
  ON_PARSER_BODY = 2,
}MyProtoParserStatus;

/*
  协议头
*/
struct MyProtoHead
{
  uint8_t version;  //协议版本号
  uint8_t magic;   //协议魔数
  uint16_t server;  //协议复用的服务号,标识协议之上的不同服务
  uint32_t len;    //协议长度(协议头长度+变长json协议体长度)
};

/*
  协议消息体
*/
struct MyProtoMsg
{
  MyProtoHead head;  //协议头
  Json::Value body;  //协议体
};

void myProtoMsgPrint(MyProtoMsg & msg)
{
    string jsonStr = "";
    Json::FastWriter fWriter;
    jsonStr = fWriter.write(msg.body);
    
    printf("Head[version=%d,magic=%d,server=%d,len=%d]\n"
        "Body:%s", msg.head.version, msg.head.magic, 
        msg.head.server, msg.head.len, jsonStr.c_str());
}

/*
  MyProto打包类
*/
class MyProtoEnCode
{
public:
  //协议消息体打包函数
  uint8_t * encode(MyProtoMsg * pMsg, uint32_t & len);
private:
  //协议头打包函数
  void headEncode(uint8_t * pData, MyProtoMsg * pMsg);
};

void MyProtoEnCode::headEncode(uint8_t * pData, MyProtoMsg * pMsg)
{
  //设置协议头版本号为1
  *pData = 1;
  ++pData;

  //设置协议头魔数
  *pData = MY_PROTO_MAGIC;
  ++pData;

  //设置协议服务号,把head.server本地字节序转换为网络字节序
  *(uint16_t *)pData = htons(pMsg->head.server);
  pData += 2;

  //设置协议总长度,把head.len本地字节序转换为网络字节序
  *(uint32_t *)pData = htonl(pMsg->head.len);
}

uint8_t * MyProtoEnCode::encode(MyProtoMsg * pMsg, uint32_t & len)
{
  uint8_t * pData = NULL;
  Json::FastWriter fWriter;
 
  //协议json体序列化
  string bodyStr = fWriter.write(pMsg->body);
  //计算协议消息序列化后的总长度
  len = MY_PROTO_HEAD_SIZE + (uint32_t)bodyStr.size();
  pMsg->head.len = len;
  //申请协议消息序列化需要的空间
  pData = new uint8_t[len];
  //打包协议头
  headEncode(pData, pMsg);
  //打包协议体
  memcpy(pData + MY_PROTO_HEAD_SIZE, bodyStr.data(), bodyStr.size());
 
  return pData;
}

/*
  MyProto解包类
*/
class MyProtoDeCode
{
public:
  void init();
  void clear();
  bool parser(void * data, size_t len);
  bool empty();
  MyProtoMsg * front();
  void pop();
private:
  bool parserHead(uint8_t ** curData, uint32_t & curLen,
    uint32_t & parserLen, bool & parserBreak);
  bool parserBody(uint8_t ** curData, uint32_t & curLen,
    uint32_t & parserLen, bool & parserBreak);
 
private:
  MyProtoMsg mCurMsg;           //当前解析中的协议消息体
  queue<MyProtoMsg *> mMsgQ;       //解析好的协议消息队列
  vector<uint8_t> mCurReserved;      //未解析的网络字节流
  MyProtoParserStatus mCurParserStatus;  //当前解析状态
};

void MyProtoDeCode::init()
{
  mCurParserStatus = ON_PARSER_INIT;
}

void MyProtoDeCode::clear()
{
  MyProtoMsg * pMsg = NULL;
 
  while (!mMsgQ.empty())
  {
    pMsg = mMsgQ.front();
    delete pMsg;
    mMsgQ.pop();
  }
}

bool MyProtoDeCode::parserHead(uint8_t ** curData, uint32_t & curLen,
  uint32_t & parserLen, bool & parserBreak)
{
  parserBreak = false;
  if (curLen < MY_PROTO_HEAD_SIZE)
  {
    parserBreak = true; //终止解析
    return true;
  }

  uint8_t * pData = *curData;
  //解析版本号
  mCurMsg.head.version = *pData;
  pData++;
  //解析魔数
  mCurMsg.head.magic = *pData;
  pData++;
  //魔数不一致,则返回解析失败
  if (MY_PROTO_MAGIC != mCurMsg.head.magic)
  {
    return false;
  }
  //解析服务号
  mCurMsg.head.server = ntohs(*(uint16_t*)pData);
  pData+=2;
  //解析协议消息体的长度
  mCurMsg.head.len = ntohl(*(uint32_t*)pData);
  //异常大包,则返回解析失败
  if (mCurMsg.head.len > MY_PROTO_MAX_SIZE)
  {
    return false;
  }
 
  //解析指针向前移动MY_PROTO_HEAD_SIZE字节
  (*curData) += MY_PROTO_HEAD_SIZE;
  curLen -= MY_PROTO_HEAD_SIZE;
  parserLen += MY_PROTO_HEAD_SIZE;
  mCurParserStatus = ON_PARSER_HAED;

  return true;
}

bool MyProtoDeCode::parserBody(uint8_t ** curData, uint32_t & curLen,
  uint32_t & parserLen, bool & parserBreak)
{
  parserBreak = false;
  uint32_t jsonSize = mCurMsg.head.len - MY_PROTO_HEAD_SIZE;
  if (curLen < jsonSize)
  {
    parserBreak = true; //终止解析
    return true;
  }

  Json::Reader reader;  //json解析类
  if (!reader.parse((char *)(*curData),
    (char *)((*curData) + jsonSize), mCurMsg.body, false))
  {
    return false;
  }

  //解析指针向前移动jsonSize字节
  (*curData) += jsonSize;
  curLen -= jsonSize;
  parserLen += jsonSize;
  mCurParserStatus = ON_PARSER_BODY;

  return true;
}

bool MyProtoDeCode::parser(void * data, size_t len)
{
  if (len <= 0)
  {
    return false;
  }

  uint32_t curLen = 0;
  uint32_t parserLen = 0;
  uint8_t * curData = NULL;
 
  curData = (uint8_t *)data;
  //把当前要解析的网络字节流写入未解析完字节流之后
  while (len--)
  {
    mCurReserved.push_back(*curData);
    ++curData;
  }

  curLen = mCurReserved.size();
  curData = (uint8_t *)&mCurReserved[0];

  //只要还有未解析的网络字节流,就持续解析
  while (curLen > 0)
  {
    bool parserBreak = false;
    //解析协议头
    if (ON_PARSER_INIT == mCurParserStatus ||
      ON_PARSER_BODY == mCurParserStatus)
    {
      if (!parserHead(&curData, curLen, parserLen, parserBreak))
      {
        return false;
      }

      if (parserBreak) break;
    }

    //解析完协议头,解析协议体
    if (ON_PARSER_HAED == mCurParserStatus)
    {
      if (!parserBody(&curData, curLen, parserLen, parserBreak))
      {
        return false;
      }

      if (parserBreak) break;
    }

    if (ON_PARSER_BODY == mCurParserStatus)
    {
      //拷贝解析完的消息体放入队列中
      MyProtoMsg * pMsg = NULL;
      pMsg = new MyProtoMsg;
      *pMsg = mCurMsg;
      mMsgQ.push(pMsg);
    }
  }

  if (parserLen > 0)
  {
    //删除已经被解析的网络字节流
    mCurReserved.erase(mCurReserved.begin(), mCurReserved.begin() + parserLen);
  }

  return true;
}

bool MyProtoDeCode::empty()
{
  return mMsgQ.empty();
}

MyProtoMsg * MyProtoDeCode::front()
{
  MyProtoMsg * pMsg = NULL;
  pMsg = mMsgQ.front();
  return pMsg;
}

void MyProtoDeCode::pop()
{
  mMsgQ.pop();
}

int main()
{
  uint32_t len = 0;
  uint8_t * pData = NULL;
  MyProtoMsg msg1;
  MyProtoMsg msg2;
  MyProtoDeCode myDecode;
  MyProtoEnCode myEncode;

  msg1.head.server = 1;
  msg1.body["op"] = "set";
  msg1.body["key"] = "id";
  msg1.body["value"] = "9856";

  msg2.head.server = 2;
  msg2.body["op"] = "get";
  msg2.body["key"] = "id";

  myDecode.init();
  pData = myEncode.encode(&msg1, len);
  if (!myDecode.parser(pData, len))
  {
    cout << "parser falied!" << endl;
  }
  else
  {
    cout << "msg1 parser successful!" << endl;
  }

  pData = myEncode.encode(&msg2, len);
  if (!myDecode.parser(pData, len))
  {
    cout << "parser falied!" << endl;
  }
  else
  {
    cout << "msg2 parser successful!" << endl;
  }

  MyProtoMsg * pMsg = NULL;
  while (!myDecode.empty())
  {
    pMsg = myDecode.front();
    myProtoMsgPrint(*pMsg);
    myDecode.pop();
  }
 
  return 0;
}

7.2运行测试

运行结果图.png

8.总结

不到350行的代码向我们展示了一个自定义的应用层协议该如何实现,当然这个协议是不够完善的,还可以对其完善,比如对协议体进行加密加强协议的安全性等。

9.写在最后

后续我会在 Linux C/C++后端研发菜鸟成长记 这个专题中完善这个协议

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,934评论 6 13
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,784评论 1 10
  • 136.泛型 泛型代码让你可以写出灵活,可重用的函数和类型,它们可以使用任何类型,受你定义的需求的约束。你可以写出...
    无沣阅读 1,456评论 0 4
  • 我一直孤独的行走,如沙漠中的仙人掌 浅浅的根,对谁都爱得不深 遇到风沙,也遇到雨露 时时刻刻体会着冥想的宁静 那个...
    一言尔阅读 489评论 5 1