嵌入式LwIP学习笔记之数据包管理1

一、数据包管理

  TCP/IP 是一种数据通信机制,因此,协议栈的实现本质上就是对数据包进行处理。 链路层、IP层和TCP层都需要对数据包进解析,并获取响应的数据,数据包在TCP/IP协议栈中是非常重要的部分,数据包管理需要提供一种高效的机制,使协议栈各层能对数据包进行灵活的处理,同时减少数据在各层间传递时的时间与空间开销,这是提高协议栈工作效率的关键点。 在 LwIP 中的描述和管理数据包的结构叫做 pbuf,本文主要分析数据包管理结构pbuf和所有的数据包相关操作函数。

1.1、数据包结构pbuf

在 LwIP 中,数据包管理机制采用数据结构pbuf来描述协议栈中使用的数据包,文件 pbuf.h和 pbuf.c 中实现了协议栈数据包管理相关的所有数据结构和函数。结构 pbuf 的定义如下:

---------pbuf.h---------------------------------------------------------------------

struct pbuf {

 /** next pbuf insingly linked pbuf chain */

  struct pbuf*next;   //构成pbuf 链表时指向下一个 pbuf 结构


  /** pointer tothe actual data in the buffer */

  void *payload;     //数据指针,指向该 pbuf 所记录的数据区域


  /**

   * total lengthof this buffer and all next buffers in chain

   * belonging tothe same packet.

   *

   * For non-queuepacket chains this is the invariant:

   * p->tot_len== p->len + (p->next? p->next->tot_len: 0)

   */

  u16_t tot_len;  //当前 pbuf 及其后续所有 pbuf 中包含的数据总长度


  /** length ofthis buffer */

  u16_t len;                          //当前 pbuf 的数据的长度


  /** pbuf_type asu8_t instead of enum to save space */

  u8_t type;                        //当前 pbuf 的类型


  /** misc flags */

  u8_t flags;                          //状态位,未用到


  /**

   * the referencecount always equals the number of pointers

   * that refer tothis pbuf. This can be pointers from an application,

   * the stackitself, or pbuf->next pointers from a chain.

   */

  u16_t ref;            //指向该pbuf 的指针数,即该 pbuf 被引用的次数

};

pbuf结构体采用链表的数据结构,新的pbuf数据可直接插入到原pbuf的next字段,下面对pbuf结构体的程序进行解析:

[if !supportLists]1、 [endif]next指针是指向下一个pbuf结构,因实际发送/接收的数据包可能很大,但每个pbuf可管理的数据空间有限,所以存在多个pbuf才能描述完一个数据包的情况,采用pbuf链表的数据结构可保证多个pbuf之间存在关联,只需要知道第一个pbuf的地址即可遍历出所有的pbuf;

[if !supportLists]2、 [endif]payload是数据指针,指向pbuf管理的数据起始地址,也就是指向了pbuf结构中ref变量后的地址;

[if !supportLists]3、 [endif]len表示当前pbuf中有效数据的字节数;

[if !supportLists]4、 [endif]tot_len表示当前pbuf和其后所有pbuf的有效数据和,即表示当前pbuf的有效数据长度+pbuf链表下一个pbuf的有效数据长度+…+最后一个pbuf的有效数据长度;

[if !supportLists]5、 [endif]type表示pbuf的类型,pbuf的类型有四种,后面会讲述四种类型的特点;

[if !supportLists]6、 [endif]ref表示该pbuf被应用的次数,初始化pbuf时,该值为1,当其他指针指向该pbuf时,ref的值自增。

1.2、pbuf的类型

  pbuf有4类:PBUF_RAM、PBUF_ROM、PBUF_REF、PBUF_POOL系统中使用了一个专门的枚举类型 pbuf_type 来描述它们:

typedef enum {

  PBUF_RAM, /* pbufdata is stored in RAM */

  PBUF_ROM, /* pbufdata is stored in ROM */

  PBUF_REF, /* pbufcomes from the pbuf pool */

  PBUF_POOL /* pbufpayload refers to RAM */

} pbuf_type;

  PBUF_RAM 类型的 pbuf 空间是通过内存堆分配得到的。这种类型的 pbuf 在协议栈中是使用得最多的,协议栈的待发送数据和应用程序的待发送数据一般都采用这个形式。申请 PBUF_RAM类型 pbuf 时,协议栈会在内存堆中分配相应空间。 下面来看看源代码是怎样申请PBUF_RAM 型的,具体流程参考后面的函数讲解。

p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF

+ offset) + LWIP_MEM_ALIGN_SIZE(length));

  分配成功的 PBUF_RAM 类型 pbuf 如图 1 所示。

[if !vml]

[endif]

       从图1中可看出 pbuf 结构和相应数据在一片连续的内存区域中,注意 payload 并没有指向整个数据区的起始处,而是间隔了一定区域。这段区域就是上面的 offset,它通常用来存储数据包的各种首部字段,如 TCP 报文首部、IP 首部、以太网帧首部等。

  PBUF_POOL 类型和 PBUF_RAM 类型的 pbuf 有很大的相似之处,但它的空间是通过内存池分配得到的。这种类型的 pbuf 可以在极短的时间内得到分配(得益于内存池的优点),在网卡接收数据包时,我们就使用了这种方式包装数据。在申请 PBUF_POOL 类型 pbuf 时,协议栈会在内存池 MEMP_PBUF_POOL 中选择一个或多个POOL,以满足用户空间大小的申请。源代码是通过下面一条语句来完成 POOL 申请的,其中 p 是pbuf 型指针。

 p = memp_malloc(MEMP_PBUF_POOL);

      通常,用户发送的数据可能很长,所以系统会多次调用上面的语句,为用户分配多个 POOL,并把它们按照 pbuf 链表的形式组织在一起,以保证用户的空间请求要求。分配成功的 PBUF_POOL 类型 pbuf 示意如图 7­2所示。

        [if !vml]

[endif]

      PBUF_ROM 和 PBUF_REF 类型的 pbuf 基本相同,它们的申请都是在内存池中分配一个相应的 pbuf结构(即 MEMP_PBUF 类型的 POOL),而不申请数据区的空间 在发送某些静态数据时,可以采用这两种类型的 pbuf,这将大大节省协议栈的内存空间。下面来看看源代码是怎样申请 PBUF_ROM 和PBUF_REF 类型的,其中 p 是 pbuf 型指针。

        [if !vml]

[endif]

对于同一个数据包,他可能使用上述任意的pbuf类型描述,也可能是多个不同类型pbuf连在一起保存一个数据包的数据,如下图两种类型的pbuf组成的pbuf链表情况:

[if !vml]

[endif]

二、数据包相关函数解析

2.1、数据包申请函数

      数据包申请函数有两个重要的参数,一是数据包 pbuf 类型,另一个是该数据包是在协议栈中哪一层被申请的,分配函数会根据这个层次的不同,在 pbuf 数据区域前为相应的协议预留出首部空间,这就是前面所说的 offset 值了。总的来说,LwIP 定义了四个层次,当数据包申请时,所处的层次不同,会导致预留空间的 offset值不同。层次的定义是通过一个枚举类型 pbuf_layer 来实现的,如下代码所示:

#define PBUF_TRANSPORT_HLEN 20 //TCP 报文首部长度

#define PBUF_IP_HLEN 20        //IP 数据报首部长度

typedef enum

{

PBUF_TRANSPORT,   //传输层

PBUF_IP,          //网络层

PBUF_LINK,        //链路层

PBUF_RAW          //原始层,不预留任何空间

} pbuf_layer;

  PBUF_TRANSPORT_HLEN和 PBUF_IP_HLEN,前者是典型的 TCP 报文首部长度,而后者是典型的不带任何选项字段的 IP 首部长度

//参数 layer,指定该pbuf 数据所处的层次,分配函数根据该值在 pbuf 数据//区预留出首部空间;length 表示需要申请的数据区长度,type 指出需要申请//的pbuf 类型

structpbuf *

pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)

{

  struct pbuf *p, *q, *r;//定义了几个局部pbuf指针,在链表封装时使用

  u16_t offset;           //标记首部预留的空间长度,即pbuf头到payload

指向的地址间的空间

  s32_t rem_len;           //还需要申请的数据空间长度

  LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F")\n", length));//打印信息


  /* determine header offset */

  switch (layer) {          //根据层次的不同,计算预留长度

  case PBUF_TRANSPORT:      //在传输层,预留出TCP首部大小

    /* add room for transport (often TCP) layerheader */

offset = PBUF_LINK_HLEN

+ PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN;//计算首部大小

    break;

  case PBUF_IP:        //在网络层或传输层,预留出IP首部的大小

    /* add room for IP layer header*/  

    offset = PBUF_LINK_HLEN +PBUF_IP_HLEN;  //计算IP首部大小

    break;

  case PBUF_LINK:   //在链路层或以上层,需要留出链路层首部大小

    /* add room for link layerheader */

    offset = PBUF_LINK_HLEN; //以太网头部大小,即14字节

    break;

  case PBUF_RAW:    //为原始层,不预留空间(常用于数据包接收)

    offset = 0;

    break;

  default:

    LWIP_ASSERT("pbuf_alloc:bad pbuf layer", 0);

    return NULL;

  }


  switch (type) {

  case PBUF_POOL:           //PBUF_POOL类型,可能需分配多个POOL

    /* allocate head of pbuf chaininto p */

    p = (struct pbuf*)memp_malloc(MEMP_PBUF_POOL); //分配第一个POOL

    LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_TRACE, ("pbuf_alloc: allocated pbuf %p\n", (void *)p)); //打印信息

    if (p == NULL) {       //说明POOL没有多余空间申请

      PBUF_POOL_IS_EMPTY();

      return NULL;

    }

    p->type = type;  //初始化pbuf的type和next字段

    p->next = NULL;


    /* make the payload pointerpoint 'offset' bytes into pbuf data memory */

    p->payload = LWIP_MEM_ALIGN((void*)((u8_t *)p + (SIZEOF_STRUCT_PBUF + offset))); //初始化payload,指向数据段的起始地址,预留头部空间offset

    LWIP_ASSERT("pbuf_alloc:pbuf p->payload properly aligned",

           ((mem_ptr_t)p->payload % MEM_ALIGNMENT) == 0);   //打印信息

    /* the total length of the pbufchain is the requested size */

    p->tot_len = length;   //初始化总长度字段

    /* set the length of the firstpbuf in the chain */

    p->len = LWIP_MIN(length,PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));//设置len字段,若当前POOL能放下所有数据,则len的值为length,否则len为实际数据区的长度

    LWIP_ASSERT("checkp->payload + p->len does not overflow pbuf",

               ((u8_t*)p->payload + p->len <=

                 (u8_t*)p +SIZEOF_STRUCT_PBUF + PBUF_POOL_BUFSIZE_ALIGNED));

   LWIP_ASSERT("PBUF_POOL_BUFSIZE must be bigger thanMEM_ALIGNMENT",

      (PBUF_POOL_BUFSIZE_ALIGNED -LWIP_MEM_ALIGN_SIZE(offset)) > 0 );

    /* set reference count (neededhere in case we fail) */

    p->ref = 1;            //ref设置为1

    /* now allocate the tail of thepbuf chain */

    /* remember first pbuf forlinkage in next iteration */

    r = p;  //检测已经分配的POOL是否能满足用户长度要求,不能则继续分配

    /* remaining length to beallocated */

    rem_len = length - p->len;  //计算还需要申请的长度

    /* any remaining pbufs to beallocated? */

    while (rem_len > 0) {       //遍历分配剩余的长度空间

      q = (struct pbuf*)memp_malloc(MEMP_PBUF_POOL);

      if (q == NULL) {          //若分配失败,则释放链表p上的所有POOL

        PBUF_POOL_IS_EMPTY();   //标记POOL已经分配完

        /* free chain so farallocated */

        pbuf_free(p);   //释放p

        /* bail out unsuccesfully */

        return NULL;

      }

//分配成功后初始化q的所有字段,并接q连接到p上

      q->type = type;

      q->flags = 0;

      q->next = NULL;

      /* make previous pbuf point tothis pbuf */

      r->next = q;   //q连接大p的尾部

      /* set total length of thispbuf and next in chain */

      LWIP_ASSERT("rem_len

      q->tot_len =(u16_t)rem_len;

      /* this pbuf length is poolsize, unless smaller sized tail */

      q->len =LWIP_MIN((u16_t)rem_len, PBUF_POOL_BUFSIZE_ALIGNED);

      q->payload = (void *)((u8_t*)q + SIZEOF_STRUCT_PBUF);

      LWIP_ASSERT("pbuf_alloc:pbuf q->payload properly aligned",

             ((mem_ptr_t)q->payload % MEM_ALIGNMENT) == 0);

      LWIP_ASSERT("checkp->payload + p->len does not overflow pbuf",

                 ((u8_t*)p->payload + p->len <=

                   (u8_t*)p +SIZEOF_STRUCT_PBUF + PBUF_POOL_BUFSIZE_ALIGNED));

      q->ref = 1;

      /* calculate remaining lengthto be allocated */

      rem_len -= q->len;       //更新还需要申请的长度

      /* remember this pbuf forlinkage in next iteration */

      r = q;      //r指向链表p的最后一个pbuf,即为q

    }  

    break; //PBUF_POOL类型的pbuf申请完成

  case PBUF_RAM:      //直接在内存堆中申请

    /* If pbuf is to be allocated inRAM, allocate memory for it. */

    p = (structpbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) +LWIP_MEM_ALIGN_SIZE(length));

    if (p == NULL) {    //申请失败,不需要释放

      return NULL;

    }

/* Set up internalstructure of the pbuf. */

//申请成功后,初始化pbuf的各个字段

    p->payload =LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset));

    p->len = p->tot_len =length;

    p->next = NULL;

    p->type = type;


    LWIP_ASSERT("pbuf_alloc:pbuf->payload properly aligned",

           ((mem_ptr_t)p->payload% MEM_ALIGNMENT) == 0);

    break;        //pbuf申请完毕

  /* pbuf references existing(non-volatile static constant) ROM payload? */

  case PBUF_ROM:  //PBUF_ROM和PBUF_REF类型的pbuf,只分配pbuf结构

  /* pbuf references existing(externally allocated) RAM payload? */

  case PBUF_REF:  //不分配数据的空间,也不分配预留的首部空间

    /* only allocate memory for thepbuf structure */

    p = (struct pbuf*)memp_malloc(MEMP_PBUF);

    if (p == NULL) {    //分配失败后,直接返回

      LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_LEVEL_SERIOUS,

                  ("pbuf_alloc:Could not allocate MEMP_PBUF for PBUF_%s.\n",

                  (type == PBUF_ROM)? "ROM" : "REF"));

      return NULL;

    }

    /* caller must set this fieldproperly, afterwards */

    //分配成功后初始化p,payload需要根据实际数据位置来设置

p->payload = NULL;

    p->len = p->tot_len =length;

    p->next = NULL;

    p->type = type;

    break;        //pbuf分配完毕

  default:

    LWIP_ASSERT("pbuf_alloc:erroneous type", 0);

    return NULL;

  }

  /* set reference count */

 //到这一步,说明pbuf申请成功,设置剩余的字段,并返回pbuf指针

  p->ref = 1;

  /* set flags */

  p->flags = 0;

  LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F") == %p\n",length, (void *)p));

  return p;

}

在这个函数中,申请一个 PBUF_POOL 类型的 pbuf比较麻烦,可能存在需要申请多次的情况。

p =pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_POOL)

这个语句申请了一个 PBUF_POOL 类型的 pbuf,且其申请的协议层为 PBUF_RAW,所以pbuf_alloc 函数不会在数据区前预留出任何首部空间;通过使用 p­>payload,就可以实现对 pbuf 中数据区的读取或写入操作了。

在 TCP 层要申请一个数据包时,常常调用下面的语句:

p =pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)

  它告诉数据包分配函数,使用 PBUF_RAM 类型的 pbuf,且数据区前应该预留一部分的首部空间,由于这里是 PBUF_TRANSPORT 层,所以预留空间将有 54 字节,即 TCP 首部长度PBUF_TRANSPORT_HLEN(20 字节)、IP 数据包首部长度 PBUF_IP_HLEN(20 字节)以及以太网帧首部长度(14 字节)。当数据包往下层递交,各层协议就可以直接操作这些预留空间中的数据,以实现数据包首部的填写,这样就避免了数据的拷贝。

2.2、数据包释放函数

  假如现在我们的 pbuf 链表由 A,B,C 三个 pbuf 结构连接起来,结构为 A­­­>B­­­>C,利用 pbuf_free(A)函数来删除 pbuf 结构,下面用 ABC 的几组不同 ref值来看看删除结果:

  (1)1­>2­>3 函数执行后变为 ...1­>3,节点 BC 仍在;  (2)3­>3­>3 函数执行后变为 2­>3­>3,节点 ABC 仍在;  (3)1­>1­>2 函数执行后变为......1,节点 C 仍在;  (4)2­>1­>1 函数执行后变为 1­>1­>1,节点 ABC 仍在;  (5)1­>1­>1 函数执行后变为.......,节点全部被删除。  假如在上面的第(4)种情况下,错误的调用数据包释放函数,如 pbuf_free(B),这会导致严重的错误。

//函数的返回值为成功删除的 pbuf 个数

u8_t pbuf_free(structpbuf *p)

{

  u16_t type;

  struct pbuf *q;         //定义一个pbuf的指针

  u8_t count;


  if (p == NULL) {         //若pbuf为0,直接返回

    LWIP_ASSERT("p != NULL", p !=NULL);

    /* if assertions are disabled, proceed withdebug output */

    LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_LEVEL_SERIOUS,

      ("pbuf_free(p == NULL) wascalled.\n"));

    return 0;

  }

  LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free(%p)\n",(void *)p));

  PERF_START;

  LWIP_ASSERT("pbuf_free: sane type",

    p->type == PBUF_RAM || p->type ==PBUF_ROM ||

    p->type == PBUF_REF || p->type ==PBUF_POOL);


  count = 0;             //记录值清0

  /* de-allocate all consecutive pbufs from thehead of the chain that

   * obtain a zero reference count afterdecrementing*/

  while (p != NULL) {    //直到pbuf释放完毕

    u16_t ref;

    SYS_ARCH_DECL_PROTECT(old_level);      //申请临界区保护变量

    /* Since decrementing ref cannot beguaranteed to be a single machine operation

     * we must protect it. We put the new refinto a local variable to prevent

     * further protection. */

    SYS_ARCH_PROTECT(old_level);          //进入临界区

    /* all pbufs in a chain are referenced atleast once */

    LWIP_ASSERT("pbuf_free: p->ref >0", p->ref > 0);

    /* decrease reference count (number ofpointers to pbuf) */

    ref = --(p->ref);   //该pbuf引用次数减1,并记录

    SYS_ARCH_UNPROTECT(old_level);  //退出临界保护区

    /* this pbuf is no longer referenced to? */

    if (ref == 0) {    //若pbuf被引用的次数为0,则删除该pbuf

      /* remember next pbuf in chain for nextiteration */

      q = p->next;    //q记录链表中的下一个pbuf

      LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE,("pbuf_free: deallocating %p\n", (void *)p));

      type = p->type;  //判断要删除的pbuf类型

#ifLWIP_SUPPORT_CUSTOM_PBUF

 /* is this a custom pbuf? */

if ((p->flags& PBUF_FLAG_IS_CUSTOM) != 0) {    //调用用户自己的函数申请的pbuf

    structpbuf_custom *pc = (struct pbuf_custom*)p;

    LWIP_ASSERT("pc->custom_free_function!= NULL", pc->custom_free_function != NULL);

    pc->custom_free_function(p);   //调用用户自己的释放函数,释放pbuf

 } else

#endif /*LWIP_SUPPORT_CUSTOM_PBUF */

 {

        /* is this a pbuf from the pool? */

        if (type == PBUF_POOL) {   //不同类型调用不同的内存管理删除函数

          memp_free(MEMP_PBUF_POOL, p);

        /* is this a ROM or RAM referencingpbuf? */

        } else if (type == PBUF_ROM || type ==PBUF_REF) {

          memp_free(MEMP_PBUF, p);

        /* type == PBUF_RAM */

        } else {

          mem_free(p);

        }

      }

      count++;    //删除记录值加1

      /* proceed to next pbuf */

      p = q;      //处理下一个pbuf

    } else {

      LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE,("pbuf_free: %p has ref %"U16_F", ending here.\n", (void*)p, ref));

      /* stop walking through the chain */

      p = NULL;    //p设置为NULL,程序退出while

    }

  }

  PERF_STOP("pbuf_free");

  /* return number of de-allocated pbufs */

  return count;    //返回成功删除的pbuf个数

}

当可以删除某个pbuf结构时,函数pbuf_free首先检查这个 pbuf是属于四个类型中的哪种,根据类型的不同, 调用不同的内存释放函数进行删除。

5、其他数据包操作函数

  pbuf_realloc 函数在相应 pbuf(链表)尾部释放一定的空间,将数据包 pbuf 中的数据长度减少为某个长度值。对于 PBUF_RAM 类型的 pbuf,函数将调用内存堆管理中介绍到的 mem_realloc 函数,释放这些多余的空间;对于其他三种类型的 pbuf,该函数只是修改 pbuf 中的长度字段值,并不释放对应的内存池空间。

  pbuf_header 函数用于调整 pbuf 的 payload 指针(向前或向后移动一定的字节数),在前面也说到过了,在 pbuf 的数据区前可能会预留一些协议首部空间,而 pbuf 被创建时,payload 指针是指向数据区的,为了实现对这些预留空间的操作,可以调用函数 pbuf_header 使 payload 指针指向数据区前的首部字段,这就为各层对数据包首部的操作提供了方便。当然,进行这个操作的时候,len和 tot_len 字段值也会随之更新。

  pbuf_take 函数用于向 pbuf 的数据区域拷贝数据;pbuf_copy 函数用于将一个任何类型的 pbuf中的数据拷贝到一个 PBUF_RAM 类型的 pbuf 中。pbuf_chain 函数用于连接两个 pbuf(链表)为一个 pbuf 链表;pbuf_ref 函数用于将 pbuf 中的 ref 值加 1。

 一、数据包管理

  TCP/IP 是一种数据通信机制,因此,协议栈的实现本质上就是对数据包进行处理。 链路层、IP层和TCP层都需要对数据包进解析,并获取响应的数据,数据包在TCP/IP协议栈中是非常重要的部分,数据包管理需要提供一种高效的机制,使协议栈各层能对数据包进行灵活的处理,同时减少数据在各层间传递时的时间与空间开销,这是提高协议栈工作效率的关键点。 在 LwIP 中的描述和管理数据包的结构叫做 pbuf,本文主要分析数据包管理结构pbuf和所有的数据包相关操作函数。

1.1、数据包结构pbuf

在 LwIP 中,数据包管理机制采用数据结构pbuf来描述协议栈中使用的数据包,文件 pbuf.h和 pbuf.c 中实现了协议栈数据包管理相关的所有数据结构和函数。结构 pbuf 的定义如下:

---------pbuf.h---------------------------------------------------------------------

struct pbuf {

 /** next pbuf insingly linked pbuf chain */

  struct pbuf*next;   //构成pbuf 链表时指向下一个 pbuf 结构


  /** pointer tothe actual data in the buffer */

  void *payload;     //数据指针,指向该 pbuf 所记录的数据区域


  /**

   * total lengthof this buffer and all next buffers in chain

   * belonging tothe same packet.

   *

   * For non-queuepacket chains this is the invariant:

   * p->tot_len== p->len + (p->next? p->next->tot_len: 0)

   */

  u16_t tot_len;  //当前 pbuf 及其后续所有 pbuf 中包含的数据总长度


  /** length ofthis buffer */

  u16_t len;                          //当前 pbuf 的数据的长度


  /** pbuf_type asu8_t instead of enum to save space */

  u8_t type;                        //当前 pbuf 的类型


  /** misc flags */

  u8_t flags;                          //状态位,未用到


  /**

   * the referencecount always equals the number of pointers

   * that refer tothis pbuf. This can be pointers from an application,

   * the stackitself, or pbuf->next pointers from a chain.

   */

  u16_t ref;            //指向该pbuf 的指针数,即该 pbuf 被引用的次数

};

pbuf结构体采用链表的数据结构,新的pbuf数据可直接插入到原pbuf的next字段,下面对pbuf结构体的程序进行解析:

[if !supportLists]1、 [endif]next指针是指向下一个pbuf结构,因实际发送/接收的数据包可能很大,但每个pbuf可管理的数据空间有限,所以存在多个pbuf才能描述完一个数据包的情况,采用pbuf链表的数据结构可保证多个pbuf之间存在关联,只需要知道第一个pbuf的地址即可遍历出所有的pbuf;

[if !supportLists]2、 [endif]payload是数据指针,指向pbuf管理的数据起始地址,也就是指向了pbuf结构中ref变量后的地址;

[if !supportLists]3、 [endif]len表示当前pbuf中有效数据的字节数;

[if !supportLists]4、 [endif]tot_len表示当前pbuf和其后所有pbuf的有效数据和,即表示当前pbuf的有效数据长度+pbuf链表下一个pbuf的有效数据长度+…+最后一个pbuf的有效数据长度;

[if !supportLists]5、 [endif]type表示pbuf的类型,pbuf的类型有四种,后面会讲述四种类型的特点;

[if !supportLists]6、 [endif]ref表示该pbuf被应用的次数,初始化pbuf时,该值为1,当其他指针指向该pbuf时,ref的值自增。

1.2、pbuf的类型

  pbuf有4类:PBUF_RAM、PBUF_ROM、PBUF_REF、PBUF_POOL系统中使用了一个专门的枚举类型 pbuf_type 来描述它们:

typedef enum {

  PBUF_RAM, /* pbufdata is stored in RAM */

  PBUF_ROM, /* pbufdata is stored in ROM */

  PBUF_REF, /* pbufcomes from the pbuf pool */

  PBUF_POOL /* pbufpayload refers to RAM */

} pbuf_type;

  PBUF_RAM 类型的 pbuf 空间是通过内存堆分配得到的。这种类型的 pbuf 在协议栈中是使用得最多的,协议栈的待发送数据和应用程序的待发送数据一般都采用这个形式。申请 PBUF_RAM类型 pbuf 时,协议栈会在内存堆中分配相应空间。 下面来看看源代码是怎样申请PBUF_RAM 型的,具体流程参考后面的函数讲解。

p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF

+ offset) + LWIP_MEM_ALIGN_SIZE(length));

  分配成功的 PBUF_RAM 类型 pbuf 如图 1 所示。

[if !vml]

[endif]

       从图1中可看出 pbuf 结构和相应数据在一片连续的内存区域中,注意 payload 并没有指向整个数据区的起始处,而是间隔了一定区域。这段区域就是上面的 offset,它通常用来存储数据包的各种首部字段,如 TCP 报文首部、IP 首部、以太网帧首部等。

  PBUF_POOL 类型和 PBUF_RAM 类型的 pbuf 有很大的相似之处,但它的空间是通过内存池分配得到的。这种类型的 pbuf 可以在极短的时间内得到分配(得益于内存池的优点),在网卡接收数据包时,我们就使用了这种方式包装数据。在申请 PBUF_POOL 类型 pbuf 时,协议栈会在内存池 MEMP_PBUF_POOL 中选择一个或多个POOL,以满足用户空间大小的申请。源代码是通过下面一条语句来完成 POOL 申请的,其中 p 是pbuf 型指针。

 p = memp_malloc(MEMP_PBUF_POOL);

      通常,用户发送的数据可能很长,所以系统会多次调用上面的语句,为用户分配多个 POOL,并把它们按照 pbuf 链表的形式组织在一起,以保证用户的空间请求要求。分配成功的 PBUF_POOL 类型 pbuf 示意如图 7­2所示。

        [if !vml]

[endif]

      PBUF_ROM 和 PBUF_REF 类型的 pbuf 基本相同,它们的申请都是在内存池中分配一个相应的 pbuf结构(即 MEMP_PBUF 类型的 POOL),而不申请数据区的空间 在发送某些静态数据时,可以采用这两种类型的 pbuf,这将大大节省协议栈的内存空间。下面来看看源代码是怎样申请 PBUF_ROM 和PBUF_REF 类型的,其中 p 是 pbuf 型指针。

        [if !vml]

[endif]

对于同一个数据包,他可能使用上述任意的pbuf类型描述,也可能是多个不同类型pbuf连在一起保存一个数据包的数据,如下图两种类型的pbuf组成的pbuf链表情况:

[if !vml]

[endif]

二、数据包相关函数解析

2.1、数据包申请函数

      数据包申请函数有两个重要的参数,一是数据包 pbuf 类型,另一个是该数据包是在协议栈中哪一层被申请的,分配函数会根据这个层次的不同,在 pbuf 数据区域前为相应的协议预留出首部空间,这就是前面所说的 offset 值了。总的来说,LwIP 定义了四个层次,当数据包申请时,所处的层次不同,会导致预留空间的 offset值不同。层次的定义是通过一个枚举类型 pbuf_layer 来实现的,如下代码所示:

#define PBUF_TRANSPORT_HLEN 20 //TCP 报文首部长度

#define PBUF_IP_HLEN 20        //IP 数据报首部长度

typedef enum

{

PBUF_TRANSPORT,   //传输层

PBUF_IP,          //网络层

PBUF_LINK,        //链路层

PBUF_RAW          //原始层,不预留任何空间

} pbuf_layer;

  PBUF_TRANSPORT_HLEN和 PBUF_IP_HLEN,前者是典型的 TCP 报文首部长度,而后者是典型的不带任何选项字段的 IP 首部长度

//参数 layer,指定该pbuf 数据所处的层次,分配函数根据该值在 pbuf 数据//区预留出首部空间;length 表示需要申请的数据区长度,type 指出需要申请//的pbuf 类型

structpbuf *

pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)

{

  struct pbuf *p, *q, *r;//定义了几个局部pbuf指针,在链表封装时使用

  u16_t offset;           //标记首部预留的空间长度,即pbuf头到payload

指向的地址间的空间

  s32_t rem_len;           //还需要申请的数据空间长度

  LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F")\n", length));//打印信息


  /* determine header offset */

  switch (layer) {          //根据层次的不同,计算预留长度

  case PBUF_TRANSPORT:      //在传输层,预留出TCP首部大小

    /* add room for transport (often TCP) layerheader */

offset = PBUF_LINK_HLEN

+ PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN;//计算首部大小

    break;

  case PBUF_IP:        //在网络层或传输层,预留出IP首部的大小

    /* add room for IP layer header*/  

    offset = PBUF_LINK_HLEN +PBUF_IP_HLEN;  //计算IP首部大小

    break;

  case PBUF_LINK:   //在链路层或以上层,需要留出链路层首部大小

    /* add room for link layerheader */

    offset = PBUF_LINK_HLEN; //以太网头部大小,即14字节

    break;

  case PBUF_RAW:    //为原始层,不预留空间(常用于数据包接收)

    offset = 0;

    break;

  default:

    LWIP_ASSERT("pbuf_alloc:bad pbuf layer", 0);

    return NULL;

  }


  switch (type) {

  case PBUF_POOL:           //PBUF_POOL类型,可能需分配多个POOL

    /* allocate head of pbuf chaininto p */

    p = (struct pbuf*)memp_malloc(MEMP_PBUF_POOL); //分配第一个POOL

    LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_TRACE, ("pbuf_alloc: allocated pbuf %p\n", (void *)p)); //打印信息

    if (p == NULL) {       //说明POOL没有多余空间申请

      PBUF_POOL_IS_EMPTY();

      return NULL;

    }

    p->type = type;  //初始化pbuf的type和next字段

    p->next = NULL;


    /* make the payload pointerpoint 'offset' bytes into pbuf data memory */

    p->payload = LWIP_MEM_ALIGN((void*)((u8_t *)p + (SIZEOF_STRUCT_PBUF + offset))); //初始化payload,指向数据段的起始地址,预留头部空间offset

    LWIP_ASSERT("pbuf_alloc:pbuf p->payload properly aligned",

           ((mem_ptr_t)p->payload % MEM_ALIGNMENT) == 0);   //打印信息

    /* the total length of the pbufchain is the requested size */

    p->tot_len = length;   //初始化总长度字段

    /* set the length of the firstpbuf in the chain */

    p->len = LWIP_MIN(length,PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));//设置len字段,若当前POOL能放下所有数据,则len的值为length,否则len为实际数据区的长度

    LWIP_ASSERT("checkp->payload + p->len does not overflow pbuf",

               ((u8_t*)p->payload + p->len <=

                 (u8_t*)p +SIZEOF_STRUCT_PBUF + PBUF_POOL_BUFSIZE_ALIGNED));

   LWIP_ASSERT("PBUF_POOL_BUFSIZE must be bigger thanMEM_ALIGNMENT",

      (PBUF_POOL_BUFSIZE_ALIGNED -LWIP_MEM_ALIGN_SIZE(offset)) > 0 );

    /* set reference count (neededhere in case we fail) */

    p->ref = 1;            //ref设置为1

    /* now allocate the tail of thepbuf chain */

    /* remember first pbuf forlinkage in next iteration */

    r = p;  //检测已经分配的POOL是否能满足用户长度要求,不能则继续分配

    /* remaining length to beallocated */

    rem_len = length - p->len;  //计算还需要申请的长度

    /* any remaining pbufs to beallocated? */

    while (rem_len > 0) {       //遍历分配剩余的长度空间

      q = (struct pbuf*)memp_malloc(MEMP_PBUF_POOL);

      if (q == NULL) {          //若分配失败,则释放链表p上的所有POOL

        PBUF_POOL_IS_EMPTY();   //标记POOL已经分配完

        /* free chain so farallocated */

        pbuf_free(p);   //释放p

        /* bail out unsuccesfully */

        return NULL;

      }

//分配成功后初始化q的所有字段,并接q连接到p上

      q->type = type;

      q->flags = 0;

      q->next = NULL;

      /* make previous pbuf point tothis pbuf */

      r->next = q;   //q连接大p的尾部

      /* set total length of thispbuf and next in chain */

      LWIP_ASSERT("rem_len

      q->tot_len =(u16_t)rem_len;

      /* this pbuf length is poolsize, unless smaller sized tail */

      q->len =LWIP_MIN((u16_t)rem_len, PBUF_POOL_BUFSIZE_ALIGNED);

      q->payload = (void *)((u8_t*)q + SIZEOF_STRUCT_PBUF);

      LWIP_ASSERT("pbuf_alloc:pbuf q->payload properly aligned",

             ((mem_ptr_t)q->payload % MEM_ALIGNMENT) == 0);

      LWIP_ASSERT("checkp->payload + p->len does not overflow pbuf",

                 ((u8_t*)p->payload + p->len <=

                   (u8_t*)p +SIZEOF_STRUCT_PBUF + PBUF_POOL_BUFSIZE_ALIGNED));

      q->ref = 1;

      /* calculate remaining lengthto be allocated */

      rem_len -= q->len;       //更新还需要申请的长度

      /* remember this pbuf forlinkage in next iteration */

      r = q;      //r指向链表p的最后一个pbuf,即为q

    }  

    break; //PBUF_POOL类型的pbuf申请完成

  case PBUF_RAM:      //直接在内存堆中申请

    /* If pbuf is to be allocated inRAM, allocate memory for it. */

    p = (structpbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) +LWIP_MEM_ALIGN_SIZE(length));

    if (p == NULL) {    //申请失败,不需要释放

      return NULL;

    }

/* Set up internalstructure of the pbuf. */

//申请成功后,初始化pbuf的各个字段

    p->payload =LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset));

    p->len = p->tot_len =length;

    p->next = NULL;

    p->type = type;


    LWIP_ASSERT("pbuf_alloc:pbuf->payload properly aligned",

           ((mem_ptr_t)p->payload% MEM_ALIGNMENT) == 0);

    break;        //pbuf申请完毕

  /* pbuf references existing(non-volatile static constant) ROM payload? */

  case PBUF_ROM:  //PBUF_ROM和PBUF_REF类型的pbuf,只分配pbuf结构

  /* pbuf references existing(externally allocated) RAM payload? */

  case PBUF_REF:  //不分配数据的空间,也不分配预留的首部空间

    /* only allocate memory for thepbuf structure */

    p = (struct pbuf*)memp_malloc(MEMP_PBUF);

    if (p == NULL) {    //分配失败后,直接返回

      LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_LEVEL_SERIOUS,

                  ("pbuf_alloc:Could not allocate MEMP_PBUF for PBUF_%s.\n",

                  (type == PBUF_ROM)? "ROM" : "REF"));

      return NULL;

    }

    /* caller must set this fieldproperly, afterwards */

    //分配成功后初始化p,payload需要根据实际数据位置来设置

p->payload = NULL;

    p->len = p->tot_len =length;

    p->next = NULL;

    p->type = type;

    break;        //pbuf分配完毕

  default:

    LWIP_ASSERT("pbuf_alloc:erroneous type", 0);

    return NULL;

  }

  /* set reference count */

 //到这一步,说明pbuf申请成功,设置剩余的字段,并返回pbuf指针

  p->ref = 1;

  /* set flags */

  p->flags = 0;

  LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F") == %p\n",length, (void *)p));

  return p;

}

在这个函数中,申请一个 PBUF_POOL 类型的 pbuf比较麻烦,可能存在需要申请多次的情况。

p =pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_POOL)

这个语句申请了一个 PBUF_POOL 类型的 pbuf,且其申请的协议层为 PBUF_RAW,所以pbuf_alloc 函数不会在数据区前预留出任何首部空间;通过使用 p­>payload,就可以实现对 pbuf 中数据区的读取或写入操作了。

在 TCP 层要申请一个数据包时,常常调用下面的语句:

p =pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)

  它告诉数据包分配函数,使用 PBUF_RAM 类型的 pbuf,且数据区前应该预留一部分的首部空间,由于这里是 PBUF_TRANSPORT 层,所以预留空间将有 54 字节,即 TCP 首部长度PBUF_TRANSPORT_HLEN(20 字节)、IP 数据包首部长度 PBUF_IP_HLEN(20 字节)以及以太网帧首部长度(14 字节)。当数据包往下层递交,各层协议就可以直接操作这些预留空间中的数据,以实现数据包首部的填写,这样就避免了数据的拷贝。

2.2、数据包释放函数

  假如现在我们的 pbuf 链表由 A,B,C 三个 pbuf 结构连接起来,结构为 A­­­>B­­­>C,利用 pbuf_free(A)函数来删除 pbuf 结构,下面用 ABC 的几组不同 ref值来看看删除结果:

  (1)1­>2­>3 函数执行后变为 ...1­>3,节点 BC 仍在;  (2)3­>3­>3 函数执行后变为 2­>3­>3,节点 ABC 仍在;  (3)1­>1­>2 函数执行后变为......1,节点 C 仍在;  (4)2­>1­>1 函数执行后变为 1­>1­>1,节点 ABC 仍在;  (5)1­>1­>1 函数执行后变为.......,节点全部被删除。  假如在上面的第(4)种情况下,错误的调用数据包释放函数,如 pbuf_free(B),这会导致严重的错误。

//函数的返回值为成功删除的 pbuf 个数

u8_t pbuf_free(structpbuf *p)

{

  u16_t type;

  struct pbuf *q;         //定义一个pbuf的指针

  u8_t count;


  if (p == NULL) {         //若pbuf为0,直接返回

    LWIP_ASSERT("p != NULL", p !=NULL);

    /* if assertions are disabled, proceed withdebug output */

    LWIP_DEBUGF(PBUF_DEBUG |LWIP_DBG_LEVEL_SERIOUS,

      ("pbuf_free(p == NULL) wascalled.\n"));

    return 0;

  }

  LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free(%p)\n",(void *)p));

  PERF_START;

  LWIP_ASSERT("pbuf_free: sane type",

    p->type == PBUF_RAM || p->type ==PBUF_ROM ||

    p->type == PBUF_REF || p->type ==PBUF_POOL);


  count = 0;             //记录值清0

  /* de-allocate all consecutive pbufs from thehead of the chain that

   * obtain a zero reference count afterdecrementing*/

  while (p != NULL) {    //直到pbuf释放完毕

    u16_t ref;

    SYS_ARCH_DECL_PROTECT(old_level);      //申请临界区保护变量

    /* Since decrementing ref cannot beguaranteed to be a single machine operation

     * we must protect it. We put the new refinto a local variable to prevent

     * further protection. */

    SYS_ARCH_PROTECT(old_level);          //进入临界区

    /* all pbufs in a chain are referenced atleast once */

    LWIP_ASSERT("pbuf_free: p->ref >0", p->ref > 0);

    /* decrease reference count (number ofpointers to pbuf) */

    ref = --(p->ref);   //该pbuf引用次数减1,并记录

    SYS_ARCH_UNPROTECT(old_level);  //退出临界保护区

    /* this pbuf is no longer referenced to? */

    if (ref == 0) {    //若pbuf被引用的次数为0,则删除该pbuf

      /* remember next pbuf in chain for nextiteration */

      q = p->next;    //q记录链表中的下一个pbuf

      LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE,("pbuf_free: deallocating %p\n", (void *)p));

      type = p->type;  //判断要删除的pbuf类型

#ifLWIP_SUPPORT_CUSTOM_PBUF

 /* is this a custom pbuf? */

if ((p->flags& PBUF_FLAG_IS_CUSTOM) != 0) {    //调用用户自己的函数申请的pbuf

    structpbuf_custom *pc = (struct pbuf_custom*)p;

    LWIP_ASSERT("pc->custom_free_function!= NULL", pc->custom_free_function != NULL);

    pc->custom_free_function(p);   //调用用户自己的释放函数,释放pbuf

 } else

#endif /*LWIP_SUPPORT_CUSTOM_PBUF */

 {

        /* is this a pbuf from the pool? */

        if (type == PBUF_POOL) {   //不同类型调用不同的内存管理删除函数

          memp_free(MEMP_PBUF_POOL, p);

        /* is this a ROM or RAM referencingpbuf? */

        } else if (type == PBUF_ROM || type ==PBUF_REF) {

          memp_free(MEMP_PBUF, p);

        /* type == PBUF_RAM */

        } else {

          mem_free(p);

        }

      }

      count++;    //删除记录值加1

      /* proceed to next pbuf */

      p = q;      //处理下一个pbuf

    } else {

      LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE,("pbuf_free: %p has ref %"U16_F", ending here.\n", (void*)p, ref));

      /* stop walking through the chain */

      p = NULL;    //p设置为NULL,程序退出while

    }

  }

  PERF_STOP("pbuf_free");

  /* return number of de-allocated pbufs */

  return count;    //返回成功删除的pbuf个数

}

当可以删除某个pbuf结构时,函数pbuf_free首先检查这个 pbuf是属于四个类型中的哪种,根据类型的不同, 调用不同的内存释放函数进行删除。

5、其他数据包操作函数

  pbuf_realloc 函数在相应 pbuf(链表)尾部释放一定的空间,将数据包 pbuf 中的数据长度减少为某个长度值。对于 PBUF_RAM 类型的 pbuf,函数将调用内存堆管理中介绍到的 mem_realloc 函数,释放这些多余的空间;对于其他三种类型的 pbuf,该函数只是修改 pbuf 中的长度字段值,并不释放对应的内存池空间。

  pbuf_header 函数用于调整 pbuf 的 payload 指针(向前或向后移动一定的字节数),在前面也说到过了,在 pbuf 的数据区前可能会预留一些协议首部空间,而 pbuf 被创建时,payload 指针是指向数据区的,为了实现对这些预留空间的操作,可以调用函数 pbuf_header 使 payload 指针指向数据区前的首部字段,这就为各层对数据包首部的操作提供了方便。当然,进行这个操作的时候,len和 tot_len 字段值也会随之更新。

  pbuf_take 函数用于向 pbuf 的数据区域拷贝数据;pbuf_copy 函数用于将一个任何类型的 pbuf中的数据拷贝到一个 PBUF_RAM 类型的 pbuf 中。pbuf_chain 函数用于连接两个 pbuf(链表)为一个 pbuf 链表;pbuf_ref 函数用于将 pbuf 中的 ref 值加 1。

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

推荐阅读更多精彩内容