LwIP之TCP

来源:互联网 发布:个体网络销售营业执照 编辑:程序博客网 时间:2024/06/05 22:42

TCP协议理论支撑

TCP控制块

和UDP控制块类似,对TCP的操作实际上就是对TCP结构中各个字段的操作。

//11种状态枚举定义enum tcp_state {  CLOSED      = 0,//没有连接  LISTEN      = 1,//服务器进入侦听,等待客户机连接  SYN_SENT    = 2,//连接请求发送,等待确认  SYN_RCVD    = 3,//已收到对方的连接请求  ESTABLISHED = 4,//连接已建立  FIN_WAIT_1  = 5,//程序已关闭该连接  FIN_WAIT_2  = 6,//另一端已接受关闭该连接  CLOSE_WAIT  = 7,//等待程序关闭连接  CLOSING     = 8,//两端同时收到关闭请求  LAST_ACK    = 9,//服务器等待对方接受关闭操作  TIME_WAIT   = 10//关闭成功,等待网络中可能出现的剩余数据};

TCP编程函数

函数就是对TCP控制块进行操作,填充相关的字段而已。
这里写图片描述

/*为连接分配一个TCP控制块结构,并进行相应的初始化。*/struct tcp_pcb *tcp_new(void){  return tcp_alloc(TCP_PRIO_NORMAL);}/** 绑定本地IP地址及端口号 * Binds the connection to a local portnumber and IP address. If the * IP address is not given (i.e., ipaddr == NULL), the IP address of * the outgoing network interface is used instead. * * @param pcb the tcp_pcb to bind (no check is done whether this pcb is already bound!)   * @param ipaddr the local ip address to bind to (use IP_ADDR_ANY to bind *        to any local address * @param port the local port to bind to * @return ERR_USE if the port is already in use *         ERR_VAL if bind failed because the PCB is not in a valid state *         ERR_OK if bound */err_ttcp_bind(struct tcp_pcb *pcb, ip_addr_t *ipaddr, u16_t port){  int i;  int max_pcb_list = NUM_TCP_PCB_LISTS;  struct tcp_pcb *cpcb;  LWIP_ERROR("tcp_bind: can only bind in state CLOSED", pcb->state == CLOSED, return ERR_VAL);#if SO_REUSE  /* Unless the REUSEADDR flag is set,     we have to check the pcbs in TIME-WAIT state, also.     We do not dump TIME_WAIT pcb's; they can still be matched by incoming     packets using both local and remote IP addresses and ports to distinguish.   */  if (ip_get_option(pcb, SOF_REUSEADDR)) {    max_pcb_list = NUM_TCP_PCB_LISTS_NO_TIME_WAIT;  }#endif /* SO_REUSE */  if (port == 0) {    port = tcp_new_port();    if (port == 0) {      return ERR_BUF;    }  }  /* Check if the address already is in use (on all lists) */  for (i = 0; i < max_pcb_list; i++) {    for(cpcb = *tcp_pcb_lists[i]; cpcb != NULL; cpcb = cpcb->next) {      if (cpcb->local_port == port) {#if SO_REUSE        /* Omit checking for the same port if both pcbs have REUSEADDR set.           For SO_REUSEADDR, the duplicate-check for a 5-tuple is done in           tcp_connect. */        if (!ip_get_option(pcb, SOF_REUSEADDR) ||            !ip_get_option(cpcb, SOF_REUSEADDR))#endif /* SO_REUSE */        {          if (ip_addr_isany(&(cpcb->local_ip)) ||              ip_addr_isany(ipaddr) ||              ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {            return ERR_USE;          }        }      }    }  }  if (!ip_addr_isany(ipaddr)) {    pcb->local_ip = *ipaddr;  }  pcb->local_port = port;  TCP_REG(&tcp_bound_pcbs, pcb);  LWIP_DEBUGF(TCP_DEBUG, ("tcp_bind: bind to port %"U16_F"\n", port));  return ERR_OK;}/** * Set the state of the connection to be LISTEN, which means that it * is able to accept incoming connections. The protocol control block * is reallocated in order to consume less memory. Setting the * connection to LISTEN is an irreversible process. * * @param: pcb the original tcp_pcb  * @param: backlog the incoming connections queue limit   用户参数,这里没有使用到 * @return tcp_pcb used for listening, consumes less memory.指向处于侦听状态的控制块 * * @note The original tcp_pcb is freed. This function therefore has to be *       called like this: *             tpcb = tcp_listen(tpcb); * 将某个绑定的控制块置为侦听状态。进入LISTEN状态后,服务器等待客户机连接,内核收到SYN报文,遍历listenpcbs链表,找到与报文匹配的IP地址目的端口控制块。 */struct tcp_pcb *tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog);/** * Connects to another host. The function given as the "connected" * argument will be called when the connection has been established. * * @param: pcb the tcp_pcb used to establish the connection  * @param: ipaddr the remote ip address to connect to服务器IP地址 * @param: port the remote tcp port to connect to服务器端口号 * @param: connected callback function to call when connected (or on error)当SYN报文得到服务器正确相应,该函数会被调用。 * @return ERR_VAL if invalid arguments are given *         ERR_OK if connect request has been sent *         other err_t values if connect request couldn't be sent * 控制块连接函数,主动打开,向服务器发送SYN握手报文段。 */err_ttcp_connect(struct tcp_pcb *pcb, ip_addr_t *ipaddr, u16_t port,      tcp_connected_fn connected);/** * Write data for sending (but does not send it immediately). * * It waits in the expectation of more data being sent soon (as * it can send them more efficiently by combining them together). * To prompt the system to send data now, call tcp_output() after * calling tcp_write(). * * @param: pcb Protocol control block for the TCP connection to enqueue data for. * @param: arg Pointer to the data to be enqueued for sending.待发送数据的起始地址 * @param: len Data length in bytes 待发送数据长度 * @param:apiflags combination of following flags : * - TCP_WRITE_FLAG_COPY (0x01) data will be copied into memory belonging to the stack * - TCP_WRITE_FLAG_MORE (0x02) for TCP connection, PSH flag will be set on last segment sent, 数据是否进行拷贝,以及报文段首部是否设置了PSH标志 * @return ERR_OK if enqueued, another err_t on error * 连接向另外一方发送数据,构造报文并放入控制块缓冲队列中 */err_ttcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags);//处理接收数据,那么就用用户自己定义的回调函数处理,这点必须明白。/** * Closes the connection held by the PCB. * * Listening pcbs are freed and may not be referenced any more. * Connection pcbs are freed if not yet connected and may not be referenced * any more. If a connection is established (at least SYN received or in * a closing state), the connection is closed, and put in a closing state. * The pcb is then automatically freed in tcp_slowtmr(). It is therefore * unsafe to reference it (unless an error is returned). * * @param pcb the tcp_pcb to close * @return ERR_OK if connection has been closed *         another err_t if closing failed and pcb is not freed * 内核会根据当前控制块的不同状态表现出不同的处理方式:当控制块处于CLOSED状态时,控制块将在tcp_bound_pcbs链表中被删除,相应的空间被释放;当控制块处于LISTEN状态时,控制块会在tcp_listen_pcbs链表中被删除,相应的空间被释放,控制块再也不能侦听任何客户端的连接请求;当控制块处于SYN_SENT状态时,控制块会在tcp_active_pcbs链表中被删除,相应的空间被释放;当控制处于其他状态时,一个FIN握手报文将被发送给连接的另一方,同时,连接的状态会按照TCP状态图做相应的转换。在TCP定时处理函数中,将回收那些成功关闭的控制块空间。 */err_ttcp_close(struct tcp_pcb *pcb);/*应用程序数据处理完毕,通知内核更新接收窗口*/void tcp_recved(struct tcp_pcb *pcb, u16_t len);/*向控制块注册回调函数,收到数据时候被回调*/voidtcp_recv(struct tcp_pcb *pcb, tcp_recv_fn recv);/*向控制块注册回调函数,数据发送成功被调用*/voidtcp_sent(struct tcp_pcb *pcb, tcp_sent_fn sent);/*控制块注册回调函数,遇到错误的时候被回调。*/voidtcp_err(struct tcp_pcb *pcb, tcp_err_fn err);/*控制块注册回调函数,侦听到连接时候,被回调。*/voidtcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept);/*向控制块注册轮询函数,该函数周期性被调用*/voidtcp_poll(struct tcp_pcb *pcb, tcp_poll_fn poll, u8_t interval);/*向控制块注册用户数据,void类型,可以传递进去任意指针。上述各个函数被回调的时候,该指针传递给对应的函数,在函数里面可以设置用户数据的值,然后用户在应用程序里面可以明确哪些函数被回调了,也就是内核做了哪些处理。这种回调传参的形式可以学习,很有用。各种内核里面用了很多。*/void tcp_arg(struct tcp_pcb *pcb, void *arg);

可靠的传输服务

TCP正因为有了这些常见机制的原理和实现,才可以称之为可靠的传输协议。

超时重传与RTT估计

在TCP两端交互过程中,包含数据、确认的报文段有可能丢失,在发送端引入超时和重传机制可以很好解决报文丢失的问题。发送端为每个发送出去的报文设置一个超时定时器,当定时器溢出而报文的确认还没有返回,它就重传该报文段。对各种TCP协议的实现而言,怎样决定超时间隔和如何确定重传的方式是提高TCP性能的关键,而这些都与往返时间估计密切相关。往返时间(RTT) ,代表了某字节数据发出到对应确认返回其间的时间间隔。

慢启动和拥塞避免

慢启动:发送方一开始便可以向网络发送多个报文段,直到填满接收方通告的窗口空间。当发送方和接收方处于同一个局域网时,这种方式可以工作得很好,但是如果在发送方和接收方之间存在多个路由器或速率较慢的链路时,就有可能产生一些问题。在网络中,路由器负责缓存和转发来自于众多主机的数据报,由于路由器的存储能力、转发能力各不相同,主机的数量和数据报流量也会动态变化,所以网络中会出现拥塞的现象。
当拥塞发生时,路由器会直接丢弃掉来不及处理的数据报。对于 TCP 报文段的发送方而言,拥塞仅仅表现为时延的增加(超时),它并不知道拥塞发生在哪里,以及发生拥塞的原因。很多 TCP 协议的实现对时延增加的处理方式就是重传报文段,而这种处理方式只会使更多的数据发送到路由器上,这不可避免的出现了一种恶性循环,最终导致网络的崩溃。
要解决这种问题, TCP 发送方必须主动减少报文段的发送速率,也就是慢启动算法。该算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。慢启动为发送方的TCP增加了另一个窗口:拥塞窗口 (congestion window),记为cwnd。 当与网络的另一台主机建立TCP连接时,阻塞窗口被初始化为1个报文段大小,发送方取阻塞窗口与通告窗口两者中的最小值作为发送上限。每收到一个ACK,阻塞窗口增加1一个报文段大小。在没有拥塞的连接上,稳定状态下的阻塞窗口和发送窗口一样大,而当拥塞发生时,阻塞窗口会减小,从而减少TCP发送的流量。阻塞窗口是发送方使用的流量控制,而窗口通告则是接收方使用的流量控制。当拥塞发生时,我们希望降低分组进入网络的传输速率,发送方通过慢启动与拥塞避免算法主动调节分组进入网络的速率,同时发送方也通过接收方通告的窗口大小来被动调节分组发送速率。

快速重传与快速恢复

在收到一个失序的报文段时, TCP立即需要产生一个ACK(一个重复的ACK)。这个重复的ACK不应该被迟延。该重复的ACK的目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。由于我们不知道一个重复的 ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的 A C K到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的 ACK之前,只可能产生1 ~ 2个重复的ACK。如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出,这就是快速重传算法,快速重传算法能在具有偶尔丢失的环境中达到很高的吞吐量。推出快速重传模式以后,阻塞窗口不会像超时那样被设置为1,相反,阻塞窗口被设置为 ssthr官sh,连接直接进入拥塞避免阶段,进入拥塞避免算法,这就是快速恢复算法。

糊涂窗口与避免

基于窗口进行流量控制,会导致一种被称为”糊涂窗口综合症 SWS (Silly Window Syndrome)”的状况。当TCP接收方通告了一个小窗口并且TCP发送方立即发送数据填充该窗口时, SWS就会发生。糊涂窗口综合症是一种能够导致网络性能严重下降的TCP现象,因为小单元报文段中IP首部和TCP首部这些字段占了大部分空间,而真正的TCP数据却很少。如果TCP的双方都是以小窗口通告和小报文段发送来实现通信,则会使TCP数据流包含很多非常小的报文段,而不是满长度的报文段,小报文的传输浪费了网络的大量带宽。

SWS可能由TCP连接双方中的任何一方引起。这是由于接收方可以通告一个小的窗口(而不是一直等到有大的窗口时才通告),而发送方也可以发送少量的数据(而不是等待更多的数据以便发送一个大的报文段)。为了避免SWS的发生,在发送方和接收方必须设法消除这种情况。接收方不必通告小窗口更新,而发送方不必发送小的报文段。在任何一方采取措施,都可以消除糊涂窗口综合症的发生。

零窗口探查机制

TCP接收方通过通告窗口大小来告诉发送方自己可以接收的数据多少,接收方采用这种方式来实现流量控制。假如接收方通告的窗口大小为0 ,这将完全阻止发送方的数据传输,直至通告窗口变为非0。所以TCP必须能够处理新的含非0窗口通告的报文段丢失的情况,通常这个非0窗口通告在一个不含任何数据的ACK报文中发送。ACK的传输并不可靠,也就是说,接收方不对ACK报文段进行确认(很明显,也就不会存在该ACK报文段的重发),TCP只确认那些包含有数据或SYN、FIN标志的ACK报文段。
如果一个非0窗口通告丢失了,则双方就有可能因为等待对方而使连接异常,接收方等待接收数据(因为它己经向发送方通告了一个非0的窗口),而发送方在等待允许它发送数据的非0窗口更新(因为刚刚丢失了报文)。为防止这种死锁情况的发生,发送方使用一个坚持定时器( persist timer)来周期性地向接收方查询,以便发现窗口是否已增大,这些从发送方发出的报文段称为零窗口探查。

保活机制

如果一个TCP连接己处于稳定状态,而同时双方都没有数据需要发送,则在这个连接之间不会再有任何信息交互。然而在很多情况下,连接双方都希望知道对方是否仍处于活动状态。常见的状况是,服务器希望知道客户主机是否仍在线,如果客户主机崩横或重启,那么原来的有效连接将变为无效。许多TCP/IP实现中,都提供了保活定时器来实现这种检测功能。
TCP必须为服务器应用程序提供保活功能,服务器通常希望知道客户主机的运行状况,从而可以合理分配客户占用的资源。如果某条连接在两个小时之内没有任何动作,则服务器就向客户端发送一个保活探查报文。此时,客户主机可能处于下述4个状态之一:
(1)客户主机依然正常运行,且从服务器仍可达。此时,当收到探查报文后,客户主机的TCP正常响应(返回ACK),服务器从客户主机的返回中可以判断对方仍工作正常。服务器将保活定时器复位,并在两小时以后重新发送探查报文,若在两小时定时到达之前此连接上出现数据流动,则保活定时器复位。
(2)客户主机己经崩溃,并且关闭或者正在重新启动,在这些情况下,客户主机都不会有任何响应。服务器也不可能收到探查报文的响应,此后,服务器还会发送9个这样的探查报文,每个报文间隔均为75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
(3)客户主机崩溃己经重新启动。此时客户主机会收到探查报文,但客户主机会判定当前连接无效,并向服务器返回一个复位报文。服务器收到复位报文后,结束该连接。
(4)客户主机正常运行,但是从服务器不可达(通信故障)。这里的处理过程与情况。)相同,服务器因为多次发送探查报文,但不能收到任何响应,最后终止连接。
在第(1)种情况下,服务器应用程序并不能感觉到保活探查的发生, TCP负责了所有的工作,该过程对应用程序是不可见的。当第(2)、(3)或(4)种情况发生时,服务器应用程序将收到来自TCP层的差错报告,在第(2)种情况下,差错是诸如”连接超时”之类的信息,而在第(3)种情况则为”连接被对方复位”,第(4)种情况看起来像是连接超时,也可根据是否收到与连接有关的ICMP差错报文,以此判断是否发生路由超时错误。

TCP定时器

重传定时器、坚持定时器和保活定时器,这些都是在500ms定时函数 tcp_slowtmr中完成的,此外这个函数中还有几个定时器我们还未涉及。总的来说,TCP为每条连接总共建立了7个定时器,依次为:
(1)连接建立 (connection establishment) 定时器在服务器响应一个 SYN握于报文并试图建立一条新连接时启动。此时服务器已经发送自己的SYN+ACK,并处于SYN_RCVD等待对方ACK的返回,如果在75秒内没有收到响应,连接建立将中止。这一点也是服务器处理SYN攻击的有效手段。
(2)重传 (retransmission) 定时器在TCP发送某个报文段时设定。如果该定时器超时而对端的确认还未到达,TCP将重传报文段。重传间隔(即 TCP等待对端确认的时间)是动态计算的,与RTT的估计值密切相关,且还取决于报文段己被重传的次数。
(3)数据组装定时器在接收缓冲队列ooseq不为空时有效,从前面的讲解知道,该队列中的都是失序报文段,如果连接上很长时间内都没有数据交互,但是ooseq上还有失序的报文,则相应的报文需要队列中删除。
(4)坚持( persist) 定时器在对方通告接收窗口为0,阻止TCP继续发送数据时设定。由于对方发送的非0窗口通告不可靠(只有数据才会被确认, ACK 不会被确认),允许TCP继续发送数据的窗口更新有可能丢失。因此,如果TCP有数据要发送,但对方通告接收窗口为0时,则启动坚持定时器。定时器超时后,将向对方发送1字节的数据,判定对方接收窗口是否己打开。
(5)保活(keep alive) 定时器在TCP控制块的so_options字段设置了SOF_KEEPALIVE选项时生效。如果连接的连续空闲时间超过2小时,则保活定时器超时,此时应向对方发送保活探查报文,强迫对方响应。如果收到了期待的响应,TCP可确定对方主机工作正常,在该连接再次空闲超过2小时之前,TCP不会再进行保活测试;如果收到的是RST复位晌应,TCP可确定对端主机已重启,如果连续若干次保活测试都未收到响应,TCP就假定对端主机己崩溃,但它无法区分是主机故障还是连接故障。
(6) F_WAIT_2定时器,当某个连接从FIN_WAIT_1状态变迁到FIN_WAIT_2状态并且不能再接收任何新数据时,FIN_WAIT_2定时器启动。定时器超时后连接被关闭。加入这个定时器的目的是为了避免对方一直不发送FIN,某个连接会永远滞留在FIN_WAIT_2状态 (LwIP 不支持半打开功能)。
(7) TIME_WAIT定时器,一般也称为2MSL定时器。 2MSL指两倍的 MSL,即最大报文段生存时间。当连接转移到 TIME_WAIT状态,即连接主动关闭时,定时器启动。在LwIP中,连接进入TIME_WAIT状态时,定时器设定为2分钟,超时后,TCP控制块被删除,端口号可重新使用。同样,服务器端在断开连接过程中会处于LAST_ACK状态等待对方ACK的返回,如果在该状态下的2MSL时间内未收到对方的响应,连接也会被立即关闭。

LwIP 中包括两个定时器相关函数:一个是上述周期为500ms的慢定时器函数tcp_ slowtmr,它完成了基本所有TCP需要实现的定时功能;第二个是周期为 250ms 的快定时器函数tcp_ fasttmr,它完成的一个重要功能是让连接上被延迟的ACK立即发送出去,同时,未被成功递交的数据也在这里被递交。为了实现TCP的功能, TCP的上述两个定时函数需要被周期性调用,在没有操作系统模拟层的支持下,用户需要以250ms为周期调用tcp_tmr。

原创粉丝点击