UDP进行程序设计可以分为客户端和服务器端两部分:
服务器端主要包含建立套接字、将套接字与地址结构进行绑定、读写数据、关闭套接字几个过程。
客户端包括建立套接字、读写数据、关闭套接字几个过程。
UDP协议的程序设计框架如下图所示,客户端和服务器之间的差别在于服务器必须使用bind()
函数来绑定侦听的本地UDP端口,而客户端则可以不进行绑定,直接发送到服务器地址的某个端口地址。
与TCP程序设计相比较,UDP缺少了connect()、listen()及accept()函数,这是用于UDP协议无连接的特性,不用维护TCP的连接、断开等状态。
①.UDP协议的服务器端流程
UDP协议的服务器端程序设计的流程分为套接字建立、套接字与地址结构进行绑定、收发数据、关闭套接字等过程,分别对应于函数socket()、bind()、sendto()、recvfrom ()和close()。
建立套接字过程使用socket()
函数,这个过程与TCP协议中的含义相同,不过建立的套接字类型为数据报套接字。
地址结构与套接字文件描述符进行绑定的过程中,与 TCP 协议中的绑定过程不同的是地址结构的类型。
当绑定操作成功后,可以调用 recvfrom()
函数从建立的套接字接收数据或者调用 sendto()
函数向建立的套接字发送网络数据。当相关的处理过程结束后,需要调用 close()函数关闭套接字。
②.UDP协议的客户端流程
UDP协议的客户端端程序设计的流程分为套接字建立、收发数据、关闭套接字等过程,分别对应于函数 socket()、sendto()、recvfrom()和close()。
建立套接字过程使用 socket()函数,这个过程与TCP 协议中的含义相同,不过建立的套接字类型为数据报套接字。
建立套接字之后,可以调用函数 sendto()
向建立的套接字发送数据或者调用 recvfrom()
函数从建立的套接字收网络数据。
当相关的处理过程结束后,需要调用 close()
函数关闭套接字。
③.UDP协议服务器和客户端之间的交互
UDP协议中服务器和客户端的交互存在于数据的收发过程中。
进行网络数据收发的时候,服务器和客户端的数据是对应的:客户端发送数据的动作,对服务器来说是接收数据的动作;客户端接收数据的动作,对服务器来说是发送数据的动作。
UDP协议服务器与客户端之间的交互,与TCP 协议的交互相比较,缺少了二者之间的连接。这是由于 UDP 协议的特点决定的,因为UDP 协议不需要流量控制、不保证数据的可靠性收发,所以不需要服务器和客户端之间建立连接的过程。
服务器流程主要分为下述6个部分,即建立套接字、设置套接字地址参数、进行端口绑定、接收数据、发送数据、关闭套接字等。
int s = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr addr_serv;
addr_serv.sin_family = AF_INET; /*地址类型为AF_INET*/
addr_serv.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
addr_serv.sin_port = htons(PORT_SERV); /*服务器端口*/
绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定:bind(s, (struct sockaddr*)&addr_serv, sizeof(addr_serv));
recvfrom()函数接收客户端的网络数据。
sendto()函数向服务器主机发送数据。
close()函数释放资源。
UDP协议的客户端流程分为套接字建立、设置目的地址和端口、向服务器发送数据、从服务器接收数据、关闭套接字5个部分。与服务器端的框架相比,少了 bind()部分。
客户端程序的端口和本地的地址可以由系统在使用时指定。
UDP协议常用的函数有 recv()/recvfrom ()、send()/sendto()、socket()、bind()
等。当然这些函数同样可以用于TCP协议的程序设计。
UDP 协议建立套接字的方式同TCP 方式一 样,使用socket()
函数,只不过协议的类型使用 SOCK_DGRAM
, 而不是SOCK_STREAM
。例如下面是建立一 个UDP 套接字文件描述符的代码。
int s;
s = socket(AF_INET,SOCK_DGRAM,0);
UDP协议使用bind()函数和TCP没有差别,将一个套接字描述符与地址结构绑定在一起,例:
struct sockaddr_in local;//本地地址信息
int from_len = sizeof(from);//地址结构的长度
local.sin_family = AF_INET; //协议族
local.sin_port = htons(8888);// 本地端口
local.sin_addr.s_addr = htonl(INADDR_ANY); //任意本地地址
s = socket(AF_INET, SOCK_DGRAM, 0); //初始化一 个IPv4族的数据报套接字
if (s == -1) {
//检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
bind(s, (struct sockaddr*)&local, sizeof(local)); //套接字绑定
绑定函数 bind()使用的时机,函数bind()的作用是将一个套接字文件描述符与一个本地地址绑定在一 起,即把发送数据的端口地址和IP 地址进行了指定。例如在发送数据的时候,如果不进行绑定,则会临时选择一个随机端口。
当客户端成功建立了一个套接字文件描述符并构建了合适的struct sockaddr
结构或者服务器端成功地将套接字文件描述符和地址结构绑定后,可以使用 recv()或者 recvfrom ()
来接收到达此套接字文件描述符上的数据,或者在这个套接字文件描述符上等待数据的到来。
①.rrecv()函数和recvfrom()函数
函数原型:
#include
#include
ssize_t recv(int s, void* buf, size_t len, int flags);
ssize_t recvfrom(int s, void* buf, size_t len, int flags, struct sockaddr* from, socklen_t* fromlen);
//s:正在监听端口的套接字文件描述符,由socket()生成。
//buf:接收数据缓冲区
//len:接收数据缓冲区大小,设置大小防止溢出
//from:指向本地的数据结构sockaddr_in的指针,接收数据时发送的地址信息放在这个结构中
//fromlen:所指内容的长度,可使用sizeof(struct sockaddr_in)获得。
recv() 函数和recvfrom()函数的返回值在出错的时候返回-1; 错误值保存在 errno 中,如下表。
成功的时候,函数将返回接收到的数据长度,数据的长度可以为0, 因此如果函数返回值为0, 并不表示发生了错误,仅能表示此时系统中接收不到数据。
注:函数 recvfrom()中的参数 from和fromlen均为指针
,不要直接将地址结构类型和地址类型的长度传入函数中,需要进行取地址的运算。
②.使用recvfrom()函数的例子
先建立一个数据报套接字文件描述符s,在地址结构local设置完毕后,将套接字s与地址结构local绑定一起。
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int s;//套接字文件描述符
struct sockaddr_in from;//发送方的地址信息
struct sockaddr_in local;//本地的地址信息
int from_len = sizeof(from);//地址结构的长度
int n;//接收到的数据长度
char buff[128];//接收数据缓冲区
s = socket(AF_INET,SOCK_DGRAM,0);//初始化一 个IPv4族的数据报套接字
if(s == -1){//检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
local.sin_family = AF_INET; //协议族
local.sin_port = htons(8888);// 本地端口
local.sin_addr.s_addr = htonl(INADDR_ANY); //任意本地地址
bind(s, (struct sockaddr*)&local, sizeof(local)); //套接字绑定
//套接字与地址绑定成功后,服务器可以直接通过这个套接字接收数据,recvfrom()函数从套接字s中每次可以接收128个字节的数据并保存到缓冲区buff中。
//函数recvfrom()所接收数据的来源可以从变量from中获得,包含发送数据的主机IP地址、端口等信息,变量from_len是发送数据主机地址结构的类型长度。
n = recvfrom(s,buff,128,0,(struct sockaddr*)&from,&from_len);//接收数据出错
if(n == -1){
perror("recvfrom");
exit(EXIT_FAILURE);
}
//处理数据
}
上面在使用recvfrom()函数的时候,没有绑定发送方的地址。所以在接收数据的时候要判断发送方的地址,只有合适的发送方才能进行相应的处理,因为不同的发送方发送的数据都可以到达接收方的套接字文件描述符,这是由于UDP协议没有按照连接进行区分
造成的,如下图所示。
③.应用层recv()函数和内核的关系
应用层的recvfrom()函数对应内核层的sys_recvfrom()系统调用函数。
系统调用函数 sys_recvfrom
主要查找文件描述符对应的内核 socket结构;
建立一个消息结构;
将用户空间的地址缓冲区指针和数据缓冲区指针打包到消息结构中;
在套接字文件描述符中对应的数据链中查找对应的数据;
将数据复制到消息中;
销毁数据链中的数据;
将数据复制到应用层空间;
减少文件描述符的引用计数。
sys _recvfrom()
调用函数sockfd_lookup _light()
查找到文件描述符对应的内核socket结构后,会申请一块内存用于保存连接成功的客户端状态。
socket结构的一些参数,例如类型type、操作方式ops等会继承服务器原来的值,如果原来服务器的类型为AF_INET
,则其操作模式仍然是af_inet.c
文件中的各个函数。
在内核空间使用了一个消息结构msghdr
用来存放所有的数据结构,其原型如下:
struct msghdr{
void *msg_name;//Socket名称
int msg_namelen;//Socket名称的长度
stuct iovec* msg_iov;//向量,存放数据
_kernel_size_t msg_iovlen;//向量数量
void *msg_control;//协议幻数
_kernel_size_t msg_controllen;//msg_control的数量
unsigned msg_flags;//消息选项
};
结构的成员msg_name
和msg_namelen
中存放发送方的地址相关的信息。
一个向量放在msg_iov
中,存放接收到的数据。
向量成员iov_base
指向用户传入的接收数据缓冲区地址,iov_len
为用户传入的接收缓冲区长度。
其示意图如下:
对于AF_INET
族,recvfrom
对应于udp_recvmsg()
函数,其实现在文件af_inet.c
中。分为如下步骤:
接收数据报数据。在接收的时候根据设置的超时时间来确定是否要一直等待至数据到来。例如当flag为MSG_DONTWAIT
时,仅仅查看一下,如果没有数据就退出
;否则就一直至超时时间的到来。在接收数据的时候,根据是否设置了MSG_PEEK
标志,决定是否将数据复制后销毁数据,或者仅仅将数据复制,而不销毁其中的数据。
计算复制出的数据长度,当接收到的数据长度比用户缓冲区的长度大时,设置MSG_TRUNC
标志,方便下一次的复制。
将数据复制到用户缓冲区空间。
复制发送方的地址和协议族。
根据消息结构的标志设置,接收其他的信息,例如TTL 、TOS、选项等。
销毁数据报缓冲区的对应变量。
成功建立套接字文件描述符,构建了struct sockaddr结构或者服务器端成功将套接字文件描述符和地址结构绑定后,可以使用send()或者sendto()函数发生数据到某个主机。
①.函数介绍
函数原型
#include
#include
ssize_t send(int s,const void *buf,size_t len,int flags);
ssize_t sendto(int s,const void *buf,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
s:正在监听端口的套接口文件描述符,通过函数 socket获得。
buf是发送数据缓冲区,发送的数据放在此指针指向的内存空间中。
len :发送数据缓冲区的大小。
to: 指向目的主机数据结构 sockaddr_ in 的指针,接收数据的主机地址信息放在这个结构中。
tolen :表示第 4 个参数所指内容的长度,可以使用 sizeof(struct sockaddr _in)
来获得。
send()函数和 sendto()函数的返回值在调用出错的时候返回-1,发生错误时 errno 的值如下表所示。
在调用成功的时候,返回发送成功的数据长度,数据的长度可以为 0, 因此函数返回值为 0 的时候是合法的。
②.sendto()函数例子
先调用socket()
函数产生一个数据报类型
的套接字文件描述符;然后设置发送数据的目的主机的IP地址和端口,将这些数值赋给地址结构;当地址结构设置完毕后,调用sendto()函数将需要发送的数据通过sendto()函数发送出去。
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int s;//套接字文件描述符
struct sockaddr_in to;//接收方的地址信息
int n;//发送到的数据长度
char buff[128];//发送数据缓冲区
s = socket(AF_INET,SOCK_DGRAM,0);//初始化一个IPv4族的数据报套接字
if(s == -1){//检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
to.sin_family = AF_INET; //协议族
to.sin_port = htons(8888);// 本地端口
to.sin_addr.s_addr = inet_addr("192.168.1.1"); //将数据发送到主机192.169.1.1上
n = sendto(s, buff,128,0,(struct sockaddr*)&to, sizeof(to));
//将数据buff发送到主机to上
if(n == -1){ //发送数据出错
perror("sendto");
exit(EXIT_FAILURE);
}
//..处理过程
}
在本例的发送过程中,由于没有设置本地的IP地址和本地端口,而这些参数是网络协议栈发送数据时的必需条件
,所以在UDP层网络协议栈会选择合适的端口。
发送的网络数据经过IP层的时候,客户端会选出合适的本地IP地址进行填充,并且将客户端的目的IP地址填充到IP报文中。
发送的数据到达数据链路层的时候,会根据硬件的情况进行发送。
如下图sendto()函数的示意图:
③.应用层sendto()函数和内核函数关系
应用层的 sendto()和内核层的 sendto()的关系如下图所示。应用层的 sendto()
函数对应内核层的 sys_sendto()
系统调用函数。
系统调用函数 sys_sendto()
查找文件描述符对应的内核socket结构、建立一个消息结构、将用户空间的地址缓冲区指针和数据缓冲区指针打包到消息结构中、在套接字文件描述符中对应的数据链中查找对应的数据、将数据复制到消息中、更新路由器信息、将数据复制到IP层、减少文件描述符的引用计数。
sys_ sendto()
调用函数 sockfd_lookup_light()
查找到文件描述符对应的内核 socket 结构后,会申请一 块内存用于保存连接成功的客户端的状态。
socket 结构的一 些参数,例如类型type、操作方式ops 等会继承服务器原来的值,如果原来服务器的类型为A F_INET
, 那么它的操作模式仍然是在文件 af_inet.c
中定义的各个函数。
然后会查找文件描述符表,获得一个新结构对应的文件描述符。在内核空间使用了一 个消息结构msghdr
用来存放所有的数据结构,原型如下:
struct msghdr{
void *msg_name;//Socket名称
int msg_namelen;//Socket名称的长度
stuct iovec* msg_iov;//向量,存放数据
_kernel_size_t msg_iovlen;//向量数量
void *msg_control;//协议幻数
_kernel_size_t msg_controllen;//msg_control的数量
unsigned msg_flags;//消息选项
};
在结构的成员msg_name
和msg_ nameIen
中存放发送方地址相关的信息。建立一个向量放在msg_iov
中,用于存放发送的数据。向量成员iov_base
指向用户传入的发送数据缓冲区地址,iov_len
为用户传入的发送缓冲区长度。消息结构msghdr存放recvfrom的各个参数如下图所示:
对于AF_INET
族,sendto()对应于udp_sendmsg()
函数,其实现在文件af_inet.c
中。分为如下步骤:
发送数据报数据。在发送数据的时候,查看是否设置了pending
,如果设置了此项,则仅仅进行检查是否可以发送数据,然后退出。如果选项中设置了OOB
,则退出,不能进行此项的发送。
确定接收方的地址和协议族。
将数据复制到用户缓冲区空间。
根据消息结构的标志设置,发送其他的信息,如TTL、TOS、选项等。
查看是否为广播,如果是,则更新广播地址。
更新路由。
将数据放入IP层。
销毁数据报缓冲区的对应变量。
UDP服务器和客户端中如何使用UDP函数进行程序设计,例子的程序框架如下图所示,客户端向服务器发送字符串UDP TEST
, 服务器接收到数据后将接收到的字符串发送回客户端。
服务器端:
s
。addr_serv
,协议为AF_INET
,地址为任意地址,端口为PORT_SERV(8888)
。s
绑定地址addr_serv
。udpserv_echo()
函数处理客户端数据。#include
#include /*包含socket()/bind()*/
#include /*包含struct sockaddr_in*/
#include /*包含memset()*/
#define PORT_SERV 8888/*服务器端口*/
#define BUFF_LEN 256/*缓冲区大小*/
//服务器循环等待客户端的数据,当服务器接收到客户端数据后,将收到的数据发给客户端。
void static udpserv_echo(int s, struct sockaddr*client)
{
int n; /*接收数据长度*/
char buff[BUFF_LEN]; /*接收发送缓冲区 */
socklen_t len; /*地址长度*/
while(1) /*循环等待*/
{
len = sizeof(*client);
n = recvfrom(s, buff, BUFF_LEN, 0, client, &len);/*接收数据放到buff中,并获得客户端地址*/
sendto(s, buff, n, 0, client, len);/*将接收到的n个字节发送回客户 端*/
}
}
int main(int argc, char*argv[])
{
int s; /*套接字文件描述符*/
struct sockaddr_in addr_serv,addr_clie; /*地址结构*/
s = socket(AF_INET, SOCK_DGRAM, 0); /*建立数据报套接字*/
memset(&addr_serv, 0, sizeof(addr_serv)); /*清空地址结构*/
addr_serv.sin_family = AF_INET; /*地址类型为AF_INET*/
addr_serv.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
addr_serv.sin_port = htons(PORT_SERV); /*服务器端口*/
bind(s, (struct sockaddr*)&addr_serv, sizeof(addr_serv));
/*绑定地址*/
udpserv_echo(s, (struct sockaddr*)&addr_clie); /*回显处理程序*/
return 0;
}
客户端:
客户端向服务器端发送数据UDP TEST,然后接收服务器端的回复消息,并将服务器端的数据打印出来。:
建立一 个套接字文件描述符s
。8
填充地址结构addr_serv
,协议为 AF_INET
,地址为任意地址,端口为 PORT_SERV
将套接字文件描述符s
绑定到地址addr_serv
。
调用udpclie_ echo()
函数和服务器通信。
#include
#include
#include
#include /*包含socket()/bind()*/
#include /*包含struct sockaddr_in*/
#include /*包含memset()*/
#define PORT_SERV 8888 /*服务器端口*/
#define BUFF_LEN 256 /*缓冲区大小*/
//向服务器端发送字符串UDP TEST, 接收服务器的响应,并将接收到的服务器数据打印出来。
static void udpclie_echo(int s, struct sockaddr*to)
{
char buff[BUFF_LEN] = "UDP TEST"; /*发送给服务器的测试数据05 */
struct sockaddr_in from; /*服务器地址*/
socklen_t len = sizeof(*to); /*地址长度*/
sendto(s, buff, BUFF_LEN, 0, to, len); /*发送给服务器*/
recvfrom(s, buff, BUFF_LEN, 0, (struct sockaddr*)&from, &len);
/*从服务器接收数据*/
printf("recved:%s\n",buff); /*打印数据*/
}
int main(int argc, char*argv[])
{
int s; /*套接字文件描述符*/
struct sockaddr_in addr_serv; /*地址结构*/
s = socket(AF_INET, SOCK_DGRAM, 0); /*建立数据报套接字*/
memset(&addr_serv, 0, sizeof(addr_serv)); /*清空地址结构*/
addr_serv.sin_family = AF_INET; /*地址类型为AF_INET*/
addr_serv.sin_addr.s_addr = htonl(INADDR_ANY); /*任意本地地址*/
addr_serv.sin_port = htons(PORT_SERV); /*服务器端口*/
udpclie_echo(s, (struct sockaddr*)&addr_serv); /*客户端回显程序*/
close(s);
return 0;
}
编译:
gcc -o server server.c
gcc -o client client.c
先运行服务器:
./server
在运行客户端:
./client
输出:
recves:UDP TEST
由于UDP协议缺少流量控制等机制,容易出现一些难以解决的问题:UDP 的报文丢失、报文乱序、 connect()函数、流量控制、外出网络接口的选择等是比较容易出现的问题。
利用UDP协议进行数据收发的时候,在局域网内一般情况下数据的接收方均能接收到发送方的数据,除非连接双方的主机发生故障,否则不会发生接收不到数据的情况。
①.UDP报文正常发送过程
而在 Internet上,由于要经过多个路由器,正常情况下一个数据报文从主机C 经过路由器A、路由器B、路由器C到达主机S,数据报文的路径如下图所示。主机C使用函数sendto()发送数据
,主机S 使用recvfrom ()函数接收数据
,主机S在没有数据到来的时候,会一 直阻塞等待。
②.UDP报文丢失
路由器要对转发的数据进行存储、处理、合法性判定、转发等操作,容易出现错误,所以很可能在路由器转发的过程中出现数据丢失的现象,如下图所示。当UDP的数据报文丢失的时候,函数 recvfrom ()会一 直阻塞,直到数据到来。
在上面的UDP服务器客户端的例子中,如果客户端发送的数据丢失,服务器会一
直等待,直到客户端合法数据到来;如果服务器的响应在中间被路由器丢弃,则客户端会一直阻塞,直到服务器数据的到来。
在程序正常运行的过程中是不允许出现这种情况的,所以可以设置超时时间来判断是否有数据到来。
对于数据丢失的原因,并不能通过简单的方法获得,例如,不能区分服务器发给客户端的响应数据是在发送的路径中被路由器丢弃,还是服务器没有发送此响应数据。
③.UDP报文丢失的对策
UDP协议中的数据报文丢失是先天性的,因为UDP是无连接的、不能保证发送数据的正确到达。
下图为TCP 连接中发送数据报文的过程,主机C 发送的数据经过路由器,到达主机S后,主机S要发送一 个接收到此数据报文的响应,主机C 要对主机S 的响应进行记录,直到之前发送的数据报文1已经被主机S接收到。
如果数据报文在经过路由器的时候,被路由器丢弃,则主机C和主机S会对超时的数据进行重发。
UDP协议数据收发过程中,会出现数据的乱序现象
。所谓乱序是发送数据的顺序和接收数据的顺序不一 致,例如发送数据的顺序为数据包A 、数据包B、数据包C, 而接收数据包的顺序变成了数据包 B 、数据包A、数据包C。
①.UDP数据顺序收发的过程
下图所示,主机C向主机S发送数据包0、数据包1、数据包2、数据包3, 各个数据包途中经过路由器A、路由器B、路由器C, 先后到达主机S, 在主机S端的循序仍然为数据包0、数据包1、数据包2、数据包3, 即发送数据时的顺序和接收数据时的顺序是一 致的。
②.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。
③.UDP乱序的对策
对于乱序的解决方法可以采用发送端在数据段中加入数据报序号的方法,这样接收端
对接收到数据的头端进行简单地处理就可以重新获得原始顺序的数据,如下图所示。
UDP协议的套接字描述符在进行数据接发之后,才能确定套接字描述符中所表示的发送方或者接收方的地址,否则仅能确定本地地址。
例:客户端的套接字描述符在发送数据之前,只要确定建立正确就可以了,在发送的时候才确定发送目的方的地址;服务器bind()
函数也仅仅绑定了本地进行接收的地址和端口。
在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,buffer,BUFF_LEN);//发送数据
read(s,buff,n);//数据接收
}
UDP协议没有TCP协议所具有的滑动窗口概念,接收数据的时候直接将数据放到缓冲区中。如果用户没有及时地从缓冲区中将数据复制出来,后面到来的数据会接着向缓冲区中放入。当缓冲区满的时候,后面到来的数据会覆盖之前的数据而造成数据的丢失。
①.UDP缺乏流量控制概念
如下图所示为UDP的接收缓冲区示意图,共有8个缓冲区,构成 一 个环状数据缓冲区。起点为0。
当接收到数据后,会将数据顺序放入之前数据的后面,并逐步递增缓冲区的序号,如下图所示。
当数据没有接收或者接收数据比发送数据的速率要慢,之前接收的数据被覆盖,造成数据的丢失,如图下所示。
②.缓冲区溢出对策
解决UDP接收缓冲区溢出
的现象需要根据实际情况确定,一般可以用增大接收数据缓冲区和接收方接收单独处理的方法来解决局部的UDP数据接收缓冲区溢出问题。
例如,在局部时间内发送方会爆发性地发送大量的数据,在后面的时间,发送的数据会较小,由于在局部时间内接收方不能及时处理接收到的数据,会造成数据的丢失,如果增大缓冲区,则可以改善此问题。
如果接收方的接收能力在绝对能力上要小于发送方,则接收方由于在处理能力或者容量方面的限制,造成数据肯定要丢失。
例如,可以这样实现客户端的代码,先将发送计数的值打包进发送缓冲区,然后复制要发送的数据,再进行数据发送。每次发送的时候,计数器增加1。
#define PORT_SERV 8888 //服务器端口
#define NUM_DATA 100 //接收缓冲区数量
#define LENGTH 1024 //单个接收缓冲区大小
static char buffer_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++)//循环发送
{
//buffer_send[0] = buffer_send;int*将地址buffer_send转换成int类型的指针;
//外面*去获取这个地址的值
*((int*)&buffer_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 udpclie_echo(int s, struct sockadd* 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);
}
}
在网络程序设计的时候,有时需要设置 一 些特定的条件 。例如 ,一 个主机有两个网卡 ,由于不同的网卡连接不同的子网,用户发送的数据从其中的一个网卡发出,将数据发送特到一特定的子网上。使用函数connect()可以将套接字文件描述符与一个网络地址结构进行绑定 ,在地址结构中所设置的值是发送接收数据时套接字采用的IP地址和端口。下面的代码是个例子:
#include
#include /*socket()/bind()*/
#include /*struct sockaddr_in*/
#include /*memset()*/
#include
#include
#include
#define PORT_SERV 8888
int main(int argc, char*argv[])
{
int s; /*套接字文件描述符*/
struct sockaddr_in addr_serv; /*服务器地址*/
struct sockaddr_in local; /*本地地址*/
socklen_t len = sizeof(local); /*地址长度*/
s = socket(AF_INET, SOCK_DGRAM, 0); /*生成数据报套接字*/
/*填充服务器地址*/
memset(&addr_serv, 0, sizeof(addr_serv)); /*清零*/
addr_serv.sin_family = AF_INET; /*AF_INET协议族*/
addr_serv.sin_addr.s_addr =inet_addr("127.0.0.1"); /*地址为127.0.0.1*/
addr_serv.sin_port = htons(PORT_SERV); /*服务器端口*/
connect(s, (struct sockaddr*)&addr_serv, sizeof(addr_serv));
/*连接服务器*/
getsockname(s, (struct sockaddr*)&local, &len); /*获得套接字文件描述符的地址*/
printf("UDP local addr:%s\n",inet_ntoa(local.sin_addr));
/*打印获得的地址*/
close(s);
return 0;
}
编译运行:系统程序中的套接字描述符与本地的回环接口进行绑定:
当使用UDP协议接收数据的时候,如果应用程序传入的接收缓冲区的大小
小于到来数据的大小
时,接收缓冲区会保存最大可能接收到的数据,其他的数据将会丢失,并且有MSG_TRUNC
的标志。
例如对函数udpclie_ echo()
做如下修改,发送一个字符串后在一个循环中接收服务器端的响应,会发现只能接收一个U
, 程序阻塞到 recvfrom ()
函数中。
这是因为服务器发送的字符串到达客户端后,客户端的第一 次接收动作没有正确地接收到全部的数据,其余的数据已经丢失了。
static void udpclie_echo(int s, struct sockaddr* to)
{
char buff[BUFF_LEN] = "UDP TEST";//要发送的数据
struct sockaddr_in from;//发送方的地址结构
int len = sizeof(*to);//发送的地址结构长度
sendto(s, buff, BUFF_LEN, 0, to, len);//发送数据
int i = 0;//接收数据的次数
for (i = 0; i < 16; i++)
{
memset(buff, 0, BUFF_LEN);//消空缓冲区
int err = recvfrom(s, buff, 1, 0, (struct sockaddr*)&from, &len);//接收数据
printf("%dst:%c,err:%d\n", i, buff[0], err);//打印数据
}
printf("recved:%s", buff);//打印信息
}
所以服务器和客户端的程序要进行配合,接收的缓冲区要比发送的数据大一 些,防止数据丢失的现象发生。