由UPD报文想到的
一、udp的报文发送
udp在通常的应用中使用的比较少,可靠的协议通常使用TCP传输,对于的关注自然没有TCP多。尽管UDP具有不可靠的传输问题,和TCP相比,它有一个隐性的优点就是对于packet结构本身的完整性保持。严格意义上讲,这个属性并不是UDP协议单独完成的,而是由IP层完成,对于上层应用来说,具体在哪里无关紧要。
这个特性带来的一个好处就是可以不用处理两次报文之间的“粘包”问题。假设应用层通过两次系统调用发送了AAAA BB两个报文,如果使用UDP发送,对方可能收到1、BB AAAA, 2、 AAAA, 3、 BB 4、没有收到任何报文 四种情况,不论哪种情况,发送放发送的基本单位AAAA和BB得到了保持。对于这一点,TCP不能保证,在TCP接收报文时,参数中的长度为应用程序声称希望接收到的数据量,同样对于上面的发送例子,如果应用程序读入长度为5,则可能收到AAAAB的报文格式,如果BB这个报文在读取时还没有到来,此时read系统调用会阻塞在此次系统调用上。
再具体来说,如果网络两端通过UDP交互,请求和发送的每次交互都是使用特定格式,格式长度有上限(通常一个页面足够),此时使用UDP就可以保证双方通过一次recvfrom就一定能够收到一个完整的、单一的报文,应用层可以方便的将这个从操作系统中接收到的数据根据包开始的包类型字段转换为特定类型的指针,而不用考虑对于接收报文的定界问题。
二、TCP如何解决粘包问题
为了实现TCP交互中的定界,应用层必须自己确定一个包的真正大小。
先以最为简单的telentd为例看下。在busybox的telentd实现中,看不到telentd本身对于网络两端的数据做打解包处理,因此也就没有所谓的定界问题。这一点也不难理解,telentd本身不处理逻辑,它本身只进行连接管理,充其量是作为一个转发器,把用户输入发给bash,把bash输入发到socket,具体的逻辑都在bash中完成。
telnet这种简单也带来了问题,比方说如果客户端的窗口大小发生变化,此时服务器本地的bash没有办法知道这个事件,当用户使用readline的快捷键命令就会出现异常。对于这种情况,需要更为高级的协议来完成,这一点就有ssh协议来完成。当然ssh相对telent更多的是加入了安全的因素,窗口变化这种用户体验只是协议变更带来的一个副产品。
随便在网上下了开源的dropbear ssh服务,可以看到,其中对于定界的处理就是通过在每个包的开始添加包含了该包长度的字段,从而实现变长包的管理。这一点更广泛的说,也正是TCP/IP等协议实现的基本方法,每层协议都会有标准的、固定位置字段表示这个包的长度。下面是packet.c文件内读取包开始函数代码:
/* Function used to read the initial portion of a packet, and determine the
* length. Only called during the first BLOCKSIZE of a packet. */
/* Returns DROPBEAR_SUCCESS if the length is determined,
* DROPBEAR_FAILURE otherwise */
static int read_packet_init() {
……
/* now we have the first block, need to get packet length, so we decrypt
* the first block (only need first 4 bytes) */
buf_setpos(ses.readbuf, 0);
if (ses.keys->recv.crypt_mode->decrypt(buf_getptr(ses.readbuf, blocksize),
buf_getwriteptr(ses.readbuf, blocksize),
blocksize,
&ses.keys->recv.cipher_state) != CRYPT_OK) {
dropbear_exit("Error decrypting");
}
len = buf_getint(ses.readbuf) + 4 + macsize;
三、IP层对于包分割/重组的处理
当用户态请求发送的报文长度超过了网络可以发送的最大传入单位MTU时,此时IP层负责对该报文进行分割和重组,这一点是IP层除了路由之外的一个重要功能,它的主要代码位于内核中的ip_fragment.c中。
发送方分割函数
static inline int ip_finish_output(struct sk_buff *skb)
{
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
/* Policy lookup after SNAT yielded a new policy */
if (skb->dst->xfrm != NULL) {
IPCB(skb)->flags |= IPSKB_REROUTED;
return dst_output(skb);
}
#endif
if (skb->len > dst_mtu(skb->dst) && !skb_is_gso(skb))
return ip_fragment(skb, ip_finish_output2);
else
return ip_finish_output2(skb);
}
接收方重组函数
/*
* Deliver IP Packets to the higher protocol layers.
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER);
if (!skb)
return 0;
}
return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
RFC791对于IP报文的说明
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| IHL |Type of Service| Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Identification: 16 bits An identifying value assigned by the sender to aid in assembling the fragments of a datagram.
Flags: 3 bits Various Control Flags. Bit 0: reserved, must be zero Bit 1: (DF) 0 = May Fragment, 1 = Don't Fragment. Bit 2: (MF) 0 = Last Fragment, 1 = More Fragments.
其中IP_MF表示还有更多的分片过来,也就说此次收到的IP报文不是完整报文,skb->nh.iph->frag_off & htons(IP_OFFSET)非零只有对此次分包片中最后一个分片中有效,此时由于是最后一个分片,MF不再置位,只有包偏移量非零。
四、更加详细的重组过程
在一个完整的IP被接收之前,这些分片需要被先缓存在IP层,等待整个IP拼接完整之后返回给上层。这个缓存结构本身和skb、iph没有通过任何结构直接耦合,而是通过全局的ipq_hash变量查找。对于一个IP报文,通过(id, saddr, daddr, protocol)几个变量确定该IP上所有的IP分包碎片,每一个未完成的IP报文对应一个
/* Describe an entry in the "incomplete datagrams" queue. */
struct ipq
在struct sk_buff *ip_defrag(struct sk_buff *skb, u32 user)函数中
if (qp->last_in == (FIRST_IN|LAST_IN) &&
qp->meat == qp->len)
ret = ip_frag_reasm(qp, dev);
当满足
qp->last_in == (FIRST_IN|LAST_IN)
时,标志第一个分片和最后一个分片都已经接收到,此时可以尝试进行重新组合出一个完整的ip报文。
五、UDP发送方对于包完整性的保持
udp_sendmsg
int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
do_append_data: up->len += ulen; getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag; err = ip_append_data(sk, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, rt, corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags); if (err) udp_flush_pending_frames(sk); else if (!corkreq) err = udp_push_pending_frames(sk); else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
在udp_push_pending_frames函数中,会将当前sk->sk_write_queue累积的所有sk_buff组合为一个IP包发送除去,这些sk->sk_write_queue引导的IP报文不能超过网络MTU限制,而一个IP包的最大发送单位是65535字节,可以在push中拼成一个更大的包一次性发送给IP来进行分片。
在分包发送函数中,它遍历的也不是包的sk_buff列表,而是另一个skb_shinfo(skb)->frag_list结构。
六、UDP接收方对于原子性的保持
try_again: skb = skb_recv_datagram(sk, flags, noblock, &err);
……
copied = skb->len - sizeof(struct udphdr); if (copied > len) { copied = len; msg->msg_flags |= MSG_TRUNC; }
……
err = copied; if (flags & MSG_TRUNC) err = skb->len - sizeof(struct udphdr); out_free: skb_free_datagram(sk, skb); out: return err;
在skb_recv_datagram函数中,如果接收设置了MSG_PEEK标志,增加该dgram的引用计数,从而避免在最后skb_free_datagram中删除报文,否则在用户执行了recvfrom之后,该报文被删除,包括未被接收的数据一同从系统中丢失。
七、当发送报文大于65535时如何保证包的原子性
当大于该值后,数据已经超过了一个IP包的最大长度,此时无法保证一个packet接收的完整性,对于这种情况,系统调用直接返回错误
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
if (len > 0xFFFF) return -EMSGSIZE;
八、TCP接收时对于数据处理
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len)
if (copied >= target) { /* Do not sleep, just process backlog. */ release_sock(sk); lock_sock(sk); } else sk_wait_data(sk, &timeo); //当接收到的数据不能满足用户态请求数据时,此时进行阻塞等待。
…… found_ok_skb: /* Ok so how much can we use? */ used = skb->len - offset; if (len < used) used = len;
……
if (!(flags & MSG_TRUNC)) { #ifdef CONFIG_NET_DMA …… #endif { err = skb_copy_datagram_iovec(skb, offset, msg->msg_iov, used); if (err) { /* Exception. Bailout! */ if (!copied) copied = -EFAULT; break; } } }
大家不要惊诧,tcp socket也可以通过recvmsg API来接收报文,当设置了MSG_TRUNC时,整包数据都被丢弃。man 7 tcp对于该行为的说明:
Sockets API flag. Since version 2.4, Linux supports the use of MSG_TRUNC in the flags argument of recv(2) (and recvmsg(2)). This flag causes the received bytes of data to be discarded, rather than passed back in a caller-sup- plied buffer. Since Linux 2.4.4, MSG_PEEK also has this effect when used in conjunction with MSG_OOB to receive out-of-band data.