实例介绍
非常详细的LwIP协议栈源码详解。嵌入式轻型tcpip协议栈
目录 1移植综述 2动态内存管理 6 3数据包pbuf---- 4pbuf释放--- 5网络接口结构-- 6以太网数据接收 一m20 7ARP表------ ,m23 8ARP表查询-- -----26 9ARP层流程 28 0IP层输入一----- 31 11IP分片重装1-- --34 2IP分片重装2--- Eaa- ---37 13ICMP处理 40 14TCP趸立与断开- m-143 15TCP状态转换 46 16TCP控制块 -------419 17'CP建立流程- 53 8TCP状态机 --56 19TCP输入输出:数1 60 20TCP输入输出涵数2 21TCP滑动窗口 56 22TP超时与重传 23TCP慢启动与拥塞避兔-- 24TCP快速恢复重传和 Nagle算法-- ---76 5TP坚持与保活定时器一 26TCP定时器-------- --84 27TCP终结与小结一---- 88 28API实现及相关数据结构 -91 29API消息机制一 …94 30API数及编程实例--- 一世mm E-mail:torrest(@foxmail.com 老衲五木出品 1移植综述 如果你认为所谓的毅力是每分每秒的“艰苦忍耐”式的奋斗,那这是一种很不足的心理 状态。毅力是一冲习惯,力是一种状态,毅力是一种生活。看了这么久的代码觉得是不是 该写点东西了,不然怎么对得起某人口中所说的科研人兵这个光荣称号。初见这如山如海的 代码,着实看出了一身冷汗。现在想想其实也不是那么难,那么多革命先辈经过N长时间 才搞出来的东东怎么可能让你个毛小子几周之内搞懂。我见到的只是冰川的一小角,万里长 征的一小步,九头牛身上的一小毛.再用某人的话说,写吧,昏写,瞎写,胡写,乱写,写 写就懂了。 我想我很适合当一个歌颂者,青春在风中劂着。你知道,就算大雨让这座城市颠倒,我 会给你怀抱;受不了,看见你背影来到,写下我度秒如年难捱的离骚;就算整个世界被寂寞 绑票,我也不会夼跑;遝不了,最后谁也都苍老,写下我,时间和声交错的城堡。我正在 听的歌。扯远了 正题,嵌入式产品连入 Internet网,这个MS是个愈演愈烈的趋势。想想,你可以足不 出户对你的产品进行配置,并获取你关心的数据信息,多好。这也许也是物联网廿界最基本 的雏形。当然,你的产品要有如此功能,那可不谷易,至少它得个目前很 Fashion的TCPP 协议栈。LWIP是一套用于嵌入式系统的开放源代码TCP/P协议栈。在你的嵌入式处理器 不是很NB,内部Fash和Ram不是很强大的情下,用它还是很合适滴 LwP的设计者为像我这样的懒惰者提供了详绀的移植说明文裆,当然这还不够,他们 还尽可能的包了大部分工作,懒人们灵要做很少的工作就功蕠圆满了纵观整个移植过程 使用者需要完成以下几个方面的东西 首先是LWP协议内部使用的数据类型的定义,如u8_,s8t,ul6t,u32_t等等等等。 巾于所移植平台处理器的不同和使用的编译器的不同,这些教振类型必须重新定义。想想, 一个int型数据在64位处理器上其长度为8个字节,在32位处理器上为4个字节,而在6 位处理器上就只有两个字节了因此这部分需要使用者根据处理器位数和和使用的编译器的 特点来编写。所以在ARM7处理器上使用的 typedef unsigned int ul32t移植语句用在64位处 埋器的移植过程中那肯定是行不道的了。 其次是实现与信号量和邮箱操作相关的函数,比如建立、删除、等待、释放等。如果 在裸机上直接跑LWIP,这点实现起來比较麻烦,使用者必须自己士建立一套信号量和邮箱 相关的杌制。一般情况下,在使用LwIP旳嵌入式系统中都会有操作系统的支持,而在操作 系统中信号量和邮箱往往是最基本的进程通信机制了。UC/OST应该算是最简单的嵌入式操 作系统了吧,它也无例外的能够提供信号量和邮箱机制,只要我们将 UC/OSIl中的相关函 薮做相应的封装,就可满足LWTP的需求。LWIP使用邮箱和信号量来实现上层应用与协议 栈间、下层硬件驱动与协议栈间的信息交互。LWP协议模拟了TCPP协议的分层思想, 表面上看LWP也是有分层思想的,但从实现二看,LWP只在一个进程内实现了各个层次 的所有工作。具体如下:IWP完成相关初始化后,会阻塞在一个邮箱上,等待接收数据进 行处理。这个邮箱内的数据可能来自底层硬件驱动接收到的数捱包,也可能来自应用程序。 当在该邮箱内取得数据后,LWIP会对数据进行解析,然后冉依次调用协议栈内部上层相关 处理函数处理数据。处珒结束后,LWP继续阻塞在邮箱上等待下一批数据。当然LWTP还 有一大串的内存管理杌制用以避免在各层间交互数据时大量的时间和内存开销,这将在后续 讲解中慢慢道来。当然,但这样的设计使得代码理解难度加人,这一点让人头人。信号量也 E-mail:torrest(@foxmail.com 老衲五木出品 可以用在应用程序与协议栈的互相通信中。比如,应用程序要发送数据了:它宄把数据发到 LwIP阻塞的邮箱二,然后饣拌起在一个信号量上;LWP从邮箱上取得数据处理后,释放 一个信号量,告诉应用程序,你要发的数据我已经搞定了:此后,应用程序得到信号量继续 运行,而LWP继续阻塞在邮箱上等待下一批处理数据。 其其次,就是与等待超时相关的函数:上面说到LwTP协议栈会阻基在邮箱上等待接收 数据的到來。这种等待在夕部看起来是一直进行的,但其实不然。一般在初始化LWP进程 的时候,都会同时约初始化一些超时事件,即当莫些事件等超时后,它们会自动调用一些 超时处理函数做相关处理,以满足TCP/P协议栈的需求。这样看来,当LWIP协议栈阻塞 等待邮箱之前,它会精明的计算到底应该等待多久,如果LWP进程中没有初始化任何超时 事件,那好,这种情况最简单了,永远的挂起程腻可以了,这时的等待就可以看做是天长 地久的….有点暧昧了。如果LwIP进程中有初始化的超时事件,这时就不能一直等了,因 为这样超时事件没有任何被执行的机会。LWIP是这样做的,等待邮箱的时间设置为第一个 超时事件的时间长度,如果刑间到了,还没等到数据,那好,直接跳出邮箱等待转而执行超 时事件,当执行完成超吋事件后,再按照上述的方法继续阻寨邮箱,可以看出,对一个LWIP 进程,需要用一个链表来管理这些超时事件。这个链表的大部分工作已经被LWTP的设计者 完成了,使用者只需要实现的仅有一个函数:该函数能够返回当前进程个超时事件链表的首 地址。LWP内部协议要利用该首地址来查找完成相关超吋事件。 其其其次,如果LWP是建立在多线程操作系统之上的话,则要实现创建一个新线程的 函数。不攴持多线程的操作系统,汗.表示还没听过。不过 UCoS显然是支持多线程的, 地球人都知道。这样一个典型的LWIP应用系统包括这样的三个进程:首先启动的是上层应 用程序进程,然后是IWI协议栈进程,最后是底层硬件数据包接收发送进程。通常IWTP 协议栈进程是在应用程序中调用LWP协议栈初始化函数来创建的。注意LWP协议栈进程 般具有最高的优先级,以便实时正确的对数据进行响应。 其其其其次,其他一些细节之处。比如临界区保扩函数,用于LWP协议栈处理某些临 界区时使用,一般通过进临界区关中断、出界区廾中断的方式来实现;又如结构体定义时 用到的结构体封装宏,LWIP的实现基于这样一科机制,即上层协议已经用确知道了下层所 传上米的数据的结构特点,上层直接使用相关取地址计算得到想要的数据,而避免了数据递 交时的复制与缓冲,所以定义结构体封装宏,絷止编译器的地址自动对齐是必须的:还有诸 如调试输出、测量记录方面的宏不做训解。 最后,也是比较重要的地方。底层网络驱动函数的实现。这取决于你嵌入式硬件系统 所使用的网终接口芯片,也就是网卡芯片,见的有RTL820BL、ENC28J60等等。不同的 接口芯片厂商都会提供丰富的驱动函数。我们只要将这些发送接收接口函数做相应的封装, 将接收釗得数据包封装为LWIP协议栈熟悉的数据结构、将发送的数据包分解为芯片熟悉的 数据结构就基本搞定了、最起码的,发送一个数据包函綬和接收一个数据包函数需要被实现。 那就这样了吧,虽然写得草草,但终于在撤退之前搞定ε好的丌始是成功的半,那 这暂上先算四分之一吧。不晓得一个月、两个月或者更多时间能写完否。预知后事如何,请 见下回分解。 E-mail:torrest(@foxmail.com 老衲五木出品 2动态内存管理 最近电力局很不给力啊,隔三差五的停电,害得我们老是痛苦的双扣斗地主,不带这样 的啊!令天还写吗?写,必须的。 昨天把LWP的移植工作杜架说了一下,网上也有一大筐的关于移植细节的文档。有兴 趣的童鞋不妨去找找。这里,我很想探究LWIP内部协议实现的细节,以及所有盘根错节的 问题的来龙去脉。以后的讨论研究将按照LWP英文说明文档《 Design and Implementation of he lwip:TCP/ IP Stack》的结构组织展开。 这里讨论LWIP的动态内存管理机制、 总的来说,LWIP的动态內存管理机制可以有三种:C运行时库自情的内存分配策略 动态内存堆(HAP分配策略和动态内存池(POOL)分配策略。 态内存堆分配策略和C运行时库白带的内存分配策略貝有很大的相似性,这是LWIP 模拟运行时库分配筼暤实现的。这两种策略使用者只能从屮选择一种,这通过头文件 Iwippools h中的宏定义 MEM LIBC MALLOC来实现的,当它被定义为1时则使用标准C 运行吋库自带的内存分配策略,而为0时则使用LWIP自身的动态内存堆分配策略。一般情 况下,我们选择使用LWP自身的动态内存堆分配策略,这里不对C运行时库自带的内存分 配策略进行讨论。 同时,动态内存堆分配绶咯可以有两种实现方式,纠结.…第一种就是如前所述的通过 开辟—个内存堆,然后通过模拟C运行吋厍的内分配策略来实现。第二种就是通过动态 内存池的方式来实现,也即动态内存堆分配函数通过简单请用动态内存池(POOL)分配函数 来完成其功(太敷衍了事了),在这种情况下,用户需要在头文件 Iwippools h中定义宏 MEM USE_ POOLS和 MEM USE_ CUSTOM POOLS为1,同时还要开辟一些额外的缓冲池 区,如下: LWIP MALLOC MEMPOOL START LWIP_ MALLOC MEMPOOL(20, 256) LWIP MALLOC MEMPOOL( 10, 512) LWIP MALLOC MEMPOOLi5, 1512) LWIP MALLOC MEMPOOL END 这几句摘自LwIP源码注释部分,表示为动态内存堆相关功能函数分配20个256字节 长度的内存块,10个512字节的内存块,5个1512字节的内存块。内存池管理会根据以上 的宏自动在内存中静态定义一个大片内存用于内存池。在内存分配申请的时候,自动根据所 请求的大小,选择最适合他长度的池里面去申请,如果启用宏 MEM USE POOLS TRY BIGGER POOL,那么,如果上述的最适合长度的池中没有空间 可以用了,分配器将从更大长度的池中去申请,不过这样会浪费更多的内存。晕乎乎就 这样了,这种方式一般不会板用到。哎,就最后这句话绘力。 下面讨论动态内存堆分配策略的第一种实现方式,这也是一般情況下被使用的方式。这 部分讨论主要参照网上Odom' s Blog,TA写得很好(但是也有一点小小的错误),所以一不 小心被我借用」 动态内存堆分配策略原坦就是在一个事先定义好大小的内存块中进行管理,其内存分配 的策略是采用最快合适( First fit)方式,只要找到一个比所请求的内存大的空闲块,就从 中切割出合适的块,并把剩余的部分返回到动态内存堆中。分配的内存块有个最小大小的限 E-mail:torrest(@foxmail.com 老衲五木出品 制,要求请求的分配大小不能小于 MIN SIZE,否则请求会被分配到 MIN SIZE大小的内存 罕。一般 MIN SIZE为12字节,在这12个字节中前几个字节会存放内存分配器管理用 的私有数据,该数据区不能被用户程序修改,否则导致致命问题。内存释放的过程是相反的 过程,但分配器会查看该节点前后相邻的内存块是否空闲,如果空闲则合并成一个大的内存 空闲块。采用这种分配策略,其优点就是为存浪费小,比较简单,适合用于小内存的管理, 其点就是如果频繁的动态分配和释放,可能会造成严重的内存碎片,如果在碎片情况严重 的话,可能会导致内存分配不成功。对于动态内存的使用,比较推荐的方法就是分配->释放 >分配-→释放,这种使用方法能够减少内存碎片。下面具体来看看IWP是怎么来实现这些 哟数的 mem init()内存堆的初始化函数,主要是告知内存堆的起止地址,以及初始化空闲列 表,由lwi袒始化时自己调用,该接口为内部私有接口,不对用户层开放。 men mallo(中请分配内存。将总共需要的字节数作为参数传递给该函数,返回值是 指向最新分配的内存的指针,而如果内存没有分配好,则返回值是NULL,分配的空间人小 会收到内存对齐的影响,可能会比申请的略大。返回的内存是“没有“初始化的。这块内存 可能包含任何随机的垃圾,你以马上用有效数据或者至少是用零来初始化这块内存。内存 的分配和释放,不能在中断函数里面进行。内存堆是全局变量,因此内存的申请、释放操作 做了线程安全保护,如果有多个线程在同时进行内存申请和释放,那么可能会因为信号量的 等待而导致申请耗时较长。 mem calloc()是对 mem malloc()图数的简单包裝,他有两个参数,分别为元素的数目 和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小,与 mem_ malloch不 同的是它会把动态分配的内存清零。有经验的程序员更喜欢使用 mem calloc(,因为这样 的话新分配内存的内容就不会有什么问题,调用 mem calloc(肯定会清0,并且可以避免 调用 mereto。 休息 动态内存池(P○OL分巸策略可以说是一个比较笨的分配策咯∫,但其分配策略实现简 单,内有的分配、释放效率高,可以有效防上内有碎片的产生。不过,也的缺点是会浪费部 分内存。 为什么叫POOL?这点很有趣,POOL有很多种,而这点依赖于用户配置LWIP的方式。 例如用户在头文件opth文什中定义 LWIT UDP为1,则在编译的时候与UDP类型内存池 就会被建立;定义LwIP_TCP为1,则在编泽的时候与TCP类型内存池就会被建立。另外, 还有很多其他类型的内存池,如专门存放网络包数据信息的 PBUF POOL、还有上面讲解动 仑内存堆分配策略时提到的 CUSTOM POOLS等等等等。某种类型的POOL其单个大小是 固定的,而分配该类POOL.的个数是可以用户配置的,用户应该根据协议栈实际使用状况 进行配置。把协议栈中所有的POOI.挨个放到一起,并把它们放在一连续的内存区域, 这呈现给用广的就是个大的缓冲池。所以,所谓的缓冲池的内部组织应亥是这样的:开始 处啟了A类型的POOL池a个,紧接着放上B类犁的POOL池b个,再接着放上C类型的 POOL池c个..直至最后N类型的POOL池n个。这一点很像UC/OSⅡ中进程控制块和事 件控制块,先廾辟堆各种类型的放那,你要用直接来取就是了。注意,这里的分配必须是 以单个缓冲池为基本单位的,在这样的情况下,可能导致内存浪费的情况。这是很明显约啊, 不解释。 卜面我来看看在LWIP实现中是怎么廾梓出上面所论述的大大的缓冲池的(的这个字 今大让我们群人笑了很久)。基本上绝大部分人看到这部分代码都会被打得晕头转向,完 全不哓得作者是在下啥,但是仔细理解后,你不得不佩服作者超凡脱俗的代码写能力,差 点用了沉鱼落雁这个词,罪过。上代码: E-mail:torrest(@foxmail.com 老衲五木出品 static u8 t memp memory MEM ALIGNMENT-1 #define LWIP MEMPOOL (name, num, size, desc)+((num)*(MEMP SIZE MEMP ALIGN SIZe (size))) #inc lude lwip/memp stdh 上面的代码定义了缓冲池所使用的内存缓冲区,很多人肯定会怀疑这到底是不是一个数组的 定义。定义一个数组,里面居然还有 define和 include关键字,解决问题的关键就在于头文 件 memp_std.h,它里血的东西可以被简化为诸多条 LWIP MEMPOOL(name,num, size. desc) 又由于用了 define关键字将 LWIP MEMPOOL(name,nm, size. desc)定义为 +(mum)“( MEMP SIZE+ MEMP ALIGN SIZE(sie),所以, memp_sld.h被编译后就为 条一条的+(),+().+(),+()…所以最终的数组 memp_ memory等价定义为 static u8_t memp_memory I MEM_ALIGNMENT-I () 如果此刻你还没懂,只能说明我的表述能力有问题了。当然还有个小小的遗留问题,为什么 数组要比实际需要的人MEM_ ALIGNMENT-1?作者考虑的是编译器的字对齐问题,到此 打住,这个问题不能深究啊,以后慢慢讲 复制上面的数组建立的方法,协议栈还建立了一些与缓冲池管理的全局变量: memp_ num:这个静态数组用于保有各种类型缓冲池的成员数目 memp sizes:这个静态数组用于保存各种类型缓冲池的结构大小 memp_tab:这个指针数组用于指向各种类犁缓冲池当前空闲节点 接下来就是理所当然的实现函数了 memp_init():内存池的初始化,主要是为每神内存池建立链表memp_tab,其链表是逆序 的,此外,如果有统计功能便能的话,也把记录了客和内有池的数目。 memp_malloc():如果相应的 memp _tab链表还有空闲的节点,则从中切出一个节点返回 合则返回空。 memp_tree():把释放的节点添加到相应的链表 memp_tab头上。 从上面的二个函数可以看出,动态内存池分配过程时相当的简洁直观啊。 HC:百度说是胡扯的意思。哈哈. E-mail:torrest(@foxmail.com 老衲五木出品 3数据包pbuf 高的地方,总是很冷。孤独,可以让人疯狂。没人能懂你!昨天讲过了LWP的内存分 配机制。再来总之一下,LWP中常用到的内存分配策珞有两种,一种是内存堆分配,一种 是内存氾分配。前者可以说能随心所欲的分配我们需要的合理大小的内存孰(又是‘的’) 缺点是当经过多次的分配释放后,内存堆中间会出瑪很多碎片,使得需要分配较大内存块时 分配失败:后者分配速度快,就是简单的链表操作,因为各种类型的POOL是我们事先建 立好的,但是采用POOL会有些情况下会浪费掉一定的内存空间。在LWP中,将这两种分 配策略混合使用,达到了很好的內存使用效率 下面我们将来看看LWIP屮是怎样合理利川这两种分配策略的。这就顾利的过渡到了这 节要讨论的话题:LwIP的数据包缓冲的实现。 在协议栈中移动的数据包,最无疑的是整个内存管理中最重要的部分了。数据包的种类 和大小也可以说是五花八门,数数,首先从网卡上来的原始数据包,它可以是长达上干个字 节的TCP数据包,也可以是仅有几个字节的ICMP数据包;再从要发送的数据包看,上层 应川可能将自己要发送的千奇百怄形态各异的数据包递交给LWIP协议栈发送,这些数据可 能存在于应用程序管埋的内存空间内,也可能仔在于某个ROM上。注意,这里有个核心的 东西是当数据在各层之间传递时,LWIP极力禁止数据的拷贝工作,因为这样会耗费大量的 时间和内存。综上,LWIP必须有个高效的数据包管理核心,它即能海纳百川似的兼容各种 类型的薮据,又能避免在各层之间的复制数据的巨大开锖。 数据包管理机构采用数据结构pbuf来描述数据包,其源码如下, struct pbuf struct pbuf *next void ekpay load u16t tot len: ul6 t len u8 L Lype; u8 t flags 6 t ref 这个看似简单的数据结构,却够我讲一大歇的了!next字段指针指向下一个pbuf结构,因 为实际发送或接收的数榍包可能很大,而每个pbuf能够管理的数据可能很少,所以,往往 需要多个 pbuf结构才能完仝描述一个数据包。所以,所有的描述同一个数据包的pbuf结构 E-mail:torrest(@foxmail.com 老衲五木出品 要链在个链表上,这点用next实现。 payload是数据指针,指向该pbut管理的数据的 起始地址,这甲,数椐的起始地址可以是紧跟在pbuf结构之后的RAM,也可能是在ROM 上的某个地址,而决定这点的是当前pbuf是什么类型的,即type字段的值,这在下面将继 续讨论。len字段表示当前pbu中的有效数据长度,而 Lol len表示当前pbu和其后所有pbur 的有效数据的长度。显然, tot len字段是len字段与pbuf链中随后一个pbuf的 tot len字段 的和;pbuf链中第一个pbuf的 I tot len字段表示整个数据包的长度,而最后一个pbuf的 tot len 字段必和len字段相等。type字段表小pbuf的类型,主要有叫冲类型,这点基本上涉及到 pbuf管理中最难的部分,将在下节仔细讨论。文栏上说fags字段也表示pbuf类型,不懂, yp字段不是说明了pu的类型吗?不过在源代码里,初始化一个pbu的时候,是将该字 段的值设为0,而在其他地方也没有用到该字段,所以,这里直接忽略掉。最后retf宇段表 示该pbut被引用的次数。这里叉是一个纠结的地方啊。初始化一个pbuf的时候,rtf字段值 被设置为1,当有其他pbuf的next指针指向该pbu时,该pbu的re字段值加一。所以, 要删除一个pbuf时,ref的值必须为1才能删除成功,否则删除失败。 pbuf的类型,令人很晕的东西。pbu有四类: PBUF RAM、 PBUF RON、 PBUF REF 和 PBUF POOL。下面,一个一个的来看看各种类犁的特点。 PBUF RAM类型的phu主要通过内存堆分配得到的。这种类型的phuf在协议栈中是 用得最多的。协议栈要发送的数据和应用程序要传递的数据一般都采用这个形式。申请 PBUF RAM关型时,协议栈公在内存堆中分配相应的大小,注意,这里的大小包括如前所 述的pbut结构头大小和相应数据缓冲区,他们是在一片连续的内存区的。下面米看看源代 码是怎样申请 PBUF RAM型的。其中p是pbuf犁指针。 p=(struct pbuf*)mem_malloc(LWIP_MEM_ALIGN- SIZE(SIZEOF_STRUCT PBUF offset) +lwiP MEM ALIGN SiZe(length)); 可以看出,系统是调用内存堆分配函数 mem malloc进行内存分配的。分配空间的大小包括: pbuf结构头大小 SIZEOF STRUCT PBUF,需要的数据存储空间大小 length,还有一个oet 关于这个ofet,也有一大堆可以讨论的东西,不过先到此打住。总之,分配成功的 PBUF RAM类型的pbu如下图 next payload len tot len flags ref E-mail:torrest(@foxmail.com 老衲五木出品 【实例截图】
【核心代码】
标签:
小贴士
感谢您为本站写下的评论,您的评论对其它用户来说具有重要的参考价值,所以请认真填写。
- 类似“顶”、“沙发”之类没有营养的文字,对勤劳贡献的楼主来说是令人沮丧的反馈信息。
- 相信您也不想看到一排文字/表情墙,所以请不要反馈意义不大的重复字符,也请尽量不要纯表情的回复。
- 提问之前请再仔细看一遍楼主的说明,或许是您遗漏了。
- 请勿到处挖坑绊人、招贴广告。既占空间让人厌烦,又没人会搭理,于人于己都无利。
关于好例子网
本站旨在为广大IT学习爱好者提供一个非营利性互相学习交流分享平台。本站所有资源都可以被免费获取学习研究。本站资源来自网友分享,对搜索内容的合法性不具有预见性、识别性、控制性,仅供学习研究,请务必在下载后24小时内给予删除,不得用于其他任何用途,否则后果自负。基于互联网的特殊性,平台无法对用户传输的作品、信息、内容的权属或合法性、安全性、合规性、真实性、科学性、完整权、有效性等进行实质审查;无论平台是否已进行审查,用户均应自行承担因其传输的作品、信息、内容而可能或已经产生的侵权或权属纠纷等法律责任。本站所有资源不代表本站的观点或立场,基于网友分享,根据中国法律《信息网络传播权保护条例》第二十二与二十三条之规定,若资源存在侵权或相关问题请联系本站客服人员,点此联系我们。关于更多版权及免责申明参见 版权及免责申明
网友评论
我要评论