UDP特点
1.无连接,TCP是基于连接的,在连接的时候需要进行三次握手
2.基于消息的数据传输服务,TCP是基于流的数据传输服务,会有粘包问题的产生
3.不可靠,表现在数据包可能会丢失、重复、乱序、缺乏流量控制
4.一般情况下UDP更加高效。
UDP客户/服务基本模型
recvfrom和sendto函数
#include<sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t nbytes, int flags,
const struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t nbytes, int flags,
const struct sockaddr* to, sockelen_t addrlen);
前三个参数sockfd,buf和nbytes等同于read和write函数的三个参数:描述符、指向读入或写出缓冲区的指针和读写字节数。
sendto的to参数指向一个含有数据报接收者的协议地址(如IP地址和端口号)的套接字地址结构,其大小是由addrlen参数指定。recvfrom的from参数指向一个将由该函数在返回时填写的数据报发送者的协议地址的套接字地址结构,而在该套接字结构中填写的字节数则在addrlen参数所指的整数中返回给调用者。注意,sendto的最后一个参数是一个整数值,而recvfrom的最后一个参数是一个指向整数值的指针(即值-结果参数)
recvfrom的最后两个参数类似于accept的最后两个参数:返回时其中套接字的地址结构告诉我们是谁发送了数据报(UDP情况下)或是谁发起了连接(TCP情况下)。sendto的最后两个参数类似于connect的最后两个参数:调用时其中套接字的地址结构被我们填入数据报将发往(UDP情况下)或与之建立连接(TCP情况下)的协议地址。
这两个函数都把所读写的数据的长度作为函数返回值。在recvfrom使用数据报协议的典型用途中,返回值就是所接收数据报中的用户数据量。
一般情况下,flags总是设置为0。
UDP回射客户/服务器
服务端
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
void echo_srv(int sock)
{
//不断接受客户端发送过来的一行数据
char recvbuf[1024]={0};
//客户端的地址信息
struct sockaddr_in peeraddr;
socklen_t peerlen;
//接受到的字节数
int n;
while(1)
{
peerlen=sizeof(peeraddr);
memset(recvbuf,0,sizeof(recvbuf));
n=recvfrom(sock,recvbuf,sizeof(recvbuf),0,(struct sockaddr*)&peeraddr,&peerlen);
if(n==1)
{
if(errno==EINTR)
continue;
ERR_EXIT("recvfrom");
}
//回射回去
else if(n>0)
{
//打印在服务端
fputs(recvbuf,stdout);
sendto(sock,recvbuf,n,0,(struct sockaddr*)&peeraddr,peerlen);
}
}
close(sock);
}
int main(void)
{
int sock;
//创建一个套接字,第二个参数是UDP套接口
if((sock=socket(PF_INET,SOCK_DGRAM,0))<0)
ERR_EXIT("socket");
//初始化一个地址绑定它
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5188);
//本机任意的一个地址
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("bind");
//没有三次握手,所以不需要监听
//回射服务器
echo_srv(sock);
return 0;
}
客户端
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
}while(0)
void echo_cli(int sock)
{
//初始化一个地址绑定它
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5188);
//指定对方的IP地址
servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
//用于说明异步的错误能返回给已连接的套接字
//UDP在调用connect的时候是不做三次握手的
connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr));
int ret;
char sendbuf[1024]={0};
char recvbuf[1024]={0};
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
//第一次调用的时候就对地址进行了绑定
//端口的绑定是在第一次调用sendto,connect仅仅只是确认了外出接口的地址
sendto(sock,sendbuf,strlen(sendbuf),0,(struct sockaddr*)&servaddr,sizeof(servaddr));
//如果是有connect的话,就可以连接到地址了
//sendto(sock,sendbuf,strlen(sendbuf),0,NULL,0);
/*
send也可以用来做发送
send(sock,sendbuf,strlen(sendbuf),0);
*/
//回射接受时,可以不指定对方的IP,因为上面已经绑定过了
ret=recvfrom(sock,recvbuf,sizeof(recvbuf),0,NULL,NULL);
if(ret==-1)
{
if(errno==EINTR);
continue;
ERR_EXIT("recvfrom");
}
fputs(recvbuf,stdout);
memset(sendbuf,0,sizeof(sendbuf));
memset(recvbuf,0,sizeof(recvbuf));
}
close(sock);
}
int main(void)
{
int sock;
//创建一个套接字,第二个参数是UDP套接口
if((sock=socket(PF_INET,SOCK_DGRAM,0))<0)
ERR_EXIT("socket");
echo_cli(sock);
return 0;
}
UDP注意点
1.UDP报文可能会丢失、重复
2.UDP报文可能会乱序
3.UDP缺乏流量控制,UDP也是维护一个缓冲区,当缓冲区满的时候,UDP没有流量控制的机制,此时如果再往里面发送数据,并不是将数据丢失掉,而是将数据覆盖到原来的缓冲区。
4.UDP协议数据报文截断,如果接收到的数据报大于接收的缓冲区,就会将数据报截断,引发丢弃。
5.recvfrom返回0,不代表连接关闭,因为UDP是无连接的。可以不发送任一字节的数据,此时发送的实际上发送的数据长度是TCP头部和IP头部。
6.ICMP异步错误
先启动客户端,然后客户端从键盘接收一行输入,调用sendto发送出去,此时程序阻塞在recvfrom的地方,但是服务端并没有启动,但是程序无法捕捉这一信息。这是因为产生了一个异步错误,异步错误不会返回给套接口。
sendto的信息是无法到达对等方的,此时有一个ICMP的错误,但是这个错误是在recvfrom的时候才能产生,所以叫做异步ICMP错误。TCP/IP规定异步错误不能返回给未连接的套接字,所以recvfrom也得不到通知,导致一直阻塞。
7.解决6问题的办法是,UDP也可以调用connect,此时异步的错误能够返回给已连接的套接字。
我们必须区分:
未连接UDP套接字,新创建UDP套接字默认如此;
已连接UDP套接字,对UDP套接字调用connect的结果。
对于已连接UDP套接字(调用connect后),与默认的未连接UDP套接字相比,发生了三个变化:
(1)我们再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sento,而改用write或send。写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址(如IP地址和端口号)。
(2)我们不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报。目的地为这个已连接UDP套接字的本地协议地址(如IP地址和端口号),发源地却不是该套接字早先connect到的协议地址的数据报,不会投递到该套接字。这样就限制一个已连接UDP套接字能且仅能与一个对端交换数据报。
(3)由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。
作为小结,我们可以说UDP客户进程或服务进程只在使用自己的UDP套接字与确定的唯一对端进行通信时,才可以调用connect,调用connect的通常是UDP客户,不过有些网络应用中的UDP服务器会与单个客户长时间通信(如TFTP),这种情况下,客户和服务器都可能调用connnect。