Lwip之API接口与原理

一:API涉及的文件

Socket.c:该文件封装了底层接口,对上层提供了一个类似于伯克利形式的接口供应用层调用。当前的封装还是比较简单的,一些选项并没有实现,而只是提供了实现的框架。

Api_lib.c:该文件提供了本协议栈的应用接口,应用程序可以直接调用这些接口完成具体的功能,这些接口都是需要在应用任务环境中运行的。调用该文件中提供的接口的好处就是效率的提高,避免了数据的再次复制,但是不足之处就是需要对这些接口提供的功能比较熟悉,因为它们都是依赖于特定的实现的。

Api_msg.c:该文件中所提供的函数接口都是需要在tcpip任务环境中运行的,用来实际完成上层的请求操作。它们与应用层通过消息邮箱进行通信,所有的请求以及处理完成后的响应都封装在特别设计的消息结构体中,通过发送消息和取消息操作在应用任务和协议栈任务之间传递

Tcpip.c文件:协议栈的正常运行主要通过协议栈的核心任务来驱动,该文件中就是用来实现这一功能的,其中的tcpip_thread任务就是整个协议栈的总的处理任务,该任务的基本工作流程就是先从消息邮箱中读取消息,然后根据消息类型的不同进行具体的处理,周而复始,不断循环。该任务处理三类消息,第一类是和上层交互的api消息;第二类处理底层驱动的输入;第三类就是需要在tcpip任务环境中进行处理的消息,通过回调函数传进来。比如某些涉及定时器的操作就需要在tcpip任务环境中运行才能够真正实现定时的功能。

二:层次关系:

Lwip之API接口与原理_第1张图片

 

 

基本的流程简单描述如下:

顶层的应用创建所需的任务,并形成应用任务运行环境

协议栈初始化时创建了协议栈运行的核心任务tcpip_thread,并形成了协议栈运行环境

应用层的请求操作都是通过套接字封装层(socket)传到协议栈自己的接口层netconn进行处理的。Netconn将所有请求的操作封装在特别设计的消息结构体中,并将其发送给tcpip核心任务进行处理,此时所有的处理都是在协议栈运行环境中完成的。协议栈任务根据消息的类型,调用消息处理层的do_XXX函数进行具体的处理,此时应用环境处于等待状态。当协议栈任务将请求处理完成后同样以消息的形式将结果发送给应用任务环境,应用任务环境接着继续完成剩余的请求操作。

三:数据结构

Lwip之API接口与原理_第2张图片

 

Netconn数据结构保存了与连接相关的各种信息和系统资源。Type表明连接的类型,可选值为tcp, UDP或者raw。State域表明连接所处的状态,是正在连接还是收发数据等。Pcb是协议控制块,包含了有关当前连接的各种信息。消息邮箱和信号量用于在不同任务之间传送连接的信息。Socket域指明该连接在socket数组中所处的位置。Callback保存回调函数的地址,在回调函数中主要出来相关网络收发事件。

Lwip之API接口与原理_第3张图片

 

Netconn域保存该socket所对应的连接。Netbuf域保存了该连接上的相关数据。Lastoffset在对连接的buffer中的数据处理时使用,表示之前读后剩余数据的偏移值。Recvevent和sendevent表明是否有收发事件存在与该套接字,这两个变量对收发数据次数进行计数,由event_callback回调函数设置。Flags域保存了套接字选项标识,比如是否非阻塞等。

Lwip之API接口与原理_第4张图片

 

该数据结构在调用select进行相关处理时使用。Next指针指向下一个等待该select的任务。信号量sem用于唤醒等待该select的一个任务。

四:接口

Socket

根据创建连接的类型申请资源,创建net conn数据结构,注册回调函数。

在成功创建connect数据结构后调用alloc_socket申请socket套接字资源,并将netconn指针指向之前创建的连接。

如果申请不到套接字资源,比如已经使用完,则释放之前创建的netconn结构,返回出错。

否则,资源成功申请到,则将connect的socket域反向指向该套接字。

在申请netconn资源时,系统最终调用netconn_new_with_proto_and_callback函数创建所需的连接。在该函数中分配连接所需的各种系统资源,初始化netconn数据结构上层可以处理的相关数据域,之后申请API_MSG_NEWCONN消息,将当前连接交给协议栈核心任务。协议栈核心任务在处理上层消息时会取到该消息,然后执行do_newconn函数,申请创建连接相关的协议控制块pcb。当底层操作成功后将相关信息反馈回上层调用任务。上层调用任务在收到底层成功执行的反馈后返回。整个过程如下图:

Lwip之API接口与原理_第5张图片

 

Bind

以后补充

Listen

以后补充

Connect

首先调用get_socket从套接字数组中找到当前套接字对应的socket数据结构,如果失败则直接返回

之后检查参数,首先检查地址协议簇。如果地址协议簇不正确,则调用netconn_disconnect断开连接。其次,我们在这里检查远程服务器的ip地址和端口。如果这两者中有一个为零,则直接返回错误。如果这些检查都通过,则调用netconn_connect建立连接。如果该过程出错则返回错误码,成功则返回0,表示连接已经建立起来。

与远程主机的连接是通过调用netconn_connect完成的。在该函数中申请一个API_MSG_CONNECT消息,将当前的连接以及远程主机的地址和端口传给协议栈核心任务处理。协议栈核心任务在取到该消息后,调用do_connect函数执行创建连接所需的动作。在该函数中会根据连接类型的不同分别做处理,不过该函数首先会检查连接的协议控制块是否创建,如果没有会执行new_connect的相关操作,之后才执行connect。在执行connect的操作中,对于raw和UDP这些非面向连接的协议,只是将远程主机的地址和端口设置到其相应的pcb中,对应tcp,则除了设置地址和端口,注册各种回调函数,还要与远程主机通信,通过握手操作建立连接。 这部分的工作是由tcp层的tcp_connect函数来完成的,具体可参考tcp部分的相关内容。Tcp模块在完成连接后会调用注册的回调函数do_connected,表明连接过程已经完成。在该函数中会向上层调用任务反馈连接建立的情况,并通知调用任务可以继续进行其他操作。整个操作过程如下图所示:

Lwip之API接口与原理_第6张图片

 

Send

对于发送数据,上层可能调用的接口有lwip_send,lwip_sendto以及lwip_write。其中lwip_write的实现是直接调用lwip_send,所以实际上只有两个接口。Lwip_sendto相对于lwip_send而言,多了远程主机的ip地址和端口号相关的参数,处理中多了netconn_connect的调用,所以实际上是lwip_connect和lwip_send的混合体。由此看来,上层调用发送最终都是通过lwip_send接口完成的。

在lwip_send中,首先从套接字数组中找到套接字描述符对应的socket数据结构,根据其指向的连接的不通类型做不同的处理。对于raw或者UDP类型的连接,调用netbuf_new创建一个netbuf类型的结构体,并将其指向将要发送的数据,最后调用netconn_send完成数据发送。发送完成后调用netbuf_delete释放之前分配的netbuf结构体。对于tcp类型的连接,则调用netconn_write发送数据。最后,如果发送成功,则返回发送的数据字节数,否则,返回-1。

在下层,数据最终都是通过调用netconn_send以及netconn_write接口进行发送的。在netconn_send中,申请一个API_MSG_SEND类型的消息,将数据指针交给该消息,并将该消息发送到消息邮箱中。在netconn_write中,会申请一个API_MSG_WRITE类型的消息,由于tcp不同于UDP和raw类型的连接,tcp有流量控制,也就是说底层tcp模块有发送窗口,数据是先放到发送窗口,之后被tcp的发送模块处理发送出去,所以,在将消息发送到邮箱之前,要先检查tcp发送窗口是否已经被塞满。如果窗口已经被塞满,则需要等待。如果窗口可以发送一些数据,则先取一部分数据,交给消息邮箱。如果这部分数据发送成功了,则移动数据指针,否则仍然等待。整个发送过程是循环进行的,直到上层要发送的数据最终都被成功发送或者连接出错(比如连接被移除了)才退出。Tcp模块维持一个满速定时器,每500毫秒超时一次,在该定时器的处理中,tcp会定时poll等待在该连接上的其他任务,以便能够让它们再次检查连接的状态,这是通过信号量实现的。Netconn中保存的信号量变量就是起这个作用的。

在核心层,类似之前的处理,协议栈核心任务在处理上层发送来的消息时会取到各个消息进行处理。对于raw或者UDP类型的连接,将执行do_send函数中的操作。对应raw类型的连接,底层调用raw_send发送数据,对应UDP类型的连接,底层则调用UDP模块的udp_send发送。对于tcp类型的连接,此时执行的是do_write操作。在该函数中,首先调用tcp_write将数据放到tcp的发送对列上,并调用tcp_output检查数据是否可以发送,如果能够发送则马上发送。为了避免出现糊涂窗口综合症,这里的实现是在窗口小于链路可发送的最大数据包长度时仍然不发送数据,如果满足这一条件,则调用回调函数处理NETCONN_EVT_SENDMINUS事件,这会使得相关任务调用select时得到链路不可写的信息。整个发送的流程如下图所示:

Lwip之API接口与原理_第7张图片

 

Recv

数据的接收操作都是通过调用lwip_recvfrom接口来完成的。在该函数中,同样先根据套接字描述符获取到对应的socket结构体。之后检查上次操作后是否还有剩余数据存在于该连接上。如果有的话,则让局部netbuf变量指向这部分数据。否则,如果该调用是非阻塞调用,或者连接是非阻塞的并且连接上没有数据则返回错误。如果不满足上述条件,则调用netconn_recv尝试从连接上得到一些数据。如果返回空,则认为数据接收完了,返回0。

执行到这里,表明连接上有数据,并且局部变量netbuf已经指向了这些数据。数据可能是之前没有接收完的,或者是新接收到的。下面就进行数据的拷贝。数据拷贝的长度由参数确定,数据拷贝的操作通过调用netbuf_copy_partial接口完成。最后获取数据的发送者信息,并将它们连同数据一起交给调用者,返回完成拷贝的数据长度。

在下层,新数据的接收是通过netconn_recv函数完成的,在该函数中,我们直接从消息邮箱中从当前连接上获取数据。之后调用注册的回调函数,通知NETCONN_EVT_RCVMINUS事件,这可及时的告知select的调用者连接不可读的信息。对于tcp连接,如果连接被关闭了,则通过设置接收邮箱为空表明不再期望去接收数据,并且申请一个API_MSG_RECV类型的消息,通知协议栈数据已经被取走。

在协议栈核心层,该消息会被取到,核心任务会调用执行do_recv函数。在该函数中,会调用tcp模块的tcp_recvd接口,通知tcp模块,数据已经被取走,可以右移接收窗口。整个接收的处理流程如下图所示:

Lwip之API接口与原理_第8张图片

 

对于数据接收部分的补充说明:对于TCP而言,底层对接收到的数据处理完成后,调用回调函数将其交给邮箱。具体就是调用recv_tcp接口函数。这个函数在建立连接时注册到相应的数据结构体上的。回调函数再调用socket层的回调函数,设置该链接有数据到达,同时将数据发送到连接的接收邮箱中。

应用接收数据时,调用select检查,会发现有数据到达,这时调用netconn_recv接收数据。该函数从连接的接收邮箱中拿出数据,检查没有错误后再次调用socket层的回调函数,告诉select当前连接的数据已经接收一部分了,另外,发送一个处理tcp窗口的消息给核心任务。因为接收了一部分数据了嘛。核心任务处理完TCP窗口的相关处理后发送相应给nettcon_recv函数,这时该函数返回,同时带回了接收到的数据。

对于select的处理,我们是按照事件来处理的,也就是说信息是按照一个事件一个事件增加和减少的,比如有数据到达了,我们就增加一个有数据的事件,而不是依据数据buffer来处理,比如buffer里有数据了select就可以返回可以接收数据的指示了。实际上,后一种更加可靠,也更符合本意,但是前一种实现起来更简单点。

接收的数据的长度是pbuf结构体里的,从底层到上层传输的过程中应该不曾变动过。

Select

Select操作提供在特定时间内检测当前主机建立的多个连接是否可以发送数据,是否有数据到达,是否出现了异常。上层任务调用该接口会被阻塞,直到有信息返回或者超时后返回。

对应判断是否有数据到达,更好的实现是判断连接的接收缓存中的数据是否都被取走,不过在lwip中是通过一种类似事件的机制完成的。

Select函数是不可重入的,因为在操作过程中会遍历协议栈的socket数组和select回调列表进行查询。因此首先创建信号量selectsem来对该函数的执行加以保护。之后从参数中拷贝读写异常数据集。准备工作完成后就进行真正的查询。调用lwip_selscan函数,遍历指定的套接字集合,获取各个套接字socket结构上关于读写的信息,通过参数传入的变量的指针返回结果。如果至少有一个套接字表明有相关事件发生,则释放信号量,返回相关结果;否则,表明当前主机中没有任何一个连接上有收发事件产生,进一步处理。

此时如果设置了超时结构体变量,并设置超时时间为零,则释放信号量,返回0表明超时。如果没有设置超时结构体变量,或者设置的超时时间不为零,则进入超时处理:首先创建一个select回调结构lwip_select_cb,并创建信号量,当前任务会在该信号量上等待。将创建的select回调结构添加到select回调处理全局链表select_cb_list上。这时候就可以安全的释放selectsem信号量了,因为对select_cb_list全局变量的操作结束了。下一步设置超时时间。如果不存在超时结构体,则将超时时间设置为无限长。否则,按照输入的参数设置超时时间。之后调用sys_sem_wait_timeout使当前调用任务在select回调创建的信号量上等待。此时任务就被阻塞了,返回的条件是超时(如果是无限长等待,则这一条件永远不会满足,除非关机)或者某个连接上有收发事件产生。返回后,会从select_cb_list上将当前任务创建的回调结构体移除。如果是超时返回,则清空读写描述符集并返回零表明select调用超时,否则,表明在超时等待时间内有读写事件发生,此时再调用一次lwip_selscan进行读写描述符集合的设置,然后最终返回操作的结果。

lwip_selscan函数用于在socket结构数组中按照划定的范围查找是否有连接上存在读写或异常事件产生。For循环变量设置的socket连接,对于每一个连接,查找其结构体上的读写事件信息,遍历完后返回操作结果。

在之前的介绍中提到过,select调用任务可能在超时等待中因为有连接上产生读写事件而返回,这一点是通过回调函数event_callback来通知的。Event_callback作为连接netconn的回调注册函数,大部分情况下在协议栈核心进行相应的处理后会被调用,比如连接上有新数据到达时,会调用该回调通知一个NETCONN_EVT_RCVPLUS事件,当上层成功读取数据后,则会调用该回调通知一个NETCONN_EVT_RCVMINUS事件等。并且每次被调用时,该函数都会检查select_cb_list上是否有某个任务等待的信号量被满足,如果有的话,则释放信号量,通知等待该信号来的任务可以继续其处理了。这样,当有新的事件到达后就可以及时的通知阻塞的任务,也就是select的调用任务。

Close

当某个连接不再需要时,需要调用close是否连接占用的资源。在lwip中,close的处理比较简单,如果当前关闭的连接是有效的连接,则调用netconn_delete释放该连接。Netconn_delete会申请一个API_MSG_DELCONN类型的消息,将其交给协议栈核心。核心任务掉用do_delconn处理该消息。在do_delconn中,会移除连接对应的pcb,释放相应的资源。在底层处理完成后,上层会接着释放recvmbox和acceptmbox中的所有消息,释放其占用的资源,并返回。Close的处理流程如下图:

    

Lwip之API接口与原理_第9张图片

 

 

下载地址:

https://download.csdn.net/download/wwwyue1985/14094126

你可能感兴趣的:(LwIP,嵌入式,协议栈,tcpip,socket)