('LWIP之SOCKET的实现http://bluefish.blog.51cto.com/214870/158413Lwip协议栈的实现目的,无非是要上层用来实现app的socket编程。好,我们就从socket开始。为了兼容性,lwip的socket应该也是提供标准的socket接口函数,恩,没错,在src\\include\\lwip\\socket.h文件中可以看到下面的宏定义:#ifLWIP_COMPAT_SOCKETS#defineaccept(a,b,c)lwip_accept(a,b,c)#definebind(a,b,c)lwip_bind(a,b,c)#defineshutdown(a,b)lwip_shutdown(a,b)#defineclosesocket(s)lwip_close(s)#defineconnect(a,b,c)lwip_connect(a,b,c)#definegetsockname(a,b,c)lwip_getsockname(a,b,c)#definegetpeername(a,b,c)lwip_getpeername(a,b,c)#definesetsockopt(a,b,c,d,e)lwip_setsockopt(a,b,c,d,e)#definegetsockopt(a,b,c,d,e)lwip_getsockopt(a,b,c,d,e)#definelisten(a,b)lwip_listen(a,b)#definerecv(a,b,c,d)lwip_recv(a,b,c,d)#definerecvfrom(a,b,c,d,e,f)lwip_recvfrom(a,b,c,d,e,f)#definesend(a,b,c,d)lwip_send(a,b,c,d)#definesendto(a,b,c,d,e,f)lwip_sendto(a,b,c,d,e,f)#definesocket(a,b,c)lwip_socket(a,b,c)#defineselect(a,b,c,d,e)lwip_select(a,b,c,d,e)#defineioctlsocket(a,b,c)lwip_ioctl(a,b,c)#ifLWIP_POSIX_SOCKETS_IO_NAMES#defineread(a,b,c)lwip_read(a,b,c)#definewrite(a,b,c)lwip_write(a,b,c)#defineclose(s)lwip_close(s)先不说实际的实现函数,光看这些定义的宏,就是标准socket所必须有的接口。接着看这些实际的函数实现。这些函数实现在src\\api\\socket.c中。先看下接受连接的函数,这个是tcp的原型:intlwip_accept(ints,structsockaddraddr,socklen_taddrlen)可以看到这里的socket类型参数s,实际上是个int型在这个函数中的第一个函数调用是sock=get_socket(s);这里的sock变量类型是lwip_socket,定义如下:/Containsallinternalpointersandstatesusedforasocketstructlwip_socket{/socketscurrentlyarebuiltonnetconns,eachsockethasonenetconnstructnetconnconn;/datathatwasleftfromthepreviousreadstructnetbuflastdata;/offsetinthedatathatwasleftfromthepreviousreadu16_tlastoffset;/numberoftimesdatawasreceived,setbyevent_callback(),testedbythereceiveandselectfunctionsu16_trcvevent;/numberoftimesdatawasreceived,setbyevent_callback(),testedbyselectu16_tsendevent;/socketflags(currently,onlyusedforO_NONBLOCK)u16_tflags;/lasterrorthatoccurredonthissocketinterr;};好,这个结构先不管它,接着看下get_socket函数的实现【也是在src\\api\\socket.c文件中】,在这里我们看到这样一条语句sock=&sockets[s];很明显,返回值也是这个sock,它是根据传进来的序列号在sockets数组中找到对应的元素并返回该元素的地址。好了,那么这个sockets数组是在哪里被赋值了这些元素的呢?进行到这里似乎应该从标准的socket编程的开始,也就是socket函数讲起,那我们就顺便看一下。它对应的实际实现是下面这个函数Intlwip_socket(intdomain,inttype,intprotocol)【src\\api\\socket.c】这个函数根据不同的协议类型,也就是函数中的type参数,创建了一个netconn结构体的指针,接着就是用这个指针作为参数调用了alloc_socket函数,下面具体看下这个函数的实现staticintalloc_socket(structnetconnnewconn){inti;/Protectsocketarraysys_sem_wait(socksem);/allocateanewsocketidentifierfor(i=0;ifunction(&(apimsg->msg));UNLOCK_TCPIP_CORE();returnERR_OK;}Callthelowerpartofanetconn_functionThisfunctionisthenrunninginthethreadcontextoftcpip_threadandhasexclusiveaccesstolwIPcorecode.err_ttcpip_apimsg(structapi_msgapimsg)【此为非locking的】{structtcpip_msgmsg;if(mbox!=SYS_MBOX_NULL){msg.type=TCPIP_MSG_API;msg.msg.apimsg=apimsg;sys_mbox_post(mbox,&msg);sys_arch_sem_wait(apimsg->msg.conn->op_completed,0);returnERR_OK;}returnERR_VAL;}其实,功能都是一样的,都是要对apimsg->function函数的调用。只是途径不一样而已。看看它们的功能说明就知道了。这么来说apimsg->function的调用很重要了。从netconn_new_with_proto_and_callback函数的实现,可以知道这个function就是do_newconnVoiddo_newconn(structapi_msg_msgmsg){if(msg->conn->pcb.tcp==NULL){pcb_new(msg);}/Else?This"new"connectionalreadyhasaPCBallocated./Isthisanerrorcondition?Shoulditbedeleted?/Wecurrentlyjustarehappyandreturn.TCPIP_APIMSG_ACK(msg);}还是看TCP的,在pcb_new函数中有如下代码:caseNETCONN_TCP:msg->conn->pcb.tcp=tcp_new();if(msg->conn->pcb.tcp==NULL){msg->conn->err=ERR_MEM;break;}setup_tcp(msg->conn);break;我们知道在这里建立了这个tcp的连接。至于这个超级牛的函数,以后再做介绍。嗯,还是回过头来接着看accept函数吧。Sock获得了,接着就是newconn=netconn_accept(sock->conn);通过mbox取得新的连接。粗略的估计了一下,这个新的连接应该和listen有关系。那就再次打断一下,看看那个listen操作。lwip_listen--ànetconn_listen_with_backlog--àdo_listen--àtcp_arg(msg->conn->pcb.tcp,msg->conn);tcp_accept(msg->conn->pcb.tcp,accept_function);//注册了一个接受函数AcceptcallbackfunctionforTCPnetconns.Allocatesanewnetconnandpoststhattoconn->acceptmbox.staticerr_taccept_function(voidarg,structtcp_pcbnewpcb,err_terr){structnetconnnewconn;structnetconnconn;conn=(structnetconn)arg;/Wehavetosetthecallbackhereeventhoughthenewsocketisunknown.conn->socketismarkedas-1.newconn=netconn_alloc(conn->type,conn->callback);if(newconn==NULL){returnERR_MEM;}newconn->pcb.tcp=newpcb;setup_tcp(newconn);newconn->err=err;/RegistereventwithcallbackAPI_EVENT(conn,NETCONN_EVT_RCVPLUS,0);if(sys_mbox_trypost(conn->acceptmbox,newconn)!=ERR_OK){/Whenreturning!=ERR_OK,theconnectionisabortedintcp_process(),sodonothinghere!newconn->pcb.tcp=NULL;netconn_free(newconn);returnERR_MEM;}returnERR_OK;}对了,accept函数中从mbox中获取的连接就是这里放进去的。再回到accept中来,取得了新的连接,接下来就是分配sock了,再然后,再然后?再然后就等用户来使用接收、发送数据了。到此整个APP层,也就是传输层以上对socket的封装讲完了。在最后再总结一些整个路径的调用情况吧LWIP之API_MSG结构及其实现http://bluefish.blog.51cto.com/214870/158414从上面一篇的socket实现来看,如果要评起到最关键作用的一个结构体,那么structapi_msg当之无愧。先看下它的定义:/Thisstructcontainsafunctiontoexecuteinanotherthreadcontextandastructapi_msg_msgthatservesasanargumentforthisfunction.Thisispassedtotcpip_apimsgtoexecutefunctionsintcpip_threadcontext.structapi_msg{/functiontoexecuteintcpip_threadcontextvoid(function)(structapi_msg_msgmsg);/argumentsforthisfunctionstructapi_msg_msgmsg;};功能说的很清楚。但是具体怎么个操作法还是不知道,没关系,接着看它的调用。举一个例子,刚好是上一篇中调用,但是没有看具体实现的err_tnetconn_getaddr(structnetconnconn,structip_addraddr,u16_tport,u8_tlocal){structapi_msgmsg;msg.function=do_getaddr;msg.msg.conn=conn;msg.msg.msg.ad.ipaddr=addr;msg.msg.msg.ad.port=port;msg.msg.msg.ad.local=local;TCPIP_APIMSG(&msg);returnconn->err;}说明一下,api_msg结构几乎都是在netconn_xxx函数中被调用,方式千篇一律,除了msg.funcion的赋值不一样外。上面的调用很简单,对该结构体变量赋值,接着就是调用TCPIP_APIMSG,这个函数上面讲过,可过去看下。既然如此,就不得不说mbox及其相关函数了。staticsys_mbox_tmbox=SYS_MBOX_NULL;【tcp.c】再看sys_mbox_t的定义,在【src\\include\\lwip\\sys.h】中/Foratotallyminimalandstandalonesystem,weprovidenulldefinitionsofthesys_functions.typedefu8_tsys_sem_t;typedefu8_tsys_mbox_t;typedefu8_tsys_prot_t;可以看到这里只是简单的定义成了u8类型,注意上面的红色字体的说明,很明显这个是可移植的一部分,需要根据不同的平台,不同的操作系统具体定义。可以借鉴焦海波大侠的关于ucos上对lwip的移植笔记来看。我们可以看到在api_msg结构的处理过程中,所有的信息都是包含在api_msg_msg结构体中的,api_msg只是将其和function简单的组合了。下面看下这个牛结构的定义:/Thisstructincludeseverythingthatisnecessarytoexecuteafunctionforanetconninanotherthreadcontext(mainlyusedtoprocessnetconnsinthetcpip_threadcontexttobethreadsafe).structapi_msg_msg{/Thenetconnwhichtoprocess-alwaysneeded:itincludesthesemaphorewhichisusedtoblocktheapplicationthreaduntilthefunctionfinished.structnetconnconn;/Dependingontheexecutedfunction,oneoftheseunionmembersisusedunion{/usedfordo_sendstructnetbufb;/usedfordo_newconnstruct{u8_tproto;}n;/usedfordo_bindanddo_connectstruct{structip_addripaddr;u16_tport;}bc;/usedfordo_getaddrstruct{structip_addripaddr;u16_tport;u8_tlocal;}ad;/usedfordo_writestruct{constvoiddataptr;intlen;u8_tapiflags;}w;/usedofrdo_recvstruct{u16_tlen;}r;#ifLWIP_IGMP/usedfordo_join_leave_groupstruct{structip_addrmultiaddr;structip_addrinterface;enumnetconn_igmpjoin_or_leave;}jl;#endif/LWIP_IGMP#ifTCP_LISTEN_BACKLOGstruct{u8_tbacklog;}lb;#endif/TCP_LISTEN_BACKLOG}msg;};一个很合理的设计,至少笔者是这么认为的。关键在于msgunion的设计。LWIP之TCP层发送相关http://bluefish.blog.51cto.com/214870/158415现在我们正式开始进入对TCP的研究,它属于传输层协议,它为应用程序提供了可靠的字节流服务。在LWIP中基本的TCP处理过程被分割为六个功能函数的实现:tcp_input(),tcp_process(),tcp_receive()【与TCP输入有关】,tcp_write(),tcp_enqueue(),tcp_output()【用于TCP输出】。这些是从大的方面来划分的。现在先从小部tcp.c文件来分析一下:我们知道这里的函数都是被socket那一层的最终调用的。为了利于分析,我选择lwip_send函数来分析,具体不多说,最终调用到了staticerr_tdo_writemore(structnetconnconn)这个函数,当然这期间也做了不少工作,最主要的就是把发送数据的指针放到了msg的指定变量中msg.msg.msg.w.dataptr=dataptr;//指针msg.msg.msg.w.len=size;//长度这些又经过转化放到了netconn的write_msg中最后就是对do_writemore的调用了,下面详细分析这个函数。这个函数的最直接调用有以下几个:available=tcp_sndbuf(conn->pcb.tcp);err=tcp_write(conn->pcb.tcp,dataptr,len,conn->write_msg->msg.w.apiflags);err=tcp_output_nagle(conn->pcb.tcp);err=tcp_output(conn->pcb.tcp);好,先看tcp_sndbuf这个。从其参数,我们应该想像pcb的重要性。#definetcp_sndbuf(pcb)((pcb)->snd_buf)//由下面分析得这里直接返回buf大小那就继续跟踪这个pcb好了。是的,这还得从lwip_socket开始,在创建连接netconn的时候调用了do_newconn,它接着调用了pcb_new,在这个函数中如果是tcp的话有以下代码msg->conn->pcb.tcp=tcp_new();哈哈,还记得吧,在前面我讨论到了这里,就没有再讨论了。嗯,现在开始吧。CreatesanewTCPprotocolcontrolblockbutdoesn\'tplaceitonanyoftheTCPPCBlists.Thepcbisnotputonanylistuntilbindingusingtcp_bind().structtcp_pcbtcp_new(void){returntcp_alloc(TCP_PRIO_NORMAL);}也许注释的意义更大一些吧,哈哈,至此,我们从注释可以知道,tcp_bind的作用是把tcp的pcb放入list中,至少是某一个list,既然都提到了,似乎不说说有点过意不去啊。Lwid_bind函数最终会调用到tcp_bind【当然是tcp为例进行分析】。这个函数也比较有意思,在进入正题之前先来了下面这么个调用if(port==0){port=tcp_new_port();}意思很明显,就是分配个新的端口号,有意思的就是这个端口号的分配函数tcp_new_portstaticu16_ttcp_new_port(void){structtcp_pcbpcb;#ifndefTCP_LOCAL_PORT_RANGE_START#defineTCP_LOCAL_PORT_RANGE_START4096#defineTCP_LOCAL_PORT_RANGE_END0x7fff#endifstaticu16_tport=TCP_LOCAL_PORT_RANGE_START;again:if(++port>TCP_LOCAL_PORT_RANGE_END){port=TCP_LOCAL_PORT_RANGE_START;}for(pcb=tcp_active_pcbs;pcb!=NULL;pcb=pcb->next){if(pcb->local_port==port){gotoagain;}}for(pcb=tcp_tw_pcbs;pcb!=NULL;pcb=pcb->next){if(pcb->local_port==port){gotoagain;}}for(pcb=(structtcp_pcb)tcp_listen_pcbs.pcbs;pcb!=NULL;pcb=pcb->next){if(pcb->local_port==port){gotoagain;}}returnport;}分别检查了3个pcb链表,本来我不想把这个函数的实现列在这里的,但是这里告诉了我们一些东西,至少我们知道有3个pcb的链表,分别是tcp_active_pcbs【处于接受发送数据状态的pcbs】、tcp_tw_pcbs【处于时间等待状态的pcbs】、tcp_listen_pcbs【处于监听状态的pcbs】。似乎tcp_bind函数更有意思,它分别检查了4个pcbs链表,除了上面的三个还有一个tcp_bound_pcbs【处于已经绑定但还没有连接或者监听pcbs】。作用是是否有与目前pcb的ipaddr相同的pcb存在。不过这些都不是最重要的,TCP_REG(&tcp_bound_pcbs,pcb);才是我们的目的,果然如此,直接加入到了tcp_bound_pcbs链表中了。×××××××××××××××××××××××××××××××××××××××structtcp_pcbtcp_alloc(u8_tprio)//是的,你没看错,这个参数是学名是叫优先级{pcb=memp_malloc(MEMP_TCP_PCB);if(pcb!=NULL){memset(pcb,0,sizeof(structtcp_pcb));pcb->prio=TCP_PRIO_NORMAL;pcb->snd_buf=TCP_SND_BUF;//对的,别的可以先不管,这就是我们要找的东西pcb->snd_queuelen=0;pcb->rcv_wnd=TCP_WND;pcb->rcv_ann_wnd=TCP_WND;pcb->tos=0;pcb->ttl=TCP_TTL;/ThesendMSSisupdatedwhenanMSSoptionisreceived.pcb->mss=(TCP_MSS>536)?536:TCP_MSS;pcb->rto=3000/TCP_SLOW_INTERVAL;pcb->sa=0;pcb->sv=3000/TCP_SLOW_INTERVAL;pcb->rtime=-1;pcb->cwnd=1;iss=tcp_next_iss();pcb->snd_wl2=iss;pcb->snd_nxt=iss;pcb->snd_max=iss;pcb->lastack=iss;pcb->snd_lbb=iss;pcb->tmr=tcp_ticks;pcb->polltmr=0;#ifLWIP_CALLBACK_APIpcb->recv=tcp_recv_null;#endif/LWIP_CALLBACK_API/InitKEEPALIVEtimerpcb->keep_idle=TCP_KEEPIDLE_DEFAULT;#ifLWIP_TCP_KEEPALIVEpcb->keep_intvl=TCP_KEEPINTVL_DEFAULT;pcb->keep_cnt=TCP_KEEPCNT_DEFAULT;#endif/LWIP_TCP_KEEPALIVEpcb->keep_cnt_sent=0;}returnpcb;}就是一个tcp_pcb的结构体初始化过程,使用默认值填充该结构好了,下面接着走,该轮到tcp_write了吧Writedataforsending(butdoesnotsenditimmediately).Itwaitsintheexpectationofmoredatabeingsentsoon(asitcansendthemmoreefficientlybycombiningthemtogether).Topromptthesystemtosenddatanow,calltcp_output()aftercallingtcp_write().【src\\core\\tcp_out.c】err_ttcp_write(structtcp_pcbpcb,constvoiddata,u16_tlen,u8_tapiflags){/connectionisinvalidstatefordatatransmission?if(pcb->state==ESTABLISHEDpcb->state==CLOSE_WAITpcb->state==SYN_SENTpcb->state==SYN_RCVD){if(len>0){returntcp_enqueue(pcb,(void)data,len,0,apiflags,NULL,0);}returnERR_OK;}else{returnERR_CONN;}}这个函数确实够简单的了,检查pcb状态,直接入队列。嗯,还是觉得看注释比看函数实现过瘾。下面的这个tcp_enqueue才是个大头函数,lwip协议栈的设计与实现文档中是这么介绍的:应用层调用tcp_write()函数以实现发送数据,接着tcp_write()函数再将控制权交给tcp_enqueue(),这个函数会在必要时将数据分割为适当大小的TCP段,然后再把这些TCP段放到所属连接的传输队列中【pcb->unsent】。函数的注释是这样写的:EnqueueeitherdataorTCPoptions(butnotboth)fortranmissionCalledbytcp_connect(),tcp_listen_input(),tcp_send_ctrl()andtcp_write().咱们接着上面往下看,tcp_output_nagle这个函数实际上是个宏,通过预判断以决定是否调用tcp_output。也就是说最后的执行是tcp_output来实现的。这个函数或许比tcp_enqueue还要恐怖,从体积上来看。这里也只做一个简单介绍,具体请参阅tcp_out.c文档。该函数的注释是这么说的:Findoutwhatwecansendandsendit。文档是这么解释的:tcp_output函数会检查现在是不是能够发送数据,也就是判断接收器窗口是否拥有足够大的空间,阻塞窗口是否也足够大,如果条件满足,它先填充未被tcp_enqueue函数填充的tcp报头字段,接着就使用ip_route或者ip_output_if函数发送数据。LWIP之TCP层接收相关既然定了这么个标题,当然是要从socket的recv来讲了。这里主要涉及到lwip_recvfrom这个函数。它的大致过程是,先通过netconn_recv(sock->conn);从netconn的recvmbox中收取数据,在这里有个do_recv的调用,而do_recv又调用了tcp_recved,关于这个函数的注释如下:Thisfunctionshouldbecalledbytheapplicationwhenithasprocessedthedata.Thepurposeistoadvertisealargerwindowwhenthedatahasbeenprocessed.知道了,这里的do_recv只是起到一个知会的作用,可能的话会对接收条件做一些调整。回到主题,lwip_recvfrom从netconn接收完数据就是要copythecontentsofthereceivedbufferintothesuppliedmemorypointermem,这一步是通过netbuf_copy_partial函数来完成的。接着往下走,我们发现,数据的来源是在netconn的recvmbox,刚才提到过的。好了,那么是在什么地方,什么时候这个recvmbox被填充的呢?在工程中搜索recvmbox,发现在recv_tcp函数有这样一句:sys_mbox_trypost(conn->recvmbox,p)ok,就是它了。看看函数的原型:ReceivecallbackfunctionforTCPnetconns.Poststhepackettoconn->recvmbox,butdoesn\'tdeleteitonerrors.staticerr_trecv_tcp(voidarg,structtcp_pcbpcb,structpbufp,err_terr)好的,p是个输入参数,并且这是个静态函数,是作为tcp的接收回调用的。嗯接着看recv_tcp的caller:Setupatcp_pcbwiththecorrectcallbackfunctionpointersandtheirarguments.staticvoidsetup_tcp(structnetconnconn){structtcp_pcbpcb;pcb=conn->pcb.tcp;tcp_arg(pcb,conn);tcp_recv(pcb,recv_tcp);tcp_sent(pcb,sent_tcp);tcp_poll(pcb,poll_tcp,4);tcp_err(pcb,err_tcp);}哈哈,可谓是一网打尽啊。对于这个函数中的几个调用,我们看一个就好了,别的实现也差不多,就是个赋值的过程Voidtcp_recv(structtcp_pcbpcb,err_t(recv)(voidarg,structtcp_pcbtpcb,structpbufp,err_terr)){pcb->recv=recv;}setup_tcp上面也有讲过,是在newconn的时候被调用的,创建完pcb,就是调用的这个函数,以配置连接的各个回调函数。然而到这里似乎走了一个死胡同里了,貌似没有什么地方对pcb->recv有调用的,而唯一有的就是接收TCP事件TCP_EVENT_RECV的宏定义中。同时,其他的几个函数也是类似的情况,例如send_tcp函数。真是“山穷水尽疑无路,柳暗花明又一村”啊。原来上面的也不只是死胡同。这里就要从tcp的三大接收处理函数说起了。最底层的(在tcp层)就是tcp_input,它是直接被ip层调用的,该函数的定义注释是这么写的:TheinitialinputprocessingofTCP.ItverifiestheTCPheader,demultiplexesthesegmentbetweenthePCBsandpassesitontotcp_process(),whichimplementstheTCPfinitestatemachine.ThisfunctioniscalledbytheIPlayer(inip_input()).Tcp_input又调用了tcp_process函数做进一步的处理,它的定义注释如下:ImplementstheTCPstatemachine.Calledbytcp_input.Insomestatestcp_receive()iscalledtoreceivedata.Thetcp_segargumentwillbefreedbythecaller(tcp_input())unlesstherecv_datapointerinthepcbisset.是的,下面是tcp_receive函数,被tcp_process调用Calledbytcp_process.ChecksifthegivensegmentisanACKforoutstandingdata,andifsofreesthememoryofthebuffereddata.Next,isplacesthesegmentonanyofthereceivequeues(pcb->recvedorpcb->ooseq).Ifthesegmentisbuffered,thepbufisreferencedbypbuf_refsothatitwillnotbefreeduntiliithasbeenremovedfromthebuffer.然而光有这些调用顺序是不行的,最重要的是下面两个变量【都在tcp_in.c中】staticu8_trecv_flags;staticstructpbufrecv_data;在tcp_receive中有以下主要几句if(inseg.p->tot_len>0){recv_data=inseg.p;}if(cseg->p->tot_len>0){/Chainthispbufontothepbufthatwewillpasstotheapplication.if(recv_data){pbuf_cat(recv_data,cseg->p);}else{recv_data=cseg->p;}cseg->p=NULL;}下面的这个是tcp_input中的,是在tcp_process处理完之后的if(recv_data!=NULL){if(flags&TCP_PSH){recv_data->flags=PBUF_FLAG_PUSH;}/Notifyapplicationthatdatahasbeenreceived.TCP_EVENT_RECV(pcb,recv_data,ERR_OK,err);}看最后一条语句就好了,很熟悉是吧,对了,就是上面传说中的死胡同,到此也解开了。LWIP之IP层实现这一部分的实现都是在ip.c文件中【src\\cor\\ipv4】,可以看到在这个文件中主要实现了3个函数,ip_input;ip_route;ip_output以及ip_output_if。下面分别来介绍它们。这些函数可以分成两大类:接收和发送。下面就先从发送开始,首先要说的就是ip_output函数,这个也是发送过程中最重要的一个,它是被tcp层调用的,详细可参见以上章节。Simpleinterfacetoip_output_if.Itfindstheoutgoingnetworkinterfaceandcallsuponip_output_iftodotheactualwork.err_tip_output(structpbufp,structip_addrsrc,structip_addrdest,u8_tttl,u8_ttos,u8_tproto){structnetifnetif;if((netif=ip_route(dest))==NULL){returnERR_RTE;}returnip_output_if(p,src,dest,ttl,tos,proto,netif);}可以看到该函数的实现就像注释所说的一样,直接调用了ip_route和ip_outputif两个函数。根据以往的经验,先看下netif这个结构的实现情况:GenericdatastructureusedforalllwIPnetworkinterfaces.Thefollowingfieldsshouldbefilledinbytheinitializationfunctionforthedevicedriver:hwaddr_len,hwaddr[],mtu,flags//这几个是要用驱动层填写的structnetif{/pointertonextinlinkedliststructnetifnext;/IPaddressconfigurationinnetworkbyteorderstructip_addrip_addr;structip_addrnetmask;structip_addrgw;/ThisfunctioniscalledbythenetworkdevicedrivertopassapacketuptheTCP/IPstack.err_t(input)(structpbufp,structnetifinp);/ThisfunctioniscalledbytheIPmodulewhenitwantstosendapacketontheinterface.Thisfunctiontypicallyfirstresolvesthehardwareaddress,thensendsthepacket.err_t(output)(structnetifnetif,structpbufp,structip_addripaddr);/ThisfunctioniscalledbytheARPmodulewhenitwantstosendapacketontheinterface.Thisfunctionoutputsthepbufas-isonthelinkmedium.err_t(linkoutput)(structnetifnetif,structpbufp);#ifLWIP_NETIF_STATUS_CALLBACK/Thisfunctioniscalledwhenthenetifstateissettoupordownvoid(status_callback)(structnetifnetif);#endif/LWIP_NETIF_STATUS_CALLBACK#ifLWIP_NETIF_LINK_CALLBACK/Thisfunctioniscalledwhenthenetiflinkissettoupordownvoid(link_callback)(structnetifnetif);#endif/LWIP_NETIF_LINK_CALLBACK/Thisfieldcanbesetbythedevicedriverandcouldpointtostateinformationforthedevice.voidstate;#ifLWIP_DHCP/theDHCPclientstateinformationforthisnetifstructdhcpdhcp;#endif/LWIP_DHCP#ifLWIP_AUTOIP/theAutoIPclientstateinformationforthisnetifstructautoipautoip;#endif#ifLWIP_NETIF_HOSTNAME/thehostnameforthisnetif,NULLisavalidvaluecharhostname;#endif/LWIP_NETIF_HOSTNAME/numberofbytesusedinhwaddru8_thwaddr_len;/linklevelhardwareaddressofthisinterfaceu8_thwaddr[NETIF_MAX_HWADDR_LEN];/maximumtransferunit(inbytes)u16_tmtu;/flags(seeNETIF_FLAG_above)u8_tflags;/descriptiveabbreviationcharname[2];/numberofthisinterfaceu8_tnum;#ifLWIP_SNMP/linktype(from"snmp_ifType"enumfromsnmp.h)u8_tlink_type;/(estimate)linkspeedu32_tlink_speed;/timestampatlastchangemade(up/down)u32_tts;/countersu32_tifinoctets;u32_tifinucastpkts;u32_tifinnucastpkts;u32_tifindiscards;u32_tifoutoctets;u32_tifoutucastpkts;u32_tifoutnucastpkts;u32_tifoutdiscards;#endif/LWIP_SNMP#ifLWIP_IGMP/ThisfunctioncouldbecalledtoaddordeleteaentryinthemulticastfiltertableoftheethernetMAC./err_t(igmp_mac_filter)(structnetifnetif,structip_addrgroup,u8_taction);#endif/LWIP_IGMP#ifLWIP_NETIF_HWADDRHINTu8_taddr_hint;#endif/LWIP_NETIF_HWADDRHINT};该结构体实现在【src\\include\\lwip\\netif.h】,注意到该结构体成员中有3个函数指针变量。好了,这个结构体先做一大体了解。用到的时候再详细讲。接下来先看下ip_route函数的实现:FindstheappropriatenetworkinterfaceforagivenIPaddress.Itsearchesthelistofnetworkinterfaceslinearly.AmatchisfoundifthemaskedIPaddressofthenetworkinterfaceequalsthemaskedIPaddressgiventothefunction.structnetifip_route(structip_addrdest){structnetifnetif;/iteratethroughnetifsfor(netif=netif_list;netif!=NULL;netif=netif->next){/networkmaskmatches?if(netif_is_up(netif)){if(ip_addr_netcmp(dest,&(netif->ip_addr),&(netif->netmask))){/returnnetifonwhichtoforwardIPpacketreturnnetif;}}}if((netif_default==NULL)(!netif_is_up(netif_default))){snmp_inc_ipoutnoroutes();returnNULL;}/nomatchingnetiffound,usedefaultnetifreturnnetif_default;}可以说这个函数的实现很简单,且作用也很容易看懂,就像其注释所说的一样。不过在这个函数中我们还是发现了一些什么,对了,就是structnetifnetif_list;【src\\core\\netif.c】的使用。既然这里都使用了这个网络接口链表,那它是在哪里被初始化的呢?好了,首先我们发现在netif_add函数中有对netif_list的调用,netif_add是被do_netifapi_netif_add函数调用的,而do_netifapi_netif_add是在netifapi_netif_add中通过netifapi_msg被TCPIP_NETIFAPI调用的。问题似乎很清楚,只要找到nnetifapi_netif_add是被谁调用的就好了,然而,搜遍整个工程也没有发现这个函数的影子,除了一个声明一个实现外。Mygod,又进入死胡同了?好吧,这里先标识一下,待解【见下面的解释】我们接着看ip_output_if这个函数,具体函数可参考【src\\core\\ipv4\\ip.c】。它的函数定义注释如下:SendsanIPpacketonanetworkinterface.ThisfunctionconstructstheIPheaderandcalculatestheIPheaderchecksum.IfthesourceIPaddressisNULL,theIPaddressoftheoutgoingnetworkinterfaceisfilledinassourceaddress.IfthedestinationIPaddressisIP_HDRINCL,pisassumedtoalreadyincludeanIPheaderandp->payloadpointstoitinsteadofthedata.再看最后一句:returnnetif->output(netif,p,dest);嗯,看来这个netif还是关键啊,如果估计不错的话,接收的时候也要用到这个结构的。那就看它在什么地方被赋值的吧。又经过一番搜索,看来在目前的代码中是找不到的了。查看lwip协议栈的设计与实现,特别是网络接口层的那一节,终于明白了,原来这些是要有设备驱动来参与的:【一下为引用】当收到一个信息包时,设备驱动程序调用input指针指向的函数。网络接口通过output指针连接到设备驱动。这个指针指向设备驱动中一个向物理网络发送信息包的函数,当信息包包被发送时由IP层调用,这个字段由设备驱动的初始设置函数填充。嗯,那就这样吧,到这里我们可以说IP层的发送流程已经走完了。接下来就是ip层的接收过程了。刚才上面也有提到驱动设备收到包,丢给netif的input函数,这个input函数也是设备驱动层来设置的。无非有两个可能,一个是ip_input,另外一个就是tcpip_input。因为tcpip_input函数的处理是最终调用到了ip_input【在tcpip_thread中】。按照正常情况下应该是ip_input函数的,我们先来看下这个函数。ThisfunctioniscalledbythenetworkinterfacedevicedriverwhenanIPpacketisreceived.ThefunctiondoesthebasicchecksoftheIPheadersuchaspacketsizebeingatleastlargerthantheheadersizeetc.Ifthepacketwasnotdestinedforus,thepacketisforwarded(usingip_forward).TheIPchecksumisalwayschecked.原型:err_tip_input(structpbufp,structnetifinp)该函数大致的处理过程是:处理ip包头;找到对应的netif;检查如果是广播或多播包,则丢掉;如果是tcp协议的话就直接调用了tcp_input函数处理数据。到此,ip层的东西大致就说完了。最后,由于tcp和ip层的东西都说完了,所以此时我们顺便看下,tcpip的整体实现,这个主要是在src\\api\\tcpip.c文件中实现。我们知道发送过程是由socket直接调用的,所以这个文件中不涉及,说白了,这个文件主要是涉及到整个接收过程。这里实现的函数有tcpip_input,和tcpip_thread以及tcpip_init函数。Tcpip_init函数很简单就是创建系统线程(sys_thread_new)tcpip_thread。Tcpip_thread函数的注释如下:ThemainlwIPthread.ThisthreadhasexclusiveaccesstolwIPcorefunctions(unlessaccesstothemisnotlocked).Otherthreadscommunicatewiththisthreadusingmessageboxes.它的整个过程就是一直从mbox中取出msg,对各种msg的一个处理过程。Tcpip_input函数,是在tcpip_thread中被调用的处理设备驱动接收到的信息包,并调用ip_input来进一步处理。整个启动过程:main--->vlwIPInit()voidvlwIPInit(void){/InitializelwIPanditsinterfacelayer.sys_init();mem_init();memp_init();pbuf_init();netif_init();ip_init();sys_set_state((signedportCHAR)"lwIP",lwipTCP_STACK_SIZE);tcpip_init(NULL,NULL);sys_set_default_state();}从上面我们知道,tcpip_init创建tcpip_thread在tcpip_thread的开始有如下代码:(void)arg;ip_init();#ifLWIP_UDPudp_init();#endif#ifLWIP_TCPtcp_init();#endif#ifIP_REASSEMBLYsys_timeout(1000,ip_timer,NULL);#endifif(tcpip_init_done!=NULL){tcpip_init_done(tcpip_init_done_arg);}下面是tcp_init的实现Voidtcp_init(void){/Clearglobals.tcp_listen_pcbs.listen_pcbs=NULL;tcp_active_pcbs=NULL;tcp_tw_pcbs=NULL;tcp_tmp_pcb=NULL;/initializetimertcp_ticks=0;tcp_timer=0;}LWIP的底层结构(物理层)我们前面讲到说是ip层的发送和接收都是直接调用了底层,也就是设备驱动层的函数实现,在这里暂且称之为物理层吧。下面就接着ip层的讲,不过由于这里的设备驱动各平台的都不一样,为此,我们选择ARM9_STR91X_IAR这个Demo作为实例,该平台的网络设备驱动在\\library\\source\\91x_enet.c文件中。而ethernetif.c文件就是我们需要的,它是连接设备驱动程序与ip层的桥梁。Ethernetif.c文件中提供的函数主要有以下这么几个:(1)low_level_init(2)low_level_input(3)low_level_output(4)ethernetif_init(5)ethernetif_input(6)ethernetif_output这里对外的接口只有ethernetif_init函数,它是main函数中通过netif_add(&EMAC_if,&xIpAddr,&xNetMast,&xGateway,NULL,ethernetif_init,tcpip_input);来被调用的。我们可以清楚的看到,tcpip_input的使用,它就是被用来当有数据接收的时候被调用的以使接收到的数据进入tcpip协议栈。在netif_add函数中,我们可以看到netif->input=input;if(init(netif)!=ERR_OK){returnNULL;}Ok,从这里就进入到ethernetif_init函数了,在这个函数中,我们主要看以下几句:netif->output=ethernetif_output;netif->linkoutput=low_level_output;low_level_init(netif);etharp_init();可以看到,netif->output和netif->linkoutput被赋值了,这个很重要的,等会再说。好,再接着看low_level_init函数s_pxNetIf=netif;//对全局变量s_pxNetIf赋初值ENET_InitClocksGPIO();ENET_Init();ENET_Start();//这3句是对网络设备的寄存等的配置xTaskCreate(ethernetif_input,(signedportCHAR)"ETH_INT",netifINTERFACE_TASK_STACK_SIZE,NULL,netifINTERFACE_TASK_PRIORITY,NULL);以ethernet_input创建task,这个函数也很有意思,首先可以看到的是一个无限循环,在循环体中有以下调用:p=low_level_input(s_pxNetIf);s_pxNetIf->input(p,s_pxNetIf);//tcpip_input虽然有了这两句,还不是很清楚,可以确定的是后一句是把接收到的数据送入tcpip协议栈处理,为此,我们想到上一句是从硬件读出数据。看下具体的low_level_input函数实现:len=ENET_HandleRxPkt(s_rxBuff);这个函数很好理解,主要的是上面的那一句。/FunctionName:ENET_HandleRxPktDescription:receiveapacketandcopyittomemorypointedbyppkt.Input:ppkt:pointeronapplicationreceivebuffer.Output:NoneReturn:ENET_NOK-Ifthereisnopacket:ENET_OK-Ifthereisapacket/u32ENET_HandleRxPkt(voidppkt){ENET_DMADSCRBasepDescr;u16size;staticintiNextRx=0;if(dmaRxDscrBase[iNextRx].dmaPackStatus&DMA_DSCR_RX_STATUS_VALID_MSK){return0;}pDescr=&dmaRxDscrBase[iNextRx];/Getthesizeofthepacket/size=((pDescr->dmaPackStatus&0x7ff)-4);//MEMCOPY_L2S_BY4((u8)ppkt,RxBuff,size);/optimizedmemcopyfunction/memcpy(ppkt,RxBuff[iNextRx],size);//string.hlibrary//GivethebufferbacktoENETpDescr->dmaPackStatus=DMA_DSCR_RX_STATUS_VALID_MSK;iNextRx++;if(iNextRx>=ENET_NUM_RX_BUFFERS){iNextRx=0;}/Returnnoerrorreturnsize;}这个函数也很好理解,是从DMA中直接拷贝数据到指定的pBuf。至此,input过程完事了,从代码调用流程上看真是千回百转,一会low_level,一会ethernetif。不过总体来说是系统通过一个task(ethernetif_input)轮询检查DMA控制器的状态以判断是否有数据接收到。下面再研究下output过程。Output过程是由应用程序以主动方式触发的,经过前面几篇的介绍,我们知道发送的数据后来被传递给了ip_output_if函数。我们就接着这个函数看,它直接调用了netif->output函数,刚才我们看到在ethernetif_init中有对这个变量【函数指针】赋值,它就是ethernetif_output。它倒是简单,直接returnetharp_output();而它有在最后调用了etharp_send_ip,在这个函数的最后调用了returnnetif->linkoutput(netif,p);好了终于找到根了这里的linkoutput函数也就是low_level_output,果然有如下调用:memcpy(&TxBuff[l],(u8_t)q->payload,q->len);还记得ENET_Init嘛,它的函数实现中有如下两句调用:ENET_TxDscrInit();ENET_RxDscrInit();它们就是初始化DMA发送和接收的地址的,也就是上面所说的TxBuff和RxBuff。OK,大功告成,自上而下的LWIP大体流程都说到了。如有以后碰到什么需要注意的细节之类,再补上吧。LWIP之SOCKET编程前几天看了关于LWIP协议栈的实现和FREERTOS的基本原理。今天开始调试LWIP的socket通信,是基于freertos系统的ARM9_STR91X_IAR开发板。这是个现成的实例,由于lwip已经由freertos移植好了,而调试的目的就是实现在lwip上的socket通信。本来以为很容易的问题,结果还是搞了一天,当然和EWARM工具的难用也还是有很大关系的。首先要注意的一点是sys_thread_new和xTaskCreate的区别。我们知道xTaskCreate是用来创建一个task任务的。类似于windows下的process。/Startsanewthreadwithpriority"prio"thatwillbeginitsexecutioninthefunction"thread()".The"arg"argumentwillbepassedasanargumenttothethread()function.Theidofthenewthreadisreturned.Boththeidandthepriorityaresystemdependent.sys_thread_tsys_thread_new(void(thread)(voidarg),voidarg,intprio){xTaskHandleCreatedTask;intresult;result=xTaskCreate(thread,(signedportCHAR)s_sys_arch_state.cTaskName,s_sys_arch_state.nStackDepth,arg,prio,&CreatedTask);//Foreachtaskcreated,storethetaskhandle(pid)inthetimersarray.//Thisschemedoesn\'tallowforthreadstobedeletedtimeoutlist[nextthread++].pid=CreatedTask;if(result==pdPASS){++s_sys_arch_state.nTaskCount;returnCreatedTask;}else{returnNULL;}}很显然,sys_thread_new最终对task的创建也是通过xTaskCreate来实现的。但是请注意不同点,前者会把新创建的task的pid放入到一个timeoutlist的链表中。起初我也没有主要这个小问题,直到用socket系列函数的select的时候,一直过不去,而是abort中断了,具体原因请看select函数实现,其中sys_arch_timeouts的调用就是获取当前task的timeout,如果没有用sys_thread_new创建的话,这里就没有我用来调用select的当前task,所以就有了以后的abort中断了。下面给一个socket编程的实现staticvoidvLWIPSendTask(voidpvParameters){intlistenfd;intremotefd;intlen;structsockaddr_inlocal_addr,remote_addr;fd_setreadset;fd_setwriteset;structtimevaltimeout;timeout.tv_sec=1;timeout.tv_usec=0;//structlwip_socketsock;listenfd=socket(AF_INET,SOCK_STREAM,0);local_addr.sin_family=AF_INET;local_addr.sin_port=htons(80);local_addr.sin_len=sizeof(local_addr);local_addr.sin_addr.s_addr=INADDR_ANY;if(bind(listenfd,(structsockaddr)&local_addr,sizeof(local_addr))<0){return;}if(listen(listenfd,1)==-1){return;}len=sizeof(remote_addr);while(1){//这里注意一下,lwip的阻塞不是在listen函数,而是acceptremotefd=accept(listenfd,(structsockaddr)&remote_addr,&len);//close(listenfd);//listenfd=-1;//getpeername(remotefd,(structsockaddr)&remote_addr,&len);if(remotefd!=-1){intret;send(remotefd,"starttowork!\\r\\n",16,0);for(;;){FD_ZERO(&readset);FD_ZERO(&writeset);FD_SET(remotefd,&readset);FD_SET(remotefd,&writeset);ret=lwip_select(remotefd+1,&readset,&writeset,0,&timeout);if(ret>0){if(FD_ISSET(remotefd,&readset)){memset(buf,0,50);if(recv(remotefd,buf,50,0)<=0){close(remotefd);remotefd=-1;break;}else{inti=strlen(buf);send(remotefd,buf,i,0);}}/elseif(FD_ISSET(remotefd,&writeset)){send(remotefd,"thisistimetosend!\\r\\n",25,0);}}elseif(ret<0){close(remotefd);remotefd=-1;break;}}}}vTaskDelete(NULL);}对的,这是作为server端实现的,可以和通用的socket客户端配。经过测试至少可以与windows上的socket程序匹配(接收,发送数据)。一些可以借鉴一下的实现方式(1)线程操作以前总是在线程中用无限循环,例如while(1)等,这样在程序最后要退出线程时总是用terminatethread。然而我们完全可以手工退出线程,只要修改成while(bactive),在创建线程的时候设置bactive=true,要退出的话只要设置bactive=false即可。这是个全局变量,它的存在有一定的风险。(2)waitefor一个事件,可以自己定义#defineWAITFOR(ucCondition,uTimeoutInMs,bOK)\\{\\UINT32uTimeRemain=(uTimeoutInMs);\\while(uTimeRemain>0)\\{\\if(ucCondition)break;\\pf_msleep(10);\\uTimeRemain-=10;\\}\\if(uTimeRemain>0)\\bOK=TRUE;\\elsebOK=FALSE;\\}(3)状态函数指针数组用来循环调用一系列函数,并由状态来控制是否退出循环调用或者下一个要调用的是哪一个状态函数。状态可以作为数组的下标,来进行调用相应的状态函数。所有的状态函数都应该有相同的形参表。要有一个end状态及状态数组以退出循环调用。在每个状态函数中可以指定下一个状态,也就是下一个要调用的状态函数。(4)环形buffer(5)封装底层的差异此时如果底层有两套不同的代码,可在一个统一的.c文件中通过不同的宏定义来#include不同的代码(.c)文件。同时要有一个统一的.h文件,来根据不同的宏定义来#include不同的头文件。(6)包含不同的.lib文件如果有多套不同的.lib文件,可以在一个统一的.c/.cpp文件(该文件应该在工程中)中通过不同的宏定义来包含不同的.lib文件。#pragmacomment(lib,".lib文件所在的路径")lwIPAPI的封装方法#首先回顾一下TCP的rawAPI:1)structtcp_pcbtcp_new(void)新建一个tcppcb2)err_ttcp_bind(structtcp_pcbpcb,structip_addripaddr,u16_tport);绑定端口到此tcp连接3)structtcp_pcbtcp_listen(structtcp_pcbpcb);进入侦听状态,如果连接进来,则会回调tcp_accept设置的回调函数4)err_ttcp_connect(structtcp_pcbpcb,structip_addripaddr,u16_tport,err_t(connected)(voidarg,structtcp_pcbtpcb,err_terr));连接远端机子,SYN会发送出去5)err_ttcp_write(structtcp_pcbpcb,voiddataptr,u16_tlen,u8_tcopy)voidtcp_sent(structtcp_pcbpcb,err_t(sent)(voidarg,structtcp_pcbtpcb,u16_tlen))6)voidtcp_recv(structtcp_pcbpcb,err_t(recv)(voidarg,structtcp_pcbtpcb,structpbufp,err_terr))voidtcp_recved(structtcp_pcbpcb,u16_tlen);tcp_recv设置回调,在回调函数里面调用tcp_recved;7)voidtcp_poll(structtcp_pcbpcb,u8_tinterval,err_t(poll)(voidarg,structtcp_pcbtpcb));err_ttcp_close(structtcp_pcbpcb);8)voidtcp_err(structtcp_pcbpcb,void(err)(voidarg,err_terr));#补充:TCP的数据传送netif建立起基本底层的网络接口netif_init->lowlevel_init->建立起线程->lowlevel_input->ip_input->tcp_input->tcp_process(这个很重要,3次,4次握手都在这里)->tcp_recevicetcp_write->tcp_enqueue->tcp_output->ip_output_if->netif[output](^_^,init的时候设置了回调)->low_level_output->#api_msg.capi_msg_input在tcp_thread里面的main_loopapi_msg_post被api_lib.c各个对应函数调用#socket调用(在lwIP里面netconn)socket->netconn_newbind->netconn_bindlisten->netconn_listenaccept->netconn_accpetrecv->netconn_recvsend->netconn_sendclose->netconn_closesocket编程中的connet()使用connect()函数/sockfd套接字文件描述符,由socket()函数返回的serv_addr是一个存储远程计算机的IP地址和端口信息的结构addrlen应该是sizeof(structsockaddr)return如果发生了错误(比如无法连接到远程主机,或是远程主机的指定端口无法进行连接等)它将会返回错误值-1全局变量errno将会存储错误代码#include#includeintconnect(intsockfd,structsockaddrserv_addr,intaddrlen);示例:#include#include#include#defineDEST_IP“166.111.69.52”#defineDEST_PORT23main(){intsockfd;/将用来存储远程信息structsockaddr_indest_addr;/注意在你自己的程序中进行错误检查!!sockfd=socket(AF_INET,SOCK_STREAM,0);/主机字节顺序dest_addr.sin_family=AF_INET;/网络字节顺序,短整型dest_addr.sin_port=htons(DEST_PORT(;dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);/将剩下的结构中的空间置0bzero(&(dest_addr.sin_zero),8);/不要忘记在你的代码中对connect()进行错误检查!!connect(sockfd,(structsockaddr)&dest_addr,sizeof(structsockaddr));注意我们没有调用bind()函数。基本上,我们并不在乎我们本地用什么端口来通讯,是不是?我们在乎的是我们连到哪台主机上的哪个端口上。Linux内核自动为我们选择了一个没有被使用的本地端口。最常用的BSDAPI函数:socket:创建一个插口(socket)bind:将本地端口号和IP地址绑定到插口上listen:TCP监听accept:TCP监听接受处理connect:TCP客户端连接select:特殊插口设置send/sendto:发送数据包到已连接/未连接插口上recv/recvfrom:接收数据包从已连接/未连接插口上getsockopt/setsockopt:获取/改变插口选项getpeername/getsockname:获取远端/本地地址信息close:关闭插口shutdown:按设置关闭插口gethostbyname/gethostbyaddr:地址域名映射read:从插口缓存读数据write:想插口缓存写数据--------------------------------------------------------------------#include#includeintsocket(intdomain,inttype,intprotocol);创建通讯用的“插口”(插口socket可以理解为IP地址和端口号组合成的地址),创建成功返回插口ID(出错返回-1)。参数:domain协议族(AF_UNIX是UNIX,AF_INET是IPv4协议,AF_ROUTE是路由器协议);type类型(SOCK_STREAM是TCP,SOCK_DGRAM是UDP,SOCK_RAW是RAM活IPv4);protocol为0。该函数返回大于等于0的整数作为插口ID,如果出错返回-1#include#includeintbind(intsockFd,conststructsockaddrsockAddr,intaddrLen);将插口名、本地端口号和本地IP地址绑定到指定插口上。一般在用作服务器时使用该函数。返回0成功,-1未成功。参数:sockFd插口ID,由socket函数创建;sockAddr结构体包含插口地址信息,AF_UNIX用下面结构体structsockaddr{unsignedshortsa_family;//AddressFamily(domain)charsa_data[14]];//ProtocolAddress};AF_INET用下面的结构体,使用前需初始化,下面使用TCP函数时相同。structsockaddr_in{shortsin_family;//AddressFamilyunsignedshortsin_port;//PortNumberstructin_addrsin_addr;//InternetAddressunsignedcharsin_zero[8];//Padstructure};addrLen是上述结构体长度。--------------------------------------------------------------------#includeintlisten(intsockFd,intbacklog);TCP服务器监听指定插口参数:sockFd已创建并被绑定的插口;backlog允许接收的客服端数量。--------------------------------------------------------------------include#includeintaccept(intsockFd,structsockaddrclientAddr,intaddrLen)TCP服务器监听到连接时的响应函数。参数:sockFd已创建、绑定并监听的插口;clientAddr远端连接信息;addrLen结构体长度。--------------------------------------------------------------------#include#includeintconnect(intsockFd,structsockaddrservAddr,intaddrLen);TCP/UDP客服端申请TCP/UDP服务器的链接。参数:sockFd已创建的插口;servAddr服务器连接信息;addrLen结构体长度。返回0成功,-1出错--------------------------------------------------------------------#include#include#includeintselect(intn,fd_setread_fds,fd_setwrite_fds,fd_setexceptfds,structtimevaltimeout);挂起当前线程,等待特定事件发生或定时器过期。本函数可以指定4类特定事件:read、write、exception和超时。返回插口ID表示事件有响应,0表示超时,-1表示出错。参数:n应该大于所有插口ID,用FD_SETSIZE代替;后面三个fd_set结构体存储三种插口事件位图:typedefstructfd_set{fd_maskfds_bits[(FD_SETSIZE+NFDBITS-1)/NFDBITS];}fd_set;用以下四个宏修改:FD_SET(fd,fdset)fd插口ID,fdset是fd_set结构体地址,设置插口事件为真FD_CLR(fd,fdset)设置插口事件为假FD_ISSET(fd,fdset)获取插口状态,是否设置FD_ZERO(fdset)清除所有设置第四个参数timeval结构体如下:structtimeval{inttv_sec;/秒inttv_usec;/毫秒};用来设置超时时间。--------------------------------------------------------------------#include#includeintsend(intsockFd,constvoidmsg,intmsgLen,unsignedintflags);intsendto(intsockFd,constvoidmsg,intmsgLen,unsignedintflags,conststructsockaddrto,inttoLen);这两个函数都用来按插口发送数据包,send用在已经连接的插口,sendto用在没有连接上的插口。send函数的参数:sockFD插口ID,msg要发送的数据指针,msgLen要发送的数据长度,flags发送选项(按位)sendto函数的参数:UDP专用,插口必须是SOCK_DGRAM类型。由于没有连接,所以sendto函数增加了两个与连接有关的参数。to定义目标地址的结构体,toLen是结构体长度。sockaddr结构体如下:structsockaddr{u_shortsa_family;charsa_data[14];};这两个函数返回值均为实际发送字节的长度(软件需要调整偏移量将数据全部发送),-1表示发送不成功。--------------------------------------------------------------------#include#includeintrecv(intsockFd,constvoidmsg,intmsgLen,unsignedintflags);intrecvfrom(intsockFd,constvoidmsg,intmsgLen,unsignedintflags,conststructsockaddrfrom,intfromLen);这两个函数均是按插口来接收数据包,recv函数用在已连接插口上,recvfrom用在未连接插口上。recv函数参数:sockFd插口ID,msg接收缓存地址,msgLen接收缓存最大空间,flags接收选项。recvfrom函数参数:UDP专用,插口必须是SOCK_DRAM类型。由于没有连接,所以recvfrom函数增加了两个与连接有关的参数。from定义目标地址的结构体,formLen是结构体长度。两个函数均返回接收到的数据数,-1接收错误,0表示目标地址已经传输完毕并关闭连接。--------------------------------------------------------------------#include#includeintsetsockopt(intsd,intlevel,intoptname,constvoidoptval,socklen_toptlen);intgetsockopt(intsd,intlevel,intoptname,voidoptval,socklen_toptlen);setsockopt函数用来改变插口的模式,这种改变是通过修改插口选项实现的。getsockopt函数用来获取插口选项的值。参数:sd插口ID;level协议栈选项,包括SOL_SOCKET(插口层)IPPROTO_TCP(TCP层)和IPPROTO_IP(IP层);optname需要修改的选项名;optval修改值地址;optlen修改值长度。返回0表示成功。--------------------------------------------------------------------#includeintgetsockname(intsd,structsockaddraddr,intaddrLen);intgetpeername(intsd,structsockaddraddr,intaddrLen);getsockname函数用于从已连接的插口中获取本地地址信息。getpeername函数用于获取远端地址信息。参数:sd插口ID;addr地址信息结构体;addrLen结构体长度。返回0成功,-1错误--------------------------------------------------------------------#includeintclose(intsd);关闭插口通信(丢弃未发送的数据包并拒绝接受数据)--------------------------------------------------------------------#includeintshutdown(intsockFd,inthow);该函数提供了更大的权限控制插口的关闭过程。参数:sockFd插口ID;how仅能为0、1和2这三个值0表示停止接收当前数据并拒绝以后的数据接收1表示停驶发送数据并丢弃未发送的数据2是0和1的合集--------------------------------------------------------------------intread(intsockFD,voidbuffer,UInt32numBytes);从指定插口中等待数据接收并存放到buffer中。该函数会挂起线程,直到有数据接收到。参数:sockFd插口ID;buffer缓存地址;numBytes缓冲大小该函数返回接收到的数据大小,-1表示出错,0表示远端已经关闭连接。--------------------------------------------------------------------intwrite(intsockFD,voidbuffer,UInt32numBytes);将缓存中数据写到指定插口准备发送。参数:sockFd插口ID;buffer缓存地址;numBytes缓存中数据大小该函数返回实际发送的数据量,-1表示出错。--------------------------------------------------------------------补充:lwIP协议栈在socket模式下也就是操作系统中运行,创建进程的方式与操作系统中创建进程的方式有所不同。要用专用函数:sys_thread_tsys_thread_new(charname,void(thread)(voidarg),voidarg,intstacksize,intprio)参数:name线程说明;thread线程函数;arg线程函数的参数;stacksize线程堆栈大小;prio线程优先级在lwIP下创建线程统一使用此函数,当然这个函数也是要调用系统创建线程的API的。非标准Socket接口,lwip提供了一套SocketAPI,这套API的标准与正常操作系统下的SocketAPI的形式不是很一致,我们先前已经在这套API上实现了WebServer,已测试在没有MobileIP环境下工作正常。下面我们就一个lwip典型的UDP协议工作过程作为对lwip的简单介绍。UDP发送过程:1.应用层:绑定UDP套接字我们必须先创建一个UDP套接字,通过调用udp_new()进行申请,然后调用udp_bind()绑定在UDP端口上,在这个调用过程中,我们必须编写一个用于处理这个UDP套接字接收到的数据报文的函数,并把这个函数作为udp_bind()的参数,以后当套接字接收到数据报文时会自动调用这个函数,我们将在后面介绍这个函数怎么调用的。绑定结束之后,必须调用udp_connect()将数据报文的目的地址绑定在UDP的数据结构中,最后就是调用udp_send()把数据报文发送出去。2.传输层的处理做好应用层的处理之后,数据报文被提交到UDP层,udp_send()函数中首先给数据报文加入UDP头部,然后调用ip_route()选择一个合适的网络接口进行发送,最后调用ip_output()把数据报文传入IP层。3.IP层的处理ip_route()函数比较各个网络接口的IP地址是否与目的IP地址在同一子网中,如果有,就把它当成发送的网络接口返回,如果没有就返回一个默认的网络接口。在ip_output()函数中,先给数据报文加上IP头部,然后比较目的IP地址与网络接口的IP地址是否在同一网段,如果不是,就必须先把数据报文发送到网关,于是使用网关的IP地址作为目的主机,如果目的IP地址与网络接口的IP地址在同一网段,则把目的IP地址作为目的主机。接着调用arp_lookup()在ARP缓存中查找目的主机的MAC地址,找到了调用ethernet_output()把数据报文传入到数据链路层发送,如果找不到,就调用arp_query()发送ARP请求解析目的主机的MAC地址。4.ARP协议的处理arp_lookup()实现在本地ARP缓存中查找目的主机的MAC地址,找到了返回该MAC地址,找不到返回NULL。arp_query()函数中构造一个ARP请求报文,然后调用ethernet_output()把该报文送到数据链路层发送。5.数据链路层的处理数据链路层的处理就是给数据报文添上相对的以太网头部,然后调用lowlever_output()直接把报文传送出去。UDP接收过程:接收过程与发送过程刚好相反,数据报文首先调用ethernet_input()函数到达数据链路层,去掉以太网头部之后如果是ARP报文传给调用arp_input()交给ARP协议处理,如果是IP报文就调用ip_input()进入IP层处理,ip_input()函数中比较数据报文的目的IP地址,如果与某个网络接口的IP地址相同,则接收这个报文,依照IP头部的协议字段,调用各自协议的输入处理函数,本例中将调用udp_input(),在udp_input()中提取数据报文的端口号,然后在已登记的套接字中查找与该端口号符合的UDP接收函数,如果没有找到相应的套接字,调用icmp_output()发送一个ICMP不可达报文,如果找到了,就调用该函数(这个函数就是我们在udp_bind()时传入的其中一个参数)。',)