UDP协议的特点:
1,无连接,也就是说:UDP内部并没有维护端到端的状态,这跟TCP是不一样的。UDP不需要三次握手,
2,基于消息的数据传输服务,传输的是数据报,跟TCP基于字节流是不一样的,不会出现所谓的粘包问题,就是这些
数据报是有边界的,而TCP是没有边界的
3,不可靠,表现在数据报可能会丢失,也可能会重复,也可能会乱序,缺乏流量控制
所以说:一般情况下,UDP是比TCP更加高效的
有些场合更适合使用UDP,使用UDP编写的一些常见的应用程序有:DNS(域名系统),NFS(网络文件系统)和
SNMP(简单网络管理协议)
基本模型:
也是需要服务端和客户端的
服务端创建socket,bind,接下来调用recvfrom(因为不需要建立连接)一直阻塞,直到收到来自客户的数据
对于客户端而言:创建socket,接下来也是直接调用sendto函数给服务器发送数据报,其中必须指定目的地(即服务
器)的地址作为参数,这时recvfrom才会收到数据,并且对数据进行处理,recvfrom将与所接受的数据报一道返回客
户的协议地址,因此服务器可以把响应发送给正确的客户。服务器这时会调用sendto函数,然后返回,客户端调用
recvfrom接受,最后调用close函数
UDP的简单回射客户/服务器
当然,这里面需要用到recvfrom,sendto函数,我们来看看:
#include
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//第一个:套接字socket
//第二个:缓冲区
//第三个:接收的长度
//第四个:flags,我们用0来表示
//第五个,第六个:指定对方的地址信息
UDP服务端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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)
{
//UDP通信的第一步创建一个套接字
int sock;
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);
//INADDR_ANY本机的任意地址
if(bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
echo_srv(sock);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
//表示对等方的地址可以不要了
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
}
int main(void)
{
//UDP通信的第一步创建一个套接字
int sock;
if((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
echo_cli(sock);
return 0;
}
那么客户端的地址,是在什么时候绑定的呢???
是在第一次sendto的时候,
因为我们前面已经知道了我客户端要跟谁(服务端)连接,我sendto的时候,会将服务端的信息加入到socket,所以我们调用sendto的时候,也就间接的知道啦跟谁进行连接,,,,
socket的结构体中:是知道客户端/服务端的端口号,IP地址
套接口:
两个属性:本地地址(通过getsockname来获取),远程地址(通过getpeername来获取)
(前提是:这个套接字是一个已连接套接字)
当前我们的套接字是一个未连接的套接字,那么我们就可以通过getsockname来获取本地地址
也就是说:对于客户端,我们的本地地址不是通过bind函数来进行绑定的,我们是通过sendto函数来绑定的,第一次
调用的时候就会绑定,接下来的操作调用是不会再去绑定的,因为我们不会去做多余的操作。很简单
如上,就是我们的UDP通信的过程
注意点:
1,UDP报文可能会丢失,重复,乱序,也就是说:如果我们要确保一个比较可靠的传输的话,我们处理丢失,重复
这一些问题(针对丢失,那么发送端/接收端就需要超时重传机制,启动定时器)(针对重复,我们应用层要维护
数据报之间的序号,)
2,UDP缺乏流量控制,UDP也有自己的缓冲区,如果缓冲区满的话,再次接收到的数据不会丢失,而是会覆盖缓冲
区中的数据,并没有流量控制的机制,不像TCP有滑动窗口协议,如果我们也要实现的话,那么我们可以在应用
层实现一个模拟的滑动窗口,来简单的控制流量
3,UDP协议数据报文端可能也会截断,因为接收到的数据可能大于缓冲区,多余的数据会被直接丢弃
我们来看一下这个程序:UDP中,发送的数据大于缓冲区的情况(为了简单方便,我们将客户端和服务端写到
啦一块)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main(void)
{
//UDP通信的第一步创建一个套接字
int sock;
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);
//INADDR_ANY本机的任意地址
if(bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
sendto(sock, "abcd", 4, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
//服务端自己给自己发送数据
char recvbuf[1];
int i;
int n;
for(i = 0; i< 4; i++)
{
n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
if(n == -1)
{
if(errno == EINTR)
continue;
ERR_EXIT("recvfrom");
}
else if(n > 0)
printf("n = %d %c\n", n, recvbuf[i]);
}
return 0;
}
可以看到,数据后面的bcd都被截断了,所以说:为了避免截断现象,我们必须缓冲区大于所要发送的数据
4,recvfrom可以返回0,因为返回0并不代表连接关闭,因为udp是无连接的
返回为0,表示不发送任何一个数据,类似于这样:
sendto(sock, "abcd", 0, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
这时候,实际发送的数据长度就是一个tcp的头部,IP的头部(都是20个字节),那么总共是40个字节,当然,这
里不算数字链路的头部,那么对方收到的时候,就代表返回为0,并不代表连接的关闭。。。。
5,ICMP的异步错误
这里,我们可以简单说一下:
如上的三个程序:
我们先启动客户端echocli,并且这个客户端从键盘接收输入数据(aaa),调用sendto将数据发送出去,接着
阻塞到啦recvfrom函数中:
那么实际上对等方并没有启动,但是我们的sendto并没有捕捉到这个信息,仍然阻塞到recvfrom函数:
为什么呢????
因为,此刻产生了一个异步错误。。。。异步错误是不会返回给套接口的。那么,为什么说产生了异步错误,首
先:sendto是不会出错的,因为UDP是无连接的,sendto不需要维护状态,sendto仅仅是将应用层的数据拷贝到
套接口的缓冲区中,仅仅只是拷贝,并不代表数据已经发送给对方了,只要缓冲区中有数据,那么套接口就可以
拷贝,但是,但是,数据是不会到达对等方的,这时就会产生一个ICMP的错误。也就是说:TCP/IP协议栈会有
一个报文的应答。。。报文应答给我们的客户端。。。。而这个错误,是在接受的时候才会产生的(调用
recvfrom)
我们称之为异步的iCMP错误。。。
这个错误被延迟到啦recvfrom才会被通知,,,但是,但是,recvfrom也不会被通知,因为TCP/IP规定,这个异
步错误,是不能发送给未连接的套接字的。。。。
recvfrom会一直阻塞
我们来将上面的程序改改(echocli.c):
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
//表示对等方的地址可以不要了
if(ret == -1)
{
if(errno == EINTR)
continue;
ERR_EXIT("recvfrom");
}
//其他程序没有改变
可以预测到:如果recvfrom能接受到应答,那么就会提示出错,并且退出程序。。。。
我们再次运行结果:
程序还是没有退出。。。
recvfrom继续阻塞。。。
6,如何处理上面这个问题呢???
UDP也可以调用connect函数,,,
int ret;
connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
可以发现,这个套接字可以返回给已连接套接字,表示连接被拒绝,,,,
但是,UDP建立的也不是跟TCP一样的连接啊,,,
UDP在调用connect的时候,实际上并不做三次握手,,,并没有跟对方在传递数据,只是维护了套接字的一种
状态,,,通过这个数据能够发送数据给对等方。。
通过这个数据能够发送数据给对等方,只能发送给这个对等方(相当于:远程地址得到了一个绑定,远程地址只
能是我们所能连接的地址,这个时候,这个套接字就不能发送数据给其他地址了)
通过运行(先打开服务端),我们可以知道客户端还是可以正常收发数据的。。。
上面的意思,也就是sendto的时候,已经指定了对方的地址,所以我们可以不用指定对方的地址了,,,
sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0); //不指定地址
也能成功,也能正常的收发,,,
同时,这里的sendto也是可以该为send和write的,,,
send(sock, sendbuf, strlen(sendbuf), 0);
同时,recvfrom也可以通过recv来替换,,,,
7,UDP的外出接口
假如客户端有多个IP的时候(192.168.1.23, 192.168.2.31)服务端有(192.168.1.33,192.168.2.33)
那么当我们的sendto连接192.168.1.33,就会自动的跟192.268.1.23进行通信
sendto(sock, 192.168.1.33,,,,,,,,),sock自动的连接到啦192.268.1.23
当然,也可以调用connect连接地址,但是并没有绑定端口,端口的绑定是在sendto
如:connect(sock, 192.168.1.33);那么数据是从192.168.1.23这张网卡出去的,但是地址可就限定了