|
因此,该项目最终成为支持TCP和应用层协议(如HTTP)的开发项目。
摘要 该项目将几个新功能集成到AdaCore驱动程序库中,通过适应和集成适用于新lwIP端口的现有网络协议Ada库,提供基于现有lwIP实现的IoT框架,该实现移植到嵌入式STM32以太网系列设备中。可以使用HTTP服务器和MQTT客户端,为它们提供基本和经典的helloworld示例代码,作为基于100%Ada的复杂可靠的嵌入式物联网和以太网网关解决方案的起点。 关于该项目 当我第一次看到比赛时,我想做一些IoT甚至一些依赖于某些网络协议的高级应用程序但很快我意识到Ada驱动程序库缺少TCP堆栈,Ada-enet库提供UDP套接字,但大多数众所周知的协议,如HTTP需要TCP。因此,该项目最终成为支持TCP和应用层协议(如HTTP)的开发项目。目前,MQTT客户端和HTTP服务器提供基本功能,提供支持代码以及支持更复杂应用程序的基本演示片段。 里程碑 I.将ipstack从AdaCore Spark2014 git移植到带有以太网外设的嵌入式STM32设备。 II。从Dmitry A. Kazakov简单组件库中调整和移植MQTT客户端代码。 III。从Simple Components调整和移植HTTP Server代码,使用底层“套接字”服务器RAWlwIP支持,以便在其他客户端/服务器实现中使用。 IV。为SMT32提供MQTT Client和Hello WorldHTTP Server的示例代码。 详细说明 1.IP堆栈初始化 初始化包括启动以太网驱动程序和lwIP堆栈。第一个类似于ada-enet库,除了ipstack没有(还)DHCP协议,所以我们在代码中修复IP地址静态,这是在AIP.OSAL包的Initialize函数中完成的,因为ipstack也是需要拥有IP地址,并且为了在一个地方设置值,ipstack从Ifnet记录中获取值。函数If_Init负责ipstack初始化,然后对它进行一些讨论。 function If_Init return Err_T is Mac : LL_Address_Storage; begin Err := NOERR; Name := "st"; NIF.Allocate_Netif (If_Id); if If_Id = NIF.IF_NOID then Err := ERR_MEM; end if; NIF.NIF_Set_Name (If_Id, Name); -- Mac (1) := Ifnet.Mac (1); for I in Integer range 1 .. 6 loop Mac (Net.Uint8 (I)) := Ifnet.Mac (I); Netif_MAC_Addr (Net.Uint8 (I)) :=Ifnet.Mac (I); end loop; NIF.NIF_Set_Offload_Checksum (If_Id,NIF.IP_CS, True); NIF.NIF_Set_Offload_Checksum (If_Id,NIF.ICMP_CS, True); NIF.NIF_Set_Offload_Checksum (If_Id,NIF.UDP_CS, True); NIF.NIF_Set_Offload_Checksum (If_Id,NIF.TCP_CS, True); NIF.NIF_Set_LL_Address_Length (If_Id,LL_Address_Range'Last); NIF.NIF_Set_LL_Address (If_Id, Mac); NIF.NIF_Set_MTU (If_Id, Net.Uint16 (1500)); IP := IPaddrs.IP4 (Ifnet.Ip (1), Ifnet.Ip (2), Ifnet.Ip (3), Ifnet.Ip (4)); Mask := IPaddrs.IP4 (Ifnet.Netmask (1), Ifnet.Netmask (2), Ifnet.Netmask (3), Ifnet.Netmask (4)); Broadcast := IPaddrs.IP4 (192, 168, 2, 255); Remote := IPaddrs.IP_ADDR_ANY; -- make static ARP for tests to isolate the issuewith ARP append buffer declare remote_mac : aliased AIP.Ethernet_Address; remote_ip : aliased AIP.IPaddrs.IPaddr; arp_err : AIP.Err_T; begin remote_mac := (16#f8#, 16#32#, 16#e4#, 16#88#, 16#57#, 16#0c#); remote_ip := IPaddrs.IP4 (192, 168, 2, 5); ARP.ARP_Update (Nid => If_Id, Eth_Address => remote_mac, IP_Address => remote_ip, Allocate => True, Err => arp_err); pragma Unreferenced (arp_err); end; -- define the callbacks NIF.NIF_Set_Input_CB (If_Id,AIP.IP.IP_Input'Access); NIF.NIF_Set_Output_CB (If_Id,AIP.ARP.ARP_Output'Access); NIF.NIF_Set_Link_Output_CB (If_Id, Net.MinIf.LL_Output'Access); NIF.If_Config (If_Id, IP, Mask, Broadcast,Remote, Err); return Err; end If_Init; 请注意,ipstack已经预见到我们会利用ChecksumOffload Checks,因此您必须使用NIF_Set_Offload_Checksum过程相应地设置标志。另请注意,我们将回调保留为原始代码,即调用C代码,但现在只调用Ada代码。 你可以注意到我们在ARP列表上设置了一个初始值,这是为了在没有ARP数据包打开和关闭的情况下进行测试,所以你可以在那里替换你的主机IP地址或只是注释代码。我不是网络方面的专家,但是当我收到一个IP数据包而不是尝试使用收到的数据包中的数据更新ARP缓存时,ipstack发送ARP查询这个事实让我有些烦,真的不知道怎么回事标准应该是,假设我们有一个服务器监听端口7(echo服务器),ipstack接收SYN控制数据包,而不是立即用SYN-ACK重放它将SYN数据包放入Unack Queue并发送一个ARP查询收到IP地址,然后收到ARP重播时,它恢复三次握手。 在调试测试期间,我使用原始的三次握手解决了很多问题,因此我决定使用该片段隔离ARP。 因此,请记住在那里更改所需的STM32 IP地址,直到DHCP准备就绪。(我将尝试从ada-enet端口DHCP端口这些天 ![]() 我从ada-enet库中提到了这个项目,所以“main”文件仍然被称为ping,这是我稍后会尝试更改的,所以Receiver和Demos包也是如此,所以对Initialize的调用实际上是在演示包,即AIP.OSAL.Initialize。 在我最初的测试中,我注意到我的STM32F769I-DISCO板和ada-enet库存在一些问题,我注意到ping过程中丢失了一些数据包,因为我不想继续处理底层问题,所以我很关心在没有最终确定解决方案的情况下报告ada-enet的git问题,后来我发现问题可能与以太网堆栈和LCD之间的一些冲突有关,当我报告问题时我禁用了STM32的D缓存,这似乎缓解了问题,经过一些测试我决定禁用定期调用的LCD刷新程序(我在开始时用某种方式做一些调试),我提到这个,因为你可能会注意到一些LCD功能,以防你想要启用。 2.原始的lwIP和它的回调 因为ipstack是基于RAW lwIP所以一切都将是一个回调所以你最好习惯在开始之前 ![]() 要开始接收将提供ipstack的以太网数据包,需要调整低级驱动程序。原始代码使用基于Linux的Tap接口,应该使用嵌入式STM32设备以太网驱动程序来替换它。Ada-enet生态系统的优秀示例代码被用作起点,但由于ipstack具有ARP,ICMP,UDP和TCP,因此将其简化为仅收集以太网帧所需的代码。基本上,Net.Buffers包用于收集以太网帧以及Net.Interfaces包,该包用于初始化接口并提供发送和接收功能以及其他支持包。 Ada-enet使用一个接收器任务,在它们到达时永远循环传送以太网帧,这仍然是类似的,所以对Ifnet.Receive(Packet)的调用将阻塞,直到有一个帧可用,之后我们将缓冲区传递给低级别接收将数据复制到ipstack缓冲区的函数,因此此版本目前不提供零拷贝。 为了尽可能保持ipstack原始结构,包AIP.OSAL.Single包含不同的函数和程序,Process_Interface_Events负责为ipstack分配缓冲区,我们不直接调用低级接口的原因是因为父包,即AIP.OSAL,持有Interface id变量,最初ipstack允许这个接口Id的数组,但是嵌入式只有一个接口,并且没有数组的意义,所以我们声明If_Id in定义文件。 返回到Process_Interface_Events函数,它调用来自src / dev文件夹下的Net.MinIf包的低级函数LL_Input,以维护与Linux tap C驱动程序的原始代码类似的结构。 LL_Input它完成将帧复制到必要的pbuf中的工作。原始代码对接收到的帧进行一些过滤,首次尝试是尝试激活STM32以太网驱动程序的本机过滤,因为它可以通过Mac进行过滤,但由于某些原因不能按预期工作,并且随着时间的推移非常快我们做了在此函数内手动过滤,仅传递与Mac地址或广播数据包匹配的数据包(ARP需要)。 这是LL_Input功能代码。 function LL_Input (Nid : AIP.NIF.Netif_Id; Buf : in out Net.Buffers.Buffer_Type) return AIP.Buffers.Buffer_Id is p : AIP.Buffers.Buffer_Id; q : AIP.Buffers.Buffer_Id; size : AIP.Buffers.Data_Length; Ether : Net.Headers.Ether_Header_Access; DstMac : Net.Ether_Addr; LL_Add : AIP.LL_Address (1 .. 6); LL_Add_Len : AIP.LL_Address_Range; Bail_Out : Boolean := False; bufLen : Standard.Integer; srcAdd : System.Address; dstAdd : System.Address; begin -- pragma Unreferenced (Nid); Ether := Buf.Ethernet; if Net.Headers.To_Host (Ether.Ether_Type) =Net.Protos.ETHERTYPE_IP then -- If we got an IP packet verifythe padding to alloc the correct size -- since Ada LwIp implementationuses Buffers.Tlen for some calcs declare IpHdr : Net.Headers.IP_Header_Access; begin IpHdr := Buf.IP; if Net.Headers.To_Host (IpHdr.Ip_Len) <= 46 then -- We set the size to allocaccording to packet size size := Net.Headers.To_Host(IpHdr.Ip_Len) + 14; else size := Net.Buffers.Get_Data_Size(Buf, Net.Buffers.RAW_PACKET); end if; end; else size := Net.Buffers.Get_Data_Size (Buf,Net.Buffers.RAW_PACKET); end if; DstMac := Ether.Ether_Dhost; AIP.NIF.Get_LL_Address (Nid, LL_Add,LL_Add_Len); if (Uint_16 (size) < Uint_16 (LL_Add_Len)) then return AIP.Buffers.NOBUF; end if; -- Compare is packet is for us -- I know I can do this with Filter options ofSTM32 ... -- but I couldn't make it work, see line 370 ofnet-interfaces-stm32.adb for I in LL_Add'Range loop if LL_Add (I) /= DstMac (Standard.Integer(I)) then Bail_Out := True; exit; end if; end loop; -- Compare Broadcast if Packet Destination Addrmatch if Bail_Out then for I in 1 .. Standard.Integer (LL_Add_Len) loop if DstMac (I) /= Bcast (I) then return AIP.Buffers.NOBUF; end if; end loop; end if; AIP.Buffers.Buffer_Alloc (0, size,AIP.Buffers.LINK_BUF, p); if p /= AIP.Buffers.NOBUF then q := p; srcAdd := Net.Buffers.Get_Data_Address(Buf); loop bufLen := Standard.Integer(AIP.Buffers.Buffer_Len (q)); dstAdd := AIP.Buffers.Buffer_Payload(q); AIP.Conversions.Memcpy (dstAdd,srcAdd, bufLen); q := AIP.Buffers.Buffer_Next (q); exit when q = AIP.Buffers.NOBUF; srcAdd :=Net.Buffers.Get_Data_Address_Pos (Buf, Uint16 (bufLen)); end loop; end if; Net.Buffers.Release (Buf); return p; end LL_Input; 这里要注意的重要一点是确定将用于保留pbuf的数据包大小的第一部分,注意到STM32以太网驱动程序检查以太网帧的最小尺寸,它是为Tx和Rx做的,因为你将稍后会看到一些tcpdump捕获,因为Tx没有关注但是对于Rx,如果你保留一个包含填充八位字节的大小,它会影响ipstack,STM32以太网驱动程序对小于60字节的数据包执行此操作。我试图找到Rx填充是否可以禁用而没有成功,所以它似乎可以禁用但它会影响Tx,所以我从未进行过测试,而是使用之前的代码段来修复大小。代码使用ada-enet中的一些util函数来提取IP头长度,如果小于46则意味着填充将跟随, 接收任务 接收器无限任务将根据其类型调度数据包,现在我们必须识别ARP和IP数据包,这是接收器任务。 task body Controller is use type Net.Uint64; use type Net.Ether_Addr; Buf : AIP.Buffers.Buffer_Id; Ethhr : System.Address; Ftype : Net.Uint16; Packet : Net.Buffers.Buffer_Type; Err : AIP.Err_T; begin -- Wait until the Ethernet driver is ready. Ada.Synchronous_Task_Control.Suspend_Until_True (Ready); AIP.IO.Put_Line ("Receiver Task Starts"); -- Loop receiving packets and dispatching them. loop if Packet.Is_Null then Net.Buffers.Allocate (Packet); exit when Packet.Is_Null; end if; if not Packet.Is_Null then -- Block until there is a packet toprocess AIP.OSAL.Ifnet.Receive (Packet); Buf := AIP.OSAL.Single.Process_Interface_Events(Packet); if Buf /= AIP.Buffers.NOBUF then Ethhr := AIP.Buffers.Buffer_Payload(Buf); Ftype :=AIP.EtherH.EtherH_Frame_Type (Ethhr); case Ftype is when AIP.EtherH.Ether_Type_IP => AIP.Buffers.Buffer_Header(Buf, AIP.S16_T (-14), Err); if AIP.Any (Err) then AIP.Buffers.Buffer_Blind_Free (Buf); else AIP.OSAL.Single.Process_Input(Buf); end if; when AIP.EtherH.Ether_Type_ARP => AIP.OSAL.Single.Process_Arp(Buf); when others => AIP.Buffers.Buffer_Blind_Free(Buf); end case; end if; end if; end loop; AIP.IO.Put_Line (" roblem with Net Stack Packet allocation");end Controller; 这里需要注意的一点是,当遇到错误时,需要释放LwIP缓冲区。对于其他情况,Free将在调用过程中完成。在讨论ipstack如何处理数据包之前,让我们完成低级输出过程。 具有之前看到的低级输入的相同包Net.MinIf具有低级输出过程,并且实际上是反向的,接受ipstack pbuf并将其数据复制到以太网Tx的Net.Buffers。困难的部分是要实现如何跟踪下一个要写入Net.Buffers数据包的位置,这是使用Get_Data_Address_Pos函数完成的,在复制完所有数据之后,只剩下设置长度并调用Net的发送过程.Interfaces.STM32包来传递它。 低级输出过程将由ipstack使用AIP.NIF包调用Link_Output过程调用,该过程仅在AIP.ARP包中完成,另一个将发送IP包的输出过程实际上是ARP_Ouput,因为以太网信息由信息填充在ARP表中。 IP数据包 接收方通过调用AIP.OSAL.Single.Process_Input(Buf)过程来调度IP数据包,该过程确实在AIP.IP包中调用已配置的回调IP_Input过程。 IP_Input对IP Header本身进行一些健全性检查,其中它检查IP地址是否配置,原始ipstack有一些(To Be Defined或TBD)函数转发,这里只有一个接口没有意义所以它总是禁用(Enable_Forwarding标志为False)。 最后,它通过调用Dispatch_Upper来调度数据包,此过程调用适当的UDP,TCP或ICMP过程。你想要尝试的第一件事就是ICMP。此时,如果IP地址配置正确,它应该可以解决问题。 TCP数据包 TCP数据包由TCP_Input过程处理。它也对TCP Header执行一些健全性检查,但是如果数据包是正确的,它会尝试找到要传送它的PCB,这是由AIP.PCBs包中的PCBs.Find_PCB完成的。PCB被命名为协议控制块,它们在TCP和UDP之间共享,当然你比我更了解这一点,但简而言之,你将拥有一个PCB,用于你拥有的每个“连接”,假设你想连接到一个服务器,ipstack将在其生命周期内为PCB连接保留一块可用的PCB,因此它可以唯一地识别它,也许这在LwIP术语中过于简单化,但在上层工作的人不必了解每个细节。它 如果收到的TCP数据包有PCB,则TCP_Input调用TCP_Receive进一步处理它,否则它可能会向对等体发送RST控制段以通知它。 TCP_Receive过程通过在需要时将其传递给应用程序并更改TPCB状态来处理TCP数据包。 这个讨论在某些方面可能很无聊但更糟糕的是让它工作(实际上令人沮丧更合适)因为原始的ipstack似乎有一些黑洞,例如在ICMP测试工作之后我尝试了已经在ipstack中的Echo Server代码,当使用连接到端口7的简单python客户端测试时“工作”,发送“Hello World”并断开连接,控制台显示收到的Echo消息,但tcpdump capture是关注的。有关详细信息,请参阅已知问题 这是第一次捕获的截图:Echo Three-Way错误 正如您所看到的那样,服务器没有正确处理三向握手,实际情况甚至更糟,因为客户端通过发送FIN-ACK服务器断开连接后发送一个空的有效负载数据包,总之它需要一些检查。 我们现在不打算讨论这个函数的所有代码行,因为它很长,而是我们将继续使用项目文档并回过头来突出显示在这个和其他aip.tcp过程中完成的某些更改和解释。和功能。 现在重要的是继续前进是高亮显示TCP堆栈通知App层有关数据包接收的方式。它通过TCP_Event过程实现,它实际上从堆栈中插入事件和PCB并调用(如果由应用层定义)分配的回调过程,让我们看看它在简单回显服务器的情况下是如何工作的。 回声服务器 Echo服务器非常简单,所有代码都位于src /services下的单个包中,名为raw_tcp_echo.adb。通过调用RAW_TCP_Echo.Init在demos.adb中完成服务器初始化。 Init功能如下所示 procedure Init with Refined_Global => (In_Out => ESP) is Pcb : AIP.PCBs.PCB_Id; Err : AIP.Err_T; begin -- Initialize the application state pool, thensetup to -- accept TCP connections on the well known echoport 7. Init_ES_Pool; AIP.TCP.TCP_New (Pcb); if Pcb = AIP.PCBs.NOPCB then Err := AIP.ERR_MEM; else AIP.TCP.TCP_Bind (PCB => Pcb, Local_IP => AIP.IPaddrs.IP_ADDR_ANY, Local_Port => 7, Err => Err); end if; if Err = AIP.NOERR then AIP.TCP.TCP_Listen (Pcb, Err); pragma Assert (AIP.No (Err)); AIP.TCP.On_TCP_Accept (Pcb, RAW_TCP_Callbacks.To_CBID(ECHO_Process_Accept'Access)); end if; end Init; 函数TCP_New(Pcb)创建服务器Pcb,然后绑定并侦听该Pcb。使用On_TCP_Accept过程为Pcb设置Accept回调。 更有趣的是在下面显示的accept回调中做了什么。 ------------------------- --ECHO_Process_Accept -- ------------------------- procedure ECHO_Process_Accept (Ev : AIP.TCP.TCP_Event_T; Pcb : AIP.PCBs.PCB_Id; Err : out AIP.Err_T) is pragma Unreferenced (Ev); Sid : ES_Id; begin ES_Alloc (Sid); if Sid = NOES then Err := AIP.ERR_MEM; Raise_Exception (Data_Error'Identity, "Alloc Sid Failed!"); else ESP (Sid).Kind := ES_ACCEPTED; ESP (Sid).Pcb := Pcb; ESP (Sid).Buf := AIP.Buffers.NOBUF; -- AIP.IO.Put_Line ("New PCBAccept: Sid"); -- AIP.IO.Put_Line (Sid'Image); AIP.TCP.TCP_Set_Udata (Pcb, ESP (Sid)'Address); AIP.TCP.On_TCP_Sent (Pcb, RAW_TCP_Callbacks.To_CBID(ECHO_Process_Sent'Access)); AIP.TCP.On_TCP_Recv (Pcb, RAW_TCP_Callbacks.To_CBID(ECHO_Process_Recv'Access)); AIP.TCP.On_TCP_Abort (Pcb, RAW_TCP_Callbacks.To_CBID(ECHO_Process_Abort'Access)); AIP.TCP.On_TCP_Poll (Pcb, RAW_TCP_Callbacks.To_CBID(ECHO_Process_Poll'Access), 500); AIP.TCP.TCP_Accepted (Pcb); Err := AIP.NOERR; end if; end ECHO_Process_Accept; ipstack的创建者是切割器来创建一个记录来保存每个连接的状态,它实际上是一个数组,因为服务器可以在给定时间连接多个客户端。数组类型是Echo_State并由 type State_Kind is (ES_FREE, ES_READY, ES_ACCEPTED,ES_RECEIVED, ES_CLOSING); type Echo_State is record Kind : State_Kind; Pcb : AIP.PCBs.PCB_Id; Buf : AIP.Buffers.Buffer_Id; Err : AIP.Err_T; end record; 堆栈的美妙之处在于回调完成后恢复状态的方式,状态数组实际上是一个存在于此文件中的数组,对于echo tcp demo它看起来不是什么大不了的事情,但是稍后你将会看看我们如何利用它。ipstack IPCB包含一个IP_PCB数组,一个保存远程/本地ip和端口,连接状态和链接类型等信息的记录,但是有一个名为Udata的记录项实际上是指向应用程序数据的指针,你猜,在TCP回声中,数据是我们的Echo_State记录。应用程序分配的方式是在我们的Echo_State数组ESP中保留Pcb之后使用接受回调中显示的函数TCP_Set_Udata,该演示允许在此阵列上有4条记录,但这需要更多的资源。 如果服务器接受连接,则会设置Sent,Receive,Abort和Poll的回调。最后一个很重要,需要注意的是,这是一个由TCP定期执行的回调,所以你可能会问,计时器在哪里? TCP定时器 TCP有两个通过Timer定期调用的函数,这些函数在TCP_Init中配置如下所示。 -------------- -- TCP_Init -- -------------- procedure TCP_Init with Refined_Global => (Output =>(All_PCBs, Boot_Time, IPCBs, TCP_Ticks, TPCBs)) is begin -- Record boot time to serve as local secret forgeneration of ISN Boot_Time := Time_Types.Now; TCP_Ticks := 0; -- Initialize all the PCBs, marking them unused,and initialize the list -- of bound PCBs as empty. IPCBs := TCP_IPCB_Array'(others => PCBs.IP_PCB_Initializer); TPCBs := TCP_TPCB_Array'(others => TCP_PCB_Initializer); All_PCBs := TCP_PCB_Heads'(others => PCBs.NOPCB); -- Allocate and Set frequency of TCP timers Timers.TID_Alloc(Timers.TIMER_EVT_TCPFASTTMR, TCP_Fast_Timer'Access); Timers.TID_Alloc(Timers.TIMER_EVT_TCPSLOWTMR, TCP_Slow_Timer'Access); Timers.Set_Interval(Timers.TIMER_EVT_TCPFASTTMR, TCP_Fast_Interval); Timers.Set_Interval (Timers.TIMER_EVT_TCPSLOWTMR,TCP_Slow_Interval); end TCP_Init; 我修改了原来的AIP.Timers,因为它有点限制,因为它繁琐耗时且更容易出错而且更容易添加一个不在定时器固定列表中的Timer,因此我花了一些时间使其灵活因此,任何需要Timer的应用程序服务都可能使用它,实际上我之所以这么做,因为MQTT协议需要一个Timer(稍后会详细介绍)。函数TID_Alloc通过将其与回调相关联来分配新的Timer。您可能会注意到我仍然使用固定计时器列表,但实际上还有另一个使用索引值的函数,因此您可以在不知道它将分配给哪个Timer的索引的情况下保留它。 在通过Set_Interval分配间隔之前,Timer不会启动。 一个重要的事情是,Timers运行在一个名为Periodic ofOs_Service包的单独任务中。此文件位于demos /utils板内,因为我找不到更好的位置。Periodic专门用于测量时间并在每个TIMER_PERIOD中调用Process_Timers,如下所示。 task body Periodic is Prev_Clock, Clock : AIP.Time_Types.Time := AIP.Time_Types.Time'First; timer_deadline : Ada.Real_Time.Time; Poll_Freq : constant := 50; begin pragma Unreferenced (Prev_Clock, Poll_Freq); Ada.Synchronous_Task_Control.Suspend_Until_True (Ready); Clock := AIP.Time_Types.Now; timer_deadline := Ada.Real_Time.Clock; loop delay until timer_deadline; Clock := AIP.Time_Types.Now; AIP.OSAL.Single.Process_Timers (Clock); timer_deadline := timer_deadline +TIMER_PERIOD; end loop; end Periodic; 定时器工作得很好,但回调不应该花太长时间才能返回,否则它将开始失败,将来应该调查一个更好的方法来解决回调。 Echo Server回调 回调并不意味着知道给定连接的实际状态,除非一旦它在某个内部它找到哪个Echo State Pool对应于该调用,所以这就是我们如前所示将应用程序数据分配给IPCB的原因,以便检索它的回调是: Es : Echo_State; for Es'Address use AIP.TCP.TCP_Udata (Pcb); 这样,代码的执行由当前状态决定,即,在接收期间更明显的用法,例如,如果在Event参数(TCP_Event_T)中没有任何缓冲区的情况下调用接收回调并且没有更多缓冲区要处理,我们可能在Pcb上调用Close但如果有待处理的待处理数据,则服务器发送它并将Close委托给Polling回调,因为它已将状态设置为ES_CLOSING。为清楚起见,下面显示了代码。 ----------------------- --ECHO_Process_Recv -- ----------------------- procedure ECHO_Process_Recv (Ev : AIP.TCP.TCP_Event_T; Pcb : AIP.PCBs.PCB_Id; Err : out AIP.Err_T) is Es : Echo_State; for Es'Address use AIP.TCP.TCP_Udata (Pcb); begin if Ev.Buf = AIP.Buffers.NOBUF then -- Remote host closed connection.Process what is left to be -- sent or close on our side. Es.Kind := ES_CLOSING; if Es.Buf /= AIP.Buffers.NOBUF then Echo_Send (Pcb, Es); else Echo_Close (Pcb, Es); end if; else -- Signal TCP layer that we canaccept more data case Es.Kind is when ES_ACCEPTED => Es.Kind := ES_RECEIVED; Es.Buf := Ev.Buf; AIP.Buffers.Buffer_Ref (Ev.Buf); Echo_Send (Pcb, Es); when ES_RECEIVED => -- Read some more data if Es.Buf = AIP.Buffers.NOBUF then AIP.Buffers.Buffer_Ref(Ev.Buf); Es.Buf := Ev.Buf; Echo_Send (Pcb, Es); else AIP.Buffers.Buffer_Chain(Es.Buf, Ev.Buf); end if; when others => -- Remote side closing twice(ES_CLOSING), or inconsistent -- state. Trash. AIP.TCP.TCP_Recved (Pcb,AIP.Buffers.Buffer_Tlen (Ev.Buf)); Es.Buf := AIP.Buffers.NOBUF; end case; end if; Err := AIP.NOERR; end ECHO_Process_Recv; 正如您所看到的,Echo服务器很简单,它只是通过实际重用Pcb来调用Echo_Send,因为数据甚至没有改变。这不是您和我可能想到的其他服务器的情况。 有一点,我将借此机会强调。如果你看到ipstack的TAP演示的tcpdump捕获有效,你会注意到一些奇怪的东西。 ipstack TCP Echo tcpdump capture 服务器不会使用Ack重播收到的数据,据我所知,这不正确,但它可能会执行作业。在我遇到HTTP服务器问题之后,我决定也接受这个问题。 原始堆栈调用程序AIP.TCP.TCP_Recved(Pcb,Plen); 一旦应用程序使用了数据但是利用Ack发送到堆栈状态机来调整接收窗口,由于某种原因它缺失了。 在我做过的几次修改中,我在函数中添加了一些Ack Now,注意在此之后从Unack_Queue中删除Buffer。实际上在看这篇文章时我花了一些时间阅读lwIPmqtt的C实现,其中有Ack Now函数,我个人认为可能有更好或不同的方法来纠正这个问题,但是这个方法可以用到你能看到的以下捕获嵌入式STM32 ipstack Echo服务器。 STM32 TCP Echo捕获 这是TCP_Recved片段 ---------------- -- TCP_Recved-- ---------------- procedure TCP_Recved (PCB : PCBs.PCB_Id; Len : AIP.U16_T) with Refined_Global => (In_Out => TPCBs) is Seg : Buffers.Buffer_Id; Thdr : System.Address; begin -- Open receive window now that application hasconsumed the data TPCBs (PCB).RCV_WND := TPCBs (PCB).RCV_WND+ AIP.M32_T (Len); if Len > 0 then Buffers.Buffer_Alloc (Size => TCPH.TCP_Header_Size / 8, Offset => Inet.HLEN_To(Inet.IP_LAYER), Kind => Buffers.SPLIT_BUF, Buf => Seg); Thdr := Buffers.Buffer_Payload (Seg); TCPH.Set_TCPH_Data_Offset (Thdr, 5); -- No options present TCPH.Set_TCPH_Seq_Num (Thdr, TPCBs (PCB).SND_NXT); TCPH.Set_TCPH_Urg (Thdr, 0); TCPH.Set_TCPH_Psh (Thdr, 0); TCPH.Set_TCPH_Syn (Thdr, 0); TCPH.Set_TCPH_Fin (Thdr, 0); TCPH.Set_TCPH_Rst (Thdr, 0); -- Force Reset since we are embedded TCPH.Set_TCPH_Ack (Thdr, 1); Buffers.Set_Packet_Info (Seg, Thdr); TCP_Send_Segment (Tbuf => Seg, IPCB => IPCBs (PCB), TPCB => TPCBs (PCB)); Buffers.Remove_Packet (Buffers.Transport, TPCBs(PCB).Unack_Queue, Seg); Buffers.Buffer_Blind_Free (Seg); end if; end TCP_Recved; TCP_Output程序也可以用于Ack Now但是如果它也刷新Send_Queue,无论如何这可能是对github的讨论,其他人比我更了解这一点。 当echo服务器App关闭连接时,它还通过调用ES_Release过程释放它的回声状态池(ESP),该过程基本上将连接标记为ES_FREE状态,并将null地址标记为应用程序数据。 所以现在Echo_Send首先调用TCP_Recved,然后使用TCP_Write直接写入。 三方握手 当我开始研究mqtt客户端时,我意识到tcp堆栈无法充当客户端,但我将在稍后讨论这些修改。服务器的原始代码似乎实现了主动关闭而不是被动关闭,或者至少我没有找到使其开箱即用的方法,因此开始查看从服务器端看起来应该是什么样的被动关闭。一旦接收到FIN,服务器应该转到Close_Wait状态并发送它的相应FIN,然后它转换到Last_Ack。 这是一个非常有用的图表。 TCPIP_State_Transition_Diagram.pdf 我将Last_Ack状态检查添加到TCP_Receive,因为当收到此Ack时,它只涉及TCP堆栈操作,因此不需要通知应用程序。该操作包括转换为关闭状态和释放/重置Pcb以供以后重用,该片段是 when Last_Ack => -- Passive Three Way Last Ack processing Set_State (PCB,Closed); TCP_Free (PCB); Err :=AIP.NOERR; 它的工作原理是因为我还在TCP_Close中做了一个修改,我将把它带到这里播放的几个片段。 首先,使用aip.tcp.adb将所有带数据的数据包传递给应用程序 if New_Data_Len > 0 then Deliver_Segment (PCB, Seg.Buf); end if; 但由于某种原因,我真的可以理解(这让我想要重写代码)New_Data_Len 变量计算FIN位,也就是说,如果接收到FIN,则向该变量添加一个“1”,以便它进入那里进行处理,但实际上除非数据包有一些数据,否则什么都不做,因为在Deliver_Segment过程中,如果段len不为零,则完成应用程序回调,然后不会发生任何其他情况。SO实际上是TCP_Receive的下一段代码,它通知我们收到的FIN的应用程序,即(现在的相关部分) if TCPH.TCPH_Fin (Seg.Thdr) = 1 then case TPCBs (PCB).State is when Closed | Listen | Syn_Sent => null; when Established | Syn_Received => -- Notify connection closed: deliver 0 bytes of data. -- First transition to Close_Wait, as the application -- may decide to call Close from within the TCP_Event -- callback. Set_State (PCB, Close_Wait); TCP_Event (Ev => TCP_Event_T'(Kind => TCP_EVENT_RECV, Len => 0, Buf => Buffers.NOBUF, Addr =>IPaddrs.IP_ADDR_ANY, Port => CBs.NOPORT, Err => AIP.NOERR), PCB => PCB, Cbid => TPCBs (PCB).Callbacks(TCP_EVENT_RECV), Err => Err); 第一件事是改变它的状态,如图所示,下一步调用应用层。现在跟随的TCP_Close(相关部分仅显示) when Close_Wait=> -- Transition to LAST_ACK after sending FIN TCP_Fin (PCB, Err); if AIP.No (Err) then Set_State (PCB, Last_Ack); -- Purge any remaining Buffer from Unack Queue TCP_Purge (PCB); Flush := False; end if; 我包含的修改是Purge PCB排队的功能,您只需等待Last_Ack释放PCB,如前所述。这个TCP_Purge的原因是因为原始堆栈调用发送FIN的TCP_Fin,但也包括Unack_Queue中的数据包。这给了我重传的问题,但它有可能如果没有收到Ack,它将不再发送FIN,所以真的需要进一步修改。我遇到的一个问题是调试这个问题,有时你需要一个不影响执行时间的真正快速printf,而ST-Link上的半主机就是这样。 3.在ipstack(raw lwIP)之上工作 让它变得聪明 回声服务器本身并不好玩,也不是非常有用,但它激励我继续努力。由于比赛时间框架非常短(特别适合我,因为我在7月中旬开始)以及我对Ada世界真的很新的事实,我不愿意重新发明轮子,所以我开始查看应用层协议的现有代码,特别是我正在寻找HTTP,这很好,真的有必要。我发现了一些堆栈,但没有为裸机嵌入式世界做过,甚至我都无法使用Ada ARM版本进行编译,因此我花了一些时间来确定哪一个是可行的端口。然后我找到了Dmitry A. Kazakov的作品,这真的引起了我的注意,因为它不仅提供HTTP,还提供其他网络协议,如MQTT和MOdbus TCP。我必须首先说,对我而言,阅读和理解是非常艰巨的工作,但坚持不懈有时会付出代价。这是它的工作,它被称为简单组件,你可以找到它这里 对于在Ada中经验太少的人来说,HTTP起初工作太多了,它使用了gnat.Sockets并且有很多代码要移植,然后我意识到MQTT客户端更简单,因为端口代码更少所以我开始仔细观察它。我还注意到MQTT的原始lwIP C实现在有疑问时作为指导。 MQTT客户端 我做了这个介绍,因为我认为这是我第一次尝试移植SImple组件,我实际上让它工作但是看看我在做HTTP时的第二次尝试。所以请记住这一点,因为将来如果项目发展,MQTT也可能会发生变化。 Simple Components实现了一个MQTT PierClient,它是一个简单的,它是一个最小的实现,因此在这个版本上没有代理。但作者还实现了支持发烧友代理的MQTT服务器 ![]() 在文档中解释的第一件事是所有调用都应该在连接服务器的上下文中完成,因为我没有gnat.Sockets我最终解释这个以保持原始lwIP服务器中所有调用的连接状态,但有一个问题,“这都是关于回调”。您还记得Echo Server中解释的App层的用户数据吗?好吧我们利用它来实现State,但是这次MQTT代码在不同的包上,并且保存连接状态的MQTT_Pier类型对于端口来说实际上是非常模糊的东西。 原来的mqtt客户端包名为GNAT.Sockets.MQTT,所以我开始在我的项目中导入这些文件并从中删除GNAT.Sockets,所以他们在我的项目中调用mqtt.adb和mqtt.ads。这主要是MQTT堆栈本身,希望你会看到没有太多的修改。我将Client实现为子包,即Mqtt.Client包(mqtt-client.adb)。很高兴从客户端开始,因为移植并不像http那么难,但ipstack实际上没有任何客户端,所以最后它花了我一些努力,但最好的部分是客户端在那里,而不是像我一样可重复使用会想要它,但这是另一种努力。 MQTT协议处理连接和断开,这是OSI模型的TCP层4的顶部。所以首先要与mqtt代理(服务器)连接。为此,我为我的测试选择了mosquitto,因为我在Linux下开发并且我可以验证mqtt码头的正确功能,所以它很方便。 第一次尝试是将MQTT_Pier类型作为TCP堆栈的Udata传递,然后踢出踢,直到我意识到在Ada下使用与C / C ++世界略有不同的指针时有几件事情。简短的故事是MQTT_Pier是一种非限制类型,这使我几乎不可能将其指定为System.Address以供以后检索。如果你找到一个更容易或更好的方式,请不要笑,只是让我知道。 所以我最终放弃尝试这种方法,而是保留我原来的状态池数组,这次我把它称为MSP for MQTT State Pool。 在继续之前,让我们更仔细地检查MQTT_Pier类型。 type MQTT_Pier (Max_Subscribe_Topics : Positive ) is new Connection with record State : MQTT_State := MQTT_Header; Count : Stream_Element_Count := 0; Length : Stream_Element_Count := 0; Max_Size : Stream_Element_Count := 0; Header : Stream_Element := 0; Version :Stream_Element; Flags : Stream_Element; Keep_Alive : Duration; QoS : QoS_Level; Packet_ID acket_Identifier; List_Length : Natural := 0; -- Secondary : Output_Buffer_Ptr; List : Unbounded_Ptr_Array; Data : Stream_Element_Array_Ptr := new Stream_Element_Array (1 .. Max_Message_Size); 您可能想知道连接,这就是它 type Connection is abstract new Object.Entity with record State_Pool : Mqtt_Conn_State_Access; App_Cbs : Mqtt_Client_Cbks; Sid : MS_Id; Client_Address : AIP.IPaddrs.IPaddr; Err : AIP.Err_T; end record; 原始版本在连接记录上有几个字段,这里没有像套接字那样适用,因为如果你知道我的意思,我害怕大跳,所以我最后减少它如上所述,你稍后会看到http端口我怎么样保留大部分,当然除了插座。 State_Pool是我用来附加lwIP客户端和mqtt代码的访问,正如您稍后将看到的那样。有时难以编写代码而不会枯燥甚至更糟以致读者丢失所以我会拿起示例代码,这是Mqtt的顶级代码所以我可以下去突出简单组件如何与lwIP连接的内部机制。 示例代码用于证明mqtt客户端,并作为高级客户端代码的指南。它如下所示,您可以在demos文件夹下找到源代码。 procedure Test_1 (Client_Ip : in AIP.IPaddrs.IPaddr) is Client_Err : AIP.Err_T; begin Set (Test_Client_Array (0), new Test_Client(Max_Subscribe_Topics => 20)); declare Client : Test_Client renames Test_Client (Ptr (Test_Client_Array (0)).all); PId : Packet_Identification (Qos => At_Most_Once); begin Client.Set_Client_Ip (Client_Ip); Client.Set_Client_Cbs (Test_Connect'Access, Test_Receive'Access); Client.Connect (Client_Err); if Client_Err = AIP.NOERR then while Client.Is_Connected = False loop delay 0.01; end loop; Send_Connect (Client, "TestMQTTclient"); while Client.Is_Connected = False loop delay 0.01; end loop; Send_Ping (Client); delay 1.0; Send_Publish (Client, "makewithAda/ipstack/test", "bonjour Ada world!", PId); delay 2.0; Send_Disconnect (Client); else AIP.IO.Put_Line ("STM32 Connect Error"); end if; end; end Test_1; 您指定代理的IP地址,即Test_MQTT_Clients.Test_1(Client_Ip); 其中Client_Ip定义为 Client_Ip:常量AIP.IPaddrs.IPaddr:= AIP.IPaddrs.IP4(192,168,2,5); 第一部分允许在给定时间激活多个客户端,因此您基本上在Test_Client上调用new并将其引用保存在Handle数组中。该示例仅使用一个客户端,因此稍后您将声明一个重命名Handle数组的第一个条目的Test_Client类型。 第一件事是应用分配代理的IP地址,也许这里的名称不是更合适,无论如何你定义了两个回调一个用于连接而另一个用于接收,两个都指TCP事件,第一个可能做实际上并没有什么,但第二个是非常重要的,因为我是将TCP连接的数据提供给正确的MQTT Pier的方式。我在这里有点懒,因为接收代码不正确,因为它拿起第一个非空句柄客户端来提供数据。所以应该执行另一个检查,但它很容易让我们现在就做。 首先让我们检查一下回调。 procedure Test_Receive (Pcb : AIP.PCBs.PCB_Id; DATA : Stream_Element_Array; Err : out AIP.Err_T) is begin pragma Unreferenced (Pcb); for Hitem in Test_Client_Array'Range loop if Test_Client_Array (Hitem) /= Null_Handle then declare Client : Test_Client renames Test_Client (Ptr (Test_Client_Array (Hitem)).all); Pointer : Stream_Element_Offset := DATA'First; begin Client.Received (DATA, Pointer); end; end if; end loop; Err := AIP.NOERR; end Test_Receive; 注意我们如何将Pcb作为参数传递。我们需要将它与Client Pcb进行比较,因为这是私有类型的一个选项是编写一个为我们做的小功能。所以让我们去mqtt.adb并添加这样一个函数,我们将调用Does_Mqtt_Pier_Match,如下所示。 function Does_Mqtt_Pier_Match (Pier : in out MQTT_Pier; Pcb : AIP.PCBs.PCB_Id) return Boolean is begin return Pier.State_Pool.Pcb = Pcb; end Does_Mqtt_Pier_Match; 现在我们可以重写回调,如下所示: procedure Test_Receive (Pcb : AIP.PCBs.PCB_Id; DATA : Stream_Element_Array; Err : out AIP.Err_T) is begin for Hitem in Test_Client_Array'Range loop if Test_Client_Array (Hitem) /= Null_Handle then declare Client : Test_Client renames Test_Client (Ptr (Test_Client_Array (Hitem)).all); Pointer : Stream_Element_Offset := DATA'First; begin if Client.Does_Mqtt_Pier_Match (Pcb) then Client.Received (DATA, Pointer); end if; end; end if; end loop; Err := AIP.NOERR; end Test_Receive; 你开始明白这个想法,现在继续使用演示代码。设置回调后,我们执行Connect,它确实使用Client的参数调用Mqtt_Client_Connect。这是代码。 -------------------------- -- Mqtt_Client_Connect --- -------------------------- procedure Mqtt_Client_Connect (Pier : in out MQTT_Pier; Port : Natural := MQTT_Port; Err : out AIP.Err_T) is Pcb : AIP.PCBs.PCB_Id; Sid : MS_Id; begin -- check if client already is allocated or sort of if Pier.State_Pool /= null then Err := AIP.ERR_MEM; return; end if; AIP.TCP.TCP_New (Pcb); if Pcb = AIP.PCBs.NOPCB then Err := AIP.ERR_MEM; return; end if; AIP.TCP.TCP_Bind (PCB => Pcb, Local_IP => AIP.IPaddrs.IP_ADDR_ANY, Local_Port => 0, Err => Err); if Err /= AIP.NOERR then goto Tcp_Error; end if; AIP.TCP.TCP_Connect (PCB => Pcb, Addr => Pier.Client_Address, Port => Net.Uint16 (Port), Cb => RAW_TCP_Callbacks.To_CBID (MQTT_Process_Connect'Access), Err => Err); if Err /= AIP.NOERR then goto Tcp_Error; end if; MS_Alloc (Sid); if Sid = NOMS then -- AIP.TCP.TCP_Free (Pcb); -- I think original stack miss this need Err := AIP.ERR_MEM; goto Tcp_Error; else Pier.Sid := Sid; Pier.State_Pool := MSP (Sid)'Access; Pier.State_Pool.Kind := TCP_CONNECTING; Pier.State_Pool.Pcb := Pcb; Pier.State_Pool.Buf := AIP.Buffers.NOBUF; MSP (Sid).App_Client_Cbs := Pier.App_Cbs; AIP.TCP.TCP_Set_Udata (Pcb, MSP (Sid)'Address); -- Start cyclic timer for the corresponding client Timers.Timer_Alloc (Pier.State_Pool.Tmr_Id, MQTT_Timer'Access); Timers.Set_Interval (Pier.State_Pool.Tmr_Id, MQTT_CYCLIC_TIMER_INTERVAL * MQTT_Tick_Interval); Pier.State_Pool.Cyclic_Tick := 0; end if; -- set error callback AIP.TCP.On_TCP_Abort (Pcb, RAW_TCP_Callbacks.To_CBID (MQTT_Process_Abort'Access)); return;<<Tcp_Error>> declare tcpErr : AIP.Err_T; begin pragma Unreferenced (tcpErr); AIP.TCP.TCP_Close (Pcb, tcpErr); end; end Mqtt_Client_Connect; 第一部分检查客户端是否正在使用,实际上在连接获得之后,指针被设置为Pool的已分配MSP变量。我们仍然使用TCP_Set_Udata来保存它的地址,以便在回调时恢复。 来自Dmitry的原始代码没有定时器,不确定,但至少MQTT Pier没有它,但在查看lwIP实现之后我认为有必要这样定时器分配并设置初始间隔,一些理智检查和中止回调。Connect回调在同一个TCP_Connect过程中设置,其代码如下。 -------------------------- -- MQTT_Process_Connect -- -------------------------- procedure MQTT_Process_Connect (Ev : AIP.TCP.TCP_Event_T; Pcb : AIP.PCBs.PCB_Id; Err : out AIP.Err_T) is Ms : Mqtt_Conn_State; for Ms'Address use AIP.TCP.TCP_Udata (Pcb); Data : Stream_Element_Array (0 .. -1); begin pragma Unreferenced (Ev); -- AIP.IO.Put_Line ("New PCB Accept: Sid"); AIP.TCP.On_TCP_Sent (Pcb, RAW_TCP_Callbacks.To_CBID (MQTT_Process_Sent'Access)); AIP.TCP.On_TCP_Recv (Pcb, RAW_TCP_Callbacks.To_CBID (MQTT_Process_Recv'Access)); AIP.TCP.On_TCP_Poll (Pcb, RAW_TCP_Callbacks.To_CBID (MQTT_Process_Poll'Access), 2 * 500); -- Enter MQTT connect state Ms.Kind := TCP_CONNECTED; -- Reset the Timer Tick for client Ms.Cyclic_Tick := 0; -- Call the User Procedure if available RAW_MQTT_Dispatcher.MQTT_Event (PCB => Pcb, DATA => Data, Cbid => Ms.App_Client_Cbs.App_Connect_Cb, Err => Err); end MQTT_Process_Connect; 你应该已经熟悉了这里的回调,除了我们调用更高层的TCP连接回调,记住什么都不做。让我们进入接收以关闭该循环。接收回调与echo服务器没有什么不同,但是不是将Pcb转发到Echo_Send,我们需要将接收到的数据分派到上层,所以这里是调度功能。 ------------------------ -- Mqtt_Dispatch_Recv -- ------------------------ procedure Mqtt_Dispatch_Recv (Pcb : AIP.PCBs.PCB_Id; Ms : in out Mqtt_Conn_State) is Buf : AIP.Buffers.Buffer_Id; srcAdd : System.Address; Plen : AIP.U16_T; TPlen : AIP.U16_T;-- Err : AIP.Err_T := AIP.NOERR; Item_Size : Stream_Element_Offset; Err : AIP.Err_T; begin if Ms.Buf = AIP.Buffers.NOBUF then return; end if; TPlen := AIP.Buffers.Buffer_Tlen (Ms.Buf); declare Data : Stream_Element_Array (1 .. Stream_Element_Offset (TPlen)); Pointer : Stream_Element_Offset := Data'First; begin loop Buf := Ms.Buf; Plen := AIP.Buffers.Buffer_Len (Buf); Item_Size := Stream_Element_Offset (Plen); declare type SEA_Pointer is access all Stream_Element_Array (1 .. Item_Size); srcPtr : SEA_Pointer; function As_SEA_Pointer is new Ada.Unchecked_Conversion (System.Address, SEA_Pointer); Data_Ptr : SEA_Pointer; begin srcAdd := AIP.Buffers.Buffer_Payload (Buf); srcPtr := As_SEA_Pointer (srcAdd); Data_Ptr := As_SEA_Pointer (Data (Pointer)'Address); Data_Ptr.all (1 .. Item_Size) := srcPtr.all (1 .. Item_Size); -- Grab reference to the following Buf, if any Ms.Buf := AIP.Buffers.Buffer_Next (Buf); if Ms.Buf /= AIP.Buffers.NOBUF then AIP.Buffers.Buffer_Ref (Ms.Buf); end if; -- Deallocate the processed buffer AIP.Buffers.Buffer_Blind_Free (Buf); exit when Ms.Buf = AIP.Buffers.NOBUF; -- Pointer "points" to Next element to copy Pointer := Pointer + Item_Size + 1; if Pointer > Data'Last then Raise_Exception (Layout_Error'Identity, "Invalid pointer" ); end if; end; end loop; -- Signal TCP layer that we can accept more data AIP.TCP.TCP_Recved (Pcb, TPlen); pragma Warnings (Off, """Err"" modified by call, *"); RAW_MQTT_Dispatcher.MQTT_Event (PCB => Pcb, DATA => Data, Cbid => Ms.App_Client_Cbs.App_Receive_Cb, Err => Err); pragma Warnings (On, """Err"" modified by call, *"); end; end Mqtt_Dispatch_Recv; 这是我学习的项目的一部分(一种方式好或坏?)如何写入Stream_Element_Array。在循环结束时,数据以接收到的字节结束,因此回调完成。 Mqtt客户端发送 Mqtt堆栈对事件做出反应,“主”代码启动TCP连接,然后它遵循显然需要发送TCP段的Mqtt Connect命令,因此我们创建了将在mqtt包中使用的发送过程是要发送的数据的Stream_Element_Array。代码位于mqtt.adb中,如下所示。 procedure Send (Pier : in out MQTT_Pier; Data : Stream_Element_Array ) is Pointer : Stream_Element_Offset := Data'First; size : AIP.Buffers.Data_Length; BufLen : AIP.Buffers.Buffer_Length; p : AIP.Buffers.Buffer_Id; dstAdd : System.Address; q : AIP.Buffers.Buffer_Id; Item_Size : Stream_Element_Offset; begin size := AIP.U16_T (Data'Last); AIP.Buffers.Buffer_Alloc (0, size, AIP.Buffers.LINK_BUF, p); if p /= AIP.Buffers.NOBUF then q := p; loop BufLen := AIP.Buffers.Buffer_Len (q); Item_Size := Stream_Element_Offset (BufLen); declare type SEA_Pointer is access all Stream_Element_Array (1 .. Item_Size); dstPtr : SEA_Pointer; function As_SEA_Pointer is new Ada.Unchecked_Conversion (System.Address, SEA_Pointer); Data_Ptr : SEA_Pointer; begin dstAdd := AIP.Buffers.Buffer_Payload (q); dstPtr := As_SEA_Pointer (dstAdd); Data_Ptr := As_SEA_Pointer (Data (Pointer)'Address); -- copy the actual data using the streams access pointers dstPtr.all (1 .. Item_Size) := Data_Ptr.all (1 .. Item_Size); end; q := AIP.Buffers.Buffer_Next (q); -- check if there is one more buffer to fill exit when q = AIP.Buffers.NOBUF; -- Pointer "points" to Next element to copy Pointer := Pointer + Item_Size + 1; if Pointer > Data'Last then Raise_Exception (Layout_Error'Identity, "Invalid pointer" ); end if; end loop; declare Ms : Mqtt_Conn_State; for Ms'Address use Clients.MSP (Pier.Sid)'Address; begin Ms.Buf := p; Clients.Mqtt_Send (Ms.Pcb, Ms); end; else Pier.Err := ERR_MEM; end if; end Send; 不知何故是接收的逆操作,这次我们需要保留lwIP缓冲区来保存数据。 Mqtt计时器 最后但并非最不重要的是mqtt计时器有助于保持活动功能,更多信息请点击这里。keepalive是为每个连接的客户端实现的,代码如下。 ----------------------------------------------- -- MQTT_Timer -- CycleTick Timer Callback -- ----------------------------------------------- procedure MQTT_Timer (Id : Integer) with Refined_Global => (In_Out => (MSP)) is Sid : MS_Id := NOMS; begin null; for J in MSP'Range loop if MSP (J).Tmr_Id = Id then Sid := J; exit; end if; end loop; if Sid = NOMS then return; -- this should not happen end if; -- Sid Points to the Client Connection State Data that MQTT_Timer belongs case MSP (Sid).Kind is when MS_CONNECTING => MSP (Sid).Cyclic_Tick := MSP (Sid).Cyclic_Tick + 1; if MSP (Sid).Cyclic_Tick * MQTT_CYCLIC_TIMER_INTERVAL >= MQTT_CONNECT_TIMOUT then -- Disconnect TCP Mqtt_Close (MSP (Sid).Pcb, MSP (Sid)); Timers.Timer_Stop (Id); end if; when MS_CONNECTED => -- keep_alive > 0 means keep alive functionality shall be used if MSP (Sid).Keep_Alive > 0 then MSP (Sid).Server_Watchdog := MSP (Sid).Server_Watchdog + 1; -- If reception from server has been idle for 1.5*keep_alive time, -- server is considered unresponsive if MSP (Sid).Server_Watchdog * MQTT_CYCLIC_TIMER_INTERVAL >= MSP (Sid).Keep_Alive + MSP (Sid).Keep_Alive / 2 then Mqtt_Close (MSP (Sid).Pcb, MSP (Sid)); Timers.Timer_Stop (Id); end if; if MSP (Sid).Cyclic_Tick * MQTT_CYCLIC_TIMER_INTERVAL >= MSP (Sid).Keep_Alive then -- Sending keep-alive message to server declare Ps : MQTT_Pier_Ptr; for Ps'Address use AIP.TCP.TCP_Udata (MSP (Sid).Pcb); begin Send_Ping (Ps.all); MSP (Sid).Cyclic_Tick := 0; end; else MSP (Sid).Cyclic_Tick := MSP (Sid).Cyclic_Tick + 1; end if; end if; when TCP_CONNECTING => MSP (Sid).Cyclic_Tick := MSP (Sid).Cyclic_Tick + 1; if MSP (Sid).Cyclic_Tick * MQTT_CYCLIC_TIMER_INTERVAL >= TCP_CONNECT_TIMOUT then -- Disconnect TCP Mqtt_Close (MSP (Sid).Pcb, MSP (Sid)); Timers.Timer_Stop (Id); end if; when others => -- Timer should not be running in this state - perhaps TCP_ABORT Timers.Timer_Stop (Id); end case; end MQTT_Timer; 请注意,它可能会关闭和无响应连接,并在需要时通过keep-alive发送Ping命令。 HTTP服务器 凭借更多的知识和以往的经验,我将向HTTP服务器移植。HTTP需要连接状态机和它的http状态机,它很大程度上依赖于gnat.Sockets.Server,幸运的是它是Simple Components源下的一个包。这个Server包有一个任务工作者,它使用gnat.sockets处理所有传入的连接,因此很大一部分代码不适用于lwIP方案,但是需要一些支持代码,所以我做的是将所有这些复制到一个名为RAW_LwIp_Server(raw_lwip_server.adb)的重命名包中,并且如果我不确定我在做什么,那就是明星评论代码,是非常疯狂的时刻只是绝望地摆脱所有编译器错误和警告。 最后我使用了60%的代码,但由于我没有测试过所有的HTTP代码,因此评论的原始代码仍然存在,所以基本上我在代码中生成了一个有效的Hello World.htm。我希望能够从SD卡加载.htm,但实际上我很难让SD卡文件系统在我的STM32F769I_DISCO上运行。 让我们讨论一下它的工作原理并进行最后的演示(是的,你可以先看看演示!) 在这里跨越每个细节是困难和无聊的,所以类似于mqtt解释,我将从演示代码开始并尝试尽可能多地提供详细信息。 你好,世界 该演示包含经典的hello world HTTP GET。包Test_HTTP_Servers包含所有代码,从“main”文件中我们只启动暂停等待标志的任务。名为worker的任务如下所示。 -- ------------------------------ -- Start the HTTP Server Loop. -- ------------------------------ procedure Start is begin Ada.Synchronous_Task_Control.Set_True (HTTP_Ready); end Start; task body Worker is timer_deadline : Ada.Real_Time.Time; begin -- Wait until started Ada.Synchronous_Task_Control.Suspend_Until_True (HTTP_Ready); AIP.IO.Put_Line ("HTTP Server Task Starting"); declare Factory : aliased Test_HTTP_Factory (Request_Length => 200, Input_Size => 1024, Output_Size => 1024, Max_Connections => 100 ); Server : Connections_Server (Factory'Access, Port_Type (80)); begin timer_deadline := Ada.Real_Time.Clock; loop Server.Poll_Connections; delay until timer_deadline; timer_deadline := timer_deadline + HTTP_Polling; end loop; end; end Worker; 工厂和服务器类型是原始服务器移植的原因,它们提供了一种简单而干净的方式来创建服务器。Connections_Factory重新定义为: type Connections_Factory is new Ada.Finalization.Limited_Controlled with record Port : Natural; Socket_SP : Socket_State_Access_Array; end record; 类似于之前看到的状态池,Socket_State_Access_Array是这些池的数组。 type Socket_State is record Kind : State_Kind; Pcb : AIP.PCBs.PCB_Id; Buf : AIP.Buffers.Buffer_Id; Err : AIP.Err_T; Server_Cbs : Socket_Server_Cbks; Client : Connection_Ptr; end record; type Socket_State_Access is access all Socket_State; type Socket_State_Array is array (Valid_SS_Id) of aliased Socket_State; type Socket_State_Access_Array is array (Valid_SS_Id) of Socket_State_Access; 这里我们有一个指针数组与服务池数组的一对一关系,而不是分配State_Pool指针。这与HTTP代码无关,因此其他服务器现在可以使用相同的Factory原语。 Connections_Server已减少为: type Connections_Server (Factory : access Connections_Factory'Class; Port : Port_Type ) is new Ada.Finalization.Limited_Controlled with record Clients : Natural := 0; Servers : Natural := 0; Postponed_Count : Natural := 0; Postponed : Connection_Ptr; Unblock_Send : Boolean := False; end record; 这里的技巧是当你实例化一个Connections_Server时,它将调用Initialize,因为我知道这是因为它是一种Ada.Finalization.Limited_Controlled,所以Initialize过程将覆盖默认过程,如下所示: overriding procedure Initialize (Listener : in out Connections_Server) is Err : AIP.Err_T; begin-- pragma Unreferenced (Err, Listener); Listener.Factory.Port := Integer (Listener.Port); API_Process.Initialize (Listener'Access, Listener.Factory, Err); if Err /= AIP.NOERR then AIP.IO.Put_Line ("Error Init Server"); end if; end Initialize; 为了不弄乱原始的Server文件,我创建了一个子包,其中包含底层已知的ipstack服务器,称为API_Process。该结构非常类似于之前看到的Echo_Server和MQTT_Client的混合,因为我们使用类似的调度程序来接收数据,但是当然有一些新的变化,让我们来讨论它们。(请注意,这是可用于其他协议服务器的通用服务器。) 初始化 我们来看看下面显示的Initialize代码。 ---------- -- Init -- ---------- procedure Initialize (Listener : access Connections_Server'Class; Factory : access Connections_Factory'Class; Err : out AIP.Err_T) is Pcb : AIP.PCBs.PCB_Id; Port : Integer; begin-- pragma Unreferenced (Pcb, Port, Err);-- pragma Unreferenced (Listener); Init_SS_Pool; lwIp_Listener := Listener.all'Unchecked_Access; lwIp_Factory := Factory.all'Unchecked_Access; Port := Integer (lwIp_Factory.Port); AIP.TCP.TCP_New (Pcb); if Pcb = AIP.PCBs.NOPCB then Err := AIP.ERR_MEM; else AIP.TCP.TCP_Bind (PCB => Pcb, Local_IP => AIP.IPaddrs.IP_ADDR_ANY, Local_Port => AIP.U16_T (Port), Err => Err); end if; if Err = AIP.NOERR then AIP.TCP.TCP_Listen (Pcb, Err); pragma Assert (AIP.No (Err)); -- Factory Socket State Pool is linked for Sid in lwIp_Factory.Socket_SP'Range loop lwIp_Factory.Socket_SP (Sid) := SSP (Sid)'Access; end loop; AIP.TCP.On_TCP_Accept (Pcb, RAW_TCP_Callbacks.To_CBID (SOCKET_Process_Accept'Access)); end if; end Initialize; 问题再一次是如何将lwIP状态池与应用层链接起来。在Ada语言中与我的无知之间的巨大斗争之后的答案是有一个指向Connections_Server和Factory的指针,现在代码只允许一个服务器,因为每个服务器只有一个实例,因此需要进一步调整才能拥有两个服务器。 访问类型声明为: lwIp_Listener : Connections_Server_Ptr; lwIp_Factory : access Connections_Factory'Class; 您可以看到分配是如何完成的,在服务器绑定和监听之后,状态池也被分配,这次由Sid索引。最后配置了Accept回调。Accept回调有新的东西需要发现。 procedure SOCKET_Process_Accept (Ev : AIP.TCP.TCP_Event_T; Pcb : AIP.PCBs.PCB_Id; Err : out AIP.Err_T) is-- Factory : constant access Connections_Factory'Class := ; Sid : SS_Id; Factory : access Connections_Factory'Class renames lwIp_Factory; begin Err := AIP.NOERR; SS_Alloc (Sid); if Sid = NOSS then Err := AIP.ERR_MEM; else Factory.Socket_SP (Sid).Kind := SS_ACCEPTED; Factory.Socket_SP (Sid).Pcb := Pcb; Factory.Socket_SP (Sid).Buf := AIP.Buffers.NOBUF; AIP.TCP.TCP_Set_Udata (Pcb, SSP (Sid)'Address); AIP.TCP.On_TCP_Sent (Pcb, RAW_TCP_Callbacks.To_CBID (SOCKET_Process_Sent'Access)); AIP.TCP.On_TCP_Recv (Pcb, RAW_TCP_Callbacks.To_CBID (SOCKET_Process_Recv'Access)); AIP.TCP.On_TCP_Abort (Pcb, RAW_TCP_Callbacks.To_CBID (SOCKET_Process_Abort'Access)); AIP.TCP.On_TCP_Poll (Pcb, RAW_TCP_Callbacks.To_CBID (SOCKET_Process_Poll'Access), 500); AIP.TCP.TCP_Accepted (Pcb); declare Data : Ada.Streams.Stream_Element_Array (1 .. 1); begin -- This is our way around to associate the Server Connection -- Pool to a given Pcb and thou fill specific client callbacks -- at the upper layer. Data (Data'First) := Ada.Streams.Stream_Element (Sid); RAW_SOCKET_Dispatcher.SOCKET_Event (Client => Factory.Socket_SP (Sid).Client, PCB => Pcb, DATA => Data, -- Not really used in upper layer let's see... Cbid => RAW_SOCKET_Callbacks.To_CBID (Do_Create'Access), Err => Err); if Factory.Socket_SP (Sid).Client = null then Err := AIP.ERR_MEM; Socket_Close (Pcb, SSP (Sid)); else declare This : Connection'Class renames Factory.Socket_SP (Sid).Client.all; begin This.Sid := Sid; This.Client := False; This.Connect_No := 0; This.Client_Address := Ev.Addr; This.Client_Port := Ev.Port; This.Err := AIP.NOERR; Clear (This); This.Listener := lwIp_Listener.all'Unchecked_Access; if This.Transport = null then -- Ready This.Session := Session_Connected; Connected (This); else This.Session := Session_Handshaking; end if; Err := This.Err; end; end if; end; end if; end SOCKET_Process_Accept; 如果套接字状态分配顺利,我们设置回调并像以前一样接受连接,我们也修改状态池,但是请注意我们从现在开始使用工厂的访问指针。此后回调到父服务器包,此回调是原始代码用来利用的gnat.Sockets的基础覆盖行为的替代,因此回调实际上是在下面显示的RAW_lwip_server中调用Create过程。 procedure Do_Create (Client : in out Connection_Ptr; Pcb : AIP.PCBs.PCB_Id; DATA : Stream_Element_Array; Err : out AIP.Err_T) is begin Client := Create (Factory => API_Process.lwIp_Factory, -- API_Process.Server'Access, Pcb => Pcb, Data => DATA, Err => Err); end Do_Create; function Create (Factory : access Connections_Factory; Pcb : AIP.PCBs.PCB_Id; Data : Stream_Element_Array; Err : out AIP.Err_T ) return Connection_Ptr is begin pragma Unreferenced (Factory, Pcb, Data, Err); -- Default Implementation returns null. return null; end Create; 正如您在默认情况下所看到的那样,它什么都不做,但Test_HTTP_Servers中的重写Create过程代替以下内容执行: overriding function Create (Factory : access Test_HTTP_Factory; Pcb : AIP.PCBs.PCB_Id; Data : Stream_Element_Array; Err : out AIP.Err_T ) return Connection_Ptr is Result : Connection_Ptr; begin pragma Unreferenced (Pcb, Data); -- if Get_Clients_Count (Listener.all) < Factory.Max_Connections then Result := new Test_HTTP_Client (Request_Length => Factory.Request_Length, Input_Size => Factory.Input_Size, Output_Size => Factory.Output_Size ); -- Receive_Body_Tracing (Test_Client (Result.all), True); -- Receive_Header_Tracing (Test_Client (Result.all), True); Err := AIP.NOERR; return Result; end Create; 在此Create函数下创建HTTP客户端,并在返回到lwIP的Accept回调时完成对Pcb的最终绑定。请注意,我们检查客户端的空值,在这种情况下它无法创建它我们继续关闭连接,否则初始化完成,其中Ip和端口号是从收到的事件设置的。 接收调度过程非常相似,只会在这种情况下显示回调。请注意,它固定为始终调用Do_Receive。 RAW_SOCKET_Dispatcher.SOCKET_Event (Client => Ss.Client, PCB => Pcb, DATA => Data, Cbid => RAW_SOCKET_Callbacks.To_CBID (Do_Receive'Access), Err => Err); Do_Receive和Received类似于Do_Create,这次Received过程在Connections_State_Machine.HTTP_Servers包中被覆盖,因为它能够相应地分派请求。Test Server实现了更高级别的抽象,例如使用Do_Get过程响应hello world演示,这里仅供参考: overriding procedure Do_Get (Client : in out Test_HTTP_Client) is Status : Status_Line renames Get_Status_Line (Client); begin case Status.Kind is when None => null; when File => if Status.File = "hello.htm" then Send_Status_Line (Client, 200, "OK"); -- Response status line Send_Date (Client); -- Date header line Send_Server (Client); -- Server name Send_Connection (Client, False); Send_Content_Type (Client, "text/html"); -- Content type Accumulate_Body (Client, "<html><body>"); -- Begin content construction Accumulate_Body (Client, "<p>Hello world!</p>"); Accumulate_Body (Client, "</body></html>"); Send_Body (Client);-- Send_Body_Now (Client); -- Evaluate total length, send length end if; when URI => null; end case; end Do_Get; 我们尚未准备好进行演示。还记得创建的HTTP任务吗?原始代码使用gnat.Sockets来集合网络描述符,就像选择将在套接字上执行,准备好读取,准备好写入等等。当我最终弄清楚但实际上不适用于此时,它是如何工作的非常敏锐的,但是因为我们需要调整我们的代码,所以HTTP状态机有一种有趣的工作方式。每个HTTP客户端都有一个在HTTP_Server上定义的非常长记录的类型,但重要的是要注意的是,这是一种新类型的State_Machine,它实际上是新类型的Connection,我们在MQTT客户端期间陷入困境。但是现在我们使用Connection类型作为原始代码(这意味着可以改进MQTT客户端并使代理人活了一天......)。 我要强调的是,Connection类型有一个读取和写入缓冲区,HTTP(以及其他简单组件下的服务器)服务器通过网络读取和写入以发送数据,并且有一些切割器逻辑应用程序通知套接字任务(Worker)是要发送的数据,在Simple Components文档中,这称为取消阻止。应用程序调用类似于之前的示例代码,例如Send_Status_Line(Client,200,“OK”); 写入写入缓冲区,然后取消阻止“套接字”发送它。 当我意识到它时我会放弃但后来我认为ipstack服务器也可以使用“几乎”类似的机制来解锁“发送”。所以我向你展示了模仿这种原始代码或类似的Pool_Connections程序。 procedure Poll_Connections (Listener : in out Connections_Server) is fab : access Connections_Factory'Class; Block : Boolean; begin while Listener.Unblock_Send loop Listener.Unblock_Send := False; fab := Listener.Factory.all'Unchecked_Access; for Sid in fab.Socket_SP'Range loop if fab.Socket_SP (Sid).Kind = SS_RECEIVED then declare Client : Connection_Ptr renames fab.Socket_SP (Sid).Client; begin if not Client.all.Written.Send_Blocked then -- Time to Process this Client Buffer Write (Client.all, Block); if not Block then Listener.Unblock_Send := True; else Client.all.Written.Send_Blocked := True; if Client.all.Data_Sent then Data_Sent (Listener, Client); end if; end if; end if; end; end if; end loop; end loop; end Poll_Connections; 我设置这个程序是每隔几毫秒从工作人员的任务调用,没有玩很多,但它适用于50或20毫秒。在这里,您可以看到hello.htm GET 请求的tcpdump捕获(请注意,此捕获期间ARP缓存已过时)。 最后一个dns代码已经在github上了(链接下面) 代码使用类似于ada-enet的方法,它定义了一个Query数组,但是我使用连接对象而不是ada-enet的UDP套接字,如下所示。此方法使用Dns查询的私有数组访问指向Dns_List包中定义的Application Array的点。 type UdpClient is abstract new Object.Entity with record Udp_State_Pool : Dns_Conn_State_Access; Sid : DS_Id; Client_Address : AIP.IPaddrs.IPaddr; Err : AIP.Err_T; end record; protected type Request is procedure Set_Result (Addr : in AIP.IPaddrs.IPaddr; Time : in Net.Uint32); procedure Set_Status (State : in Status_Type); function Get_IP return AIP.IPaddrs.IPaddr; function Get_Status return Status_Type; function Get_TTL return Net.Uint32; private Status : Status_Type := NOQUERY; Ip : AIP.IPaddrs.IPaddr := 0; Ttl : Net.Uint32; end Request; type Query is new UdpClient with record Name : String (1 .. DNS_NAME_MAX_LENGTH); Name_Len : Natural := 0; Deadline : Ada.Real_Time.Time; Xid : Net.Uint16; Result : Request; end record; 在任何Resolve之前必须调用函数DNS.Init,初始化将查询数组传递给包DNS,以便正确设置访问指针。 procedure Init (Query_Array : access Dns_Query_Array) is begin for item in Dns_Queries'Range loop Dns_Queries (item) := Query_Array (item)'Access; end loop; Init_Done := True; end Init; 之后,Resolve可以与原始ada-enet非常相似。示例代码显示了一个等待DNS服务器响应的简单循环。以下是代码。 Dns_List.Queries (1).Resolve ("www.google.com"); -- This won't get thru since original code accept non-authoritative only-- Dns_List.Queries (1).Resolve ("x83vdeb2.localdeb.net"); declare Timeout : Natural := 0; begin timer_deadline := Ada.Real_Time.Clock; loop timer_deadline := timer_deadline + timer_delay; delay until timer_deadline; if Dns_List.Queries (1).Get_Status = NOERROR then if Dns_List.Queries (1).Get_Name'Length > 0 then AIP.IO.Put_Line (Dns_List.Queries (1).Get_Name); AIP.IO.Put_Line (To_String (Dns_List.Queries (1).Get_Ip)); exit; end if; elsif Timeout > 10 then AIP.IO.Put_Line ("DNS Lookup Timeout"); exit; else Timeout := Timeout + 1; end if; end loop; end; 我故意在上面的代码中留下关于本地查询的注释,这是因为我在我的开发机器上设置了dnsmasq来处理我连接发现板的接口。我发现有一个由ada-enet代码和Flags收到的检查,在这种情况下Flags值为0x8580,代码检查0x8100,这是因为域是本地的NDS Answer作为权威服务器,设置为在这种情况下使其成为0x85xx的位。 我还没有研究代码,知道它是否可以解析答案。重要的是,为了更完整的ipstack,还有一件事情可以处理(它仍然是ada-enet提供的DHCP和NTP)。 源代码和构建说明可以在github项目中找到,所以我们的想法是召集贡献者和测试人员。 已知的问题 应该有一个关于git项目的部分来讨论这个,但是我们在这里提一下项目的完整性。 正如我所评论的那样,lwIP被修改了,我尽了很大的压力来获得结果,但是需要更多的测试并且可能需要一些返工。 我注意到的一个问题是在运行一段时间后来自HTTP服务器的一些数据包以及几个似乎破坏状态机或客户端状态的查询。tcpdump.zip文件包含一些包含意外段的捕获,例如捕获FIN_ACK_RETRANS显示服务器如何成功处理多个HTTP GET查询,然后出现意外的FIN-ACK 重新传输。 来自服务器的类似意外段在另一个捕获中显示,这次是看似正常的GET HTTP查询期间的RST-ACK 段。 捕获显示巧合或不与意外的段如何服务器发送一些ICMP段,我注意到并注释执行那些Pings的代码部分,原始ipstack IP_Input过程在调度数据包时,如果它没有找到协议数据包属于它发送的ICMP_Reject数据包(请参阅IP_Input中的Dispatch_Upper过程)。这可能不是这里的情况,但是我的Linux机器上的几个数据包刚刚进入广播的MAC地址,也许ARP引擎在这里失败了。 我在STM ARP缓存中添加一个永久性条目的原因是因为我注意到当ARP处于中间时它无法进行后续的服务器响应,这里需要更多的测试,因为最后我还纠正了pbufs已经分配而不是免费的问题这可能与ARP有关,因为它在使用这种方式时会使用很多Unack_Queue。 碰巧使用TCP_New分配缓冲区,但是在开发过程中我以某种方式打破了TCP队列的机制,实际上我发现了一个问题,让我回到Linux主机上测试ipstack以确认问题是否存在,实际上有一个错误,我怀疑与缓冲区分配和队列有关,因为我为stm32 ipstack纠正了它,如果从一些Queue中取出一个缓冲区,你最好在那之后用缓冲区做一些事情。例如,TCP_Output从Send_Queue获取缓冲区,在发送段之后,它将缓冲区放在Unack_Queue中,因此后面的逻辑是,从Unack_Queue取出它们之后它们应该是Free,否则它们将存在于内存中,所以问题将会出现直到TCP_New失败才会引起状态机的制动。 您可以轻松确认ipstack错误,我花了一些时间让它在linux下工作,因为低级驱动程序通过操作系统调用来配置接口,所以我手动调整这些东西以使其工作。简单地说,有一个名为aip-config.ads的ipstack配置文件,静态分配的数据缓冲区数是Data_Buffer_Num这是32,只是做测试将它降低到5例如为了快速测试它,然后使用echo服务器尝试做五个“回声测试”,即连接,发送一些数据并断开连接,当然将以异常结束并且程序突然结束。我发现这个问题是因为嵌入式程序正在做同样的事情,然后经过长时间的调试会话后,我注意到新连接启动时的Unack_Queue状态持有一个总是增加的buffer_id数,这应该不会发生,因为断开连接应释放内存。 我在某处提到过来自Timer的Callbacks以及来自TCP堆栈的一般回调在返回之前不应该占用太多,MQTT_DELAY_EXAMPLEpcap捕获显示延迟的影响,你可以注意到等待Last_ack的服务器的FIN-ACK ,在这种情况下,从我们的MQTT客户端重新传输,但可以注意到然后传输ACK,在调试问题之后我注意到它是断开连接回调上的半主机打印。如你所知,半主机在这里非常糟糕,因为ST-Link没有很好的支持。 最后的想法 这个项目解决了几个问题或更好的说,Ada Library的几个缺失的组件,并留下了良好的路径继续开发,因为它是100%Ada代码,可以改进和扩展,但它的创新方式,除了解决那些缺乏领域它以切割方式合并不同的项目,由于它的性质很难在比赛的时间框架中进行测试和调试,所以我对所取得的成果感到满意。 我无法讨论有关实现的所有细节,但尝试覆盖重要的主题,所以我把一些关于移植到stm32的方面放在一边,例如,在源代码下有一个名为build的文件夹,它有一些代码来处理网络数据包碰巧这是使用xmlada生成的,这对于ARM版本是不可用的,所以诀窍是包括已经生成的文件。HTTP有一些包依赖,Ada.Calendar它不可用,我尽力填充骨架函数但实际上现在没有处理任何东西。我在github中共享代码,因为我相信协作和自由软件,毕竟我从现有的免费代码中获得了部分机会, 这个项目对我来说是一个非常好的挑战,因为我必须尽快学习Ada,并且不太习惯lwIP在开始得到一些结果之前每天学习几乎是新东西,但截止日期的压力有时会给你什么是需要这样做,所以我喜欢这样做,我很欣赏与我互动的人,特别感谢Fabien分享我的ipstack代码(这是远离谷歌视线的git上的一些模糊路径)和Stephane的git问题支持。 但也特别感谢我的妻子在这个项目的许多隔夜工作时间的支持。 |
微信公众号
手机版