网编(20):UDP传输数据经常遇到的问题

相对于TCP 协议的程序设计, UDP 协议的程序虽然程序设计的环节要少一些,但是由于UDP 协议缺少流量控制等机制,容易出现一些难以解决的问题。UDP 的报文丢失、报文乱序、connect()函数、流量控制、外出网络接口的选择等是比较容易出现的问题。

1 UDP 报文丢失数据

利用UDP 协议进行数据收发的时候,在局域网内一般情况下数据的接收方均能接收到发送方的数据,除非连接双方的主机发生故障, 否则不会发生接收不到数据的情况。

1. UDP报文的正常发送过程

而在Internet 上,由于要经过多个路由器, 正常情况下一个数据报文从主机C 经过路由器A、路由器B 、路由器C 到达主机S, 数据报文的路径如图所示。主机C 使用函数sendto()发送数据,主机S 使用recvfrom()函数接收数据,主机S 在没有数据到来的时候,会一直阻塞等待。

                                   

2. UDP报文的丢失

路由器要对转发的数据进行存储、处理、合法性判定、转发等操作,容易出现错误,所以很可能在路由器转发的过程中出现数据丢失的现象,如图所示。当UDP 的数据报文丢失的时候,函数recvfrom()会一直阻塞,直到数据到来。

                                  

如果客户端发送的数据丢失,服务器会一直等待直到客户端合法数据到来;如果服务器的响应在中间被路由器丢弃,则客户端会一直阻塞,直到服务器数据的到来。在程序正常运行的过程中是不允许出现这种清况的,所以可以设置超时时间来判断是否有数据到来。对千数据丢失的原因,并不能通过一种简单的方法获得,例如,不能区分服务器发给客户端的响应数据是在发送的路径中被路由器丢弃,还是服务器没有发送此响应数据。

3. UDP报文丢失的对策

UDP 协议中的数据报文丢失是先天性的,因为UDP 是无连接的、不能保证发送数据的正确到达。下图为TCP 的连接中发送数据报文的过程, 主机C 发送的数据经过路由器,到达主机S 后,主机S 要发送一个接收到此数据报文的响应,主机C 要对主机S 的响应进行记录,直到之前发送的数据报文l 已经被主机S 接收到。如果数据报文在经过路由器的时候, 被路由器丢弃,则主机C 和主机S 会对超时的数据进行重发。

                           

当我们重传的时候,主机S可能已经收到了,这样就会收到一样的数据,这里可以用一个序列号表示,搜到相同序列号的数据就丢掉。

2 UDP 数据发送中的乱序

UDP 协议数据收发过程中,会出现数据的乱序现象。所谓乱序是发送数据的顺序和接收数据的顺序不一致,例如发送数据的顺序为数据包A、数据包B 、数据包C, 而接收数据包的顺序变成了数据包B、数据包A、数据包C 。

1. UDP数据顺序收发的过程

如图所示, 主机C 向主机S 发送数据包0、数据包1 、数据包2、数据包3, 各个数据包途中经过路由器A、路由器B、路由器c, 先后到达主机s, 在主机S 端的循序仍然为数据包0、数据包1 、数据包2 、数据包3, 即发送数据时的顺序和接收数据时的顺序是一致的。

                                             

2. UDP数据的乱序

UDP 的数据包在网络上传输的时候,有可能造成数据的顺序更改, 接收方的数据顺序和发送方的数据顺序发生了颠倒。这主要是由千路由的不同和路由的存储转发的顺序不同造成的。

路由器的存储转发可能造成数据顺序的更改,如图所示。主机C 发送的数据在经过路由器A 和路由器C 的时候,顺序均没有发生顺序更改。而在经过主机B 的时候,数据的顺序由数据0123 变为了0312, 这样主机C 的数据0123 顺序经过路由器到达主机S的时候变为了数据0312 。

                             

UDP 协议的数据经过路由器时的路径造成了发送数据的混乱,如图所示。从主机C 发送的数据0123, 其中数据0 和3 经过路由器B、路由器C 到达主机s, 数据1 和数据2 经过路由器A 、路由器C 到达主机s, 所以数据由发送时的顺序0123 变成了顺序1032 。

                                 

3. UDP乱序的对策

对千乱序的解决方法可以采用发送端在数据段中加入数据报序号的方法,这样接收端对接收到数据的头端进行简单地处理就可以重新获得原始顺序的数据,如图所示。

                                           

3. UDP 协议中的connect()函数

UDP 协议的套接字描述符在进行了数据收发之后,才能确定套接字描述符中所表示的发送方或者接收方的地址,否则仅能确定本地的地址。例如客户端的套接字描述符在发送数据之前, 只要确定建立正确就可以了, 在发送的时候才确定发送目的方的地址: 服务器bind()函数也仅仅绑定了本地进行接收的地址和端口。

connect()函数在TCP 协议中会发生三次握手,建立一个持续的连接, 一般不用于UDP 。在UDP 协议中使用connect()函数的作用仅仅表示确定了另一方的地址,并没有其他的含义。

connect()函数在UDP 协议中使用后会产生如下的副作用:

  • 使用connect()函数绑定套接字后,发送操作不能再使用sendto()函数,要使用write()函数直接操作套接字文件描述符,不在指定目的地址和端口号。
  • 使用connect()函数绑定套接字后, 接收操作不能再使用recvfrom()函数, 要使用read()类的函数,函数不会返回发送方的地址和端口号。
  • 在使用多次connect()函数的时候,会改变原来套接字绑定的目的地址和端口号,用新绑定的地址和端口号代替,原有的绑定状态会失效。可以使用这种特定来断开原来的连接。

下面是一个使用connect()函数的例子, 在发送数据之前, 将套接字文件描述符与目的地址使用connect() 函数进行了绑定, 之后使用write()函数发送数据并使用read()函数接收数据。

static void udpclie_echo(int s, struct sockaddr*to)
{
	char buff[BUFF_LEN] = "UDP TEST";//向服务器端发送的数据可
	connect(s, to, sizeof(*to));//连接
	n = write(s, buff, BUFF_LEN);//发送数据
	read(s, buff, n);//接收数据
}

4 UDP 缺乏流量控制

UDP 协议没有TCP 协议所具有的滑动窗口概念,接收数据的时候直接将数据放到缓冲区中。如果用户没有及时地从缓冲区中将数据复制出来,后面到来的数据会接着向缓冲区中放入。当缓冲区满的时候, 后面到来的数据会覆盖之前的数据造成数据的丢失。

1. UDP缺乏流量控制概念

如图所示为UDP 的接收缓冲区示意图,共有8 个缓冲区, 构成一个环状数据缓冲区。起点为0 。
当接收到数据后, 会将数据顺序放入之前数据的后面,并逐步递增缓冲区的序号, 如图。

                           

当数据没有接收或者接收数据比发送数据的速率要慢,之前接收的数据被覆盖,造成数据的丢失,如图所示。

                                                                 

2. 缓冲区溢出对策

解决UDP 接收缓冲区溢出的现象需要根据实际情况确定, 一般可以用增大接收数据缓冲区和接收方接收单独处理的方法来解决局部的UDP 数据接收缓冲区溢出问题。例如在局部时间内发送方会爆发性地发送大量的数据,后面的时间则发送的数据会较小,由千在局部时间内接收方不能及时处理接收到的数据,会造成数据的丢失,如果增大缓冲区,则可以改善此问题。如果接收方的接收能力在绝对能力上要小于发送方,则接收方由千在处理能力或者容量方面的限制,造成数据肯定要丢失。

客户端的代码如下 ,先将发送计数的值打包进发送缓冲区,然后复制要发送的数据,再进行数据发送。每次发送的时候,计数器增加 1 。
 

#define PORT_SERV 8888                  //服务器端口
#define NUM_DATA 100                    //接收缓冲区数量
#define LENGTH 1024                     //单个接收缓冲区大小
static char buff_send[LENGTH];          //接收缓冲区

static void udpclie_echo(int s, struct sockaddr*to)
{
	char buff_init[BUFF_LEN] = "UDP TEST";//向服务器端发送的数据
	struct sockaddr_in from;              //发送数据的主机地址
	int len = sizeof (*to);               //地址长度
	int i = 0;//计数
	for(i = 0; i < NUM_DATA; i++)         //循环发送
	{
		*((int*)&buff_send[0]) = htonl(i);//将数据标记打包
		memcpy(&buff_send[4], buff_init, sizeof(buff_init));
		//数据复制到发送缓冲区
		sendto(s, &buff_send[0], NUM_DATA, 0 , to, len);//发送数据
	}
}

服务器端的代码如下,接收到发送方的数据后,判断接收到数据的计数器的值,将不同计数器的值放入缓冲区不同的位咒,在使用的时候可以判断一下计数器是否正确,即是否有数据到来,再进行使用。

#define PORT SERV 8888              /*服务器端口*/
#define NUM DATA 100                /*接收缓冲区数量*/
#define LENGTH 1024                 /*单个接收缓冲区大小*/ 
static char buff [NUM DATA][LENGTH];/*接收缓冲区*/
static udpserv_echo(int s, struct sockaddr*client)
{
	int n;                  /*接收数量*/
	char tmp_buff[LENGTH]; /*临时缓冲区*/
	int len;                /*地址长度*/
	while (1)/*接收过程*/
	{
		len = sizeof (*client);
		n = recvfrom(s, tmp_buff, LENGTH, 0, client, &len);
		                     /*接收数据放到临时缓冲区中*/
							 
		//根据接收到数据的头部标志,选择合适的缓冲区位置复制数据
		memcpy (&buff[ntohl(*( (int*) &buff[i][0]))][0], tmp_buff+4, n-4);
	}
}

5 UDP 协议中的外出网络接口

在网络程序设计的时候,有时需要设置一些特定的条件。 例如一个主机有两个网卡 ,由千不同的网卡连接不同的子网,用户发送的数据从其中的一个网卡发出,将数据发送到特定的子网上。使用函数 connect()可以将套接字文件描述符与一个网络地址结构进行绑定,
在地址结构中所设置的值是发送接收数据时套接字采用的 IP 地址和端口。下面的代码是一个例子:

#include <sys/types.h>
#include <sys/socket.h>
#include <Linux/in.h>
#include <string.h>
#include <stdio.h>
#define PORT_SERV 8888
int main(int argc, char•argv[])
{
	int s;//套接字文件描述符
	struct sockaddr in addr serv;//服务器地址
	struct sockaddr_in local;//本地地址
	int len = sizeof(local);
	s = socket(AF_INET, SOCK_DGRAM, 0);

	memset(&addr_serv, 0, sizeof(addr_serv));
	addr_serv.sin_family = AF_INET;
	addr_serv.sin_addr.s_addr = inet_addr("l27.0.0.l" ); 
	addr_serv.sin_port = htons(PORT_SERV);//服务器端口
	connect(s, (struct sockaddr*) &addr_serv, sizeof(addr_serv));
	//连接服务器
	getsockname(s, &local, &len);//获得套接字文件描述符的地址
	printf("UDP local addr:%s\n", inet_ntoa(local.sin_addr));
	close(s);
	return 0;
}

编译运行后其结果如下,系统将程序中的套接字描述符与本地的回环接口进行了绑定。
 

UDP local addr : 127.0. 0 .1

6 UDP 协议中的数据报文截断

当使用 UDP 协议接收数据的时候,如果应用程序传入的接收缓冲区的大小小千到来数据的大小时,接收缓冲区会保存最大可能接收到的数据,其他的数据将会丢失,并且有MSG_TRUNC 的标志 。

例如对函数 udpclie_echo()做如下修改,发送一个字符串后在一个循环中接收服务器端的响应,会发现只能接收—个 u, 程序阻塞到 recvfrom 函数中。这是因为服务器发送的字符串到达客户端后,客户端的第一次接收动作没有正确地接收到全部的数据,其余的数据已经丢失了 。

所以服务器和客户端的程序要进行配合,接收的缓冲区要比发送的数据大一些,防止数据丢失的现象发生 。

 

 

 

 

 

 

 

 

 

 

  • 16
    点赞
  • 94
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值