UDP C/S的典型函数调用
UDP没有像TCP那样的连接,客户端直接sendto向某服务器发送数据,服务器端一直recvfrom阻塞,以接收任何客户端发送的数据。
sendto和recvfrom函数
int sendto(int sockfd, const void* buff, size_t nbytes, int flag, const struct sockaddr* to, socklen_taddrlen);
int recvfrom(int sockfd, void* buff, size_t nbytes, int flag, struct sockaddr* from, socklen_t* addrlen);
- 成功返回字节数,失败返回-1
- 这两个函数相比较于read和write多了三个参数
(1) flag后面说,这里先置为0
(2) sendto的地址结构指明发送目的地的套接字地址。addrlen指明地址长度,为整数型。相当于TCP的connect中的套接字地址。
(3) recvfrom的地址结构指明发送此数据报的发送端的套接字地址。addrlen为此套接字地址,为整型地址。相当于TCP的accept中的套接字地址。 - 写一个长度为0的数据报是可行的,会形成一个只包含IP首部(20字节)和UDP首部(8字节)的IP数据报。所以recvfrom返回0,是可接受的。而不是像TCP那样read返回0表示关闭连接。
- recvfrom的套接字地址参数可以是NULL,表示不关心数据是谁发的。此时addrlen也必须是NULL。
使用UDP书写回射服务器
回射服务器代码 点击查看
回射服务器dg_echo代码 点击查看
- 首先正常情况下,函数永不会终止。它不像TCP连接那样,还有终止连接的四次。
- 处理函数str_echo,是一个迭代函数,不像TCP那样是并发的。一般TCP服务器都是并发的,而UDP服务器都是迭代的。为何?下面说
- 每个UDP套接字都会有一个接收缓冲区,类似于一个队列。多个数据报到达UDP服务器,则会排队,调用recvfrom函数,从这个队列头取出数据报给进程。而TCP是为每个客户一个连接fork一个子进程,并且每个连接一个套接字,每个套接字一个接收缓冲区,所以我们要并发监听每个接收缓冲区。而UDP是任何客户发送的数据报放入一个接收缓冲区,所以根本无需什么并发服务器,也不可能做成并发的。
- str_echo函数是协议无关的。
使用UDP重写回射客户端
- 没有为客户端指定本地端口,则客户端第一次sendto的时候,内核自动分配。
- 这里recvfrom的套接字地址是空指针,这样做是非常危险的
因为可能任何主机给此客户端的这个临时端口发送一个消息时,此客户端会认为这个消息是从服务器端发送过来的。造成消息混乱。所以这个是有问题的,下面解决。 - 数据报的丢失问题
如果某次客户端发送给服务器端的数据报丢失了,或者服务器端发给客户端的数据包丢失了,则这都会引起客户端永远阻塞在recvfrom函数上。
我们可以为recvfrom设置一个超时,但是超时还是不能完全解决这个问题。因为如果超时,我们不知道是客户端->服务器端数据报丢失了,还是服务器->客户端数据库丢失了。 - 针对上面2提到的问题,我们试着获取recvfrom的套接字地址和sendto发送的套接字地址是否一致,来决定此消息是否是来自对端服务器。
我们修改的str_cli函数如下:
void str_cli(int sockfd, FILE* fd, const struct sockaddr* servaddr, socklen_t addrlen)
{
int nbytes;
charbuff[MAXLINE],recvbuff[MAXLINE];
struct sockaddr * fromaddr=new sockaddr();
socklen_tfromaddrlen=addrlen;
while(fgets(buff,MAXLINE,fd)!=NULL)
{
sendto(sockfd, buff,strlen(buff), 0, servaddr, addrlen);
nbytes=recvfrom(sockfd,recvbuff, MAXLINE,0, fromaddr, &fromaddrlen );
if(fromaddrlen !=addrlen || memcmp(servaddr, fromaddr, addrlen)!=0)
{
fputs("not from server, ignored",stdout);
continue;
}
if(nbytes<0)
err_sys("recvfrom error");
recvbuff[nbytes]=0;
fputs(recvbuff,stdout);
}
}
可以看出,我们就是比较sendto时,服务器的套接字地址,和recvfrom得到的服务器套接字地址是否相等。这里我们先比较两者的长度,然后再逐字节比较。
这里还是有问题的:
如果服务器是多宿主机,即两个IP地址,如Ip1,Ip2。由于我们在写服务器端程序时,bind函数的参数是通配IP,所以当我们sendto时,是使用Ip1,而服务器回射时,内核自动选择了Ip2,则这会让我们客户端误判该回射消息不是来自服务器端。
两个解决办法:
1> recvfrom得到IP后,查询DNS获得主机的域名,以判断消息是否来自该主机。
2>服务器端为每个IP创建一个套接字,使用bind到每个IP地址。然后使用select监听这些套接字,等待其中一个变为可读,说明客户端使用的是这个IP,则服务器使用这个IP套接字回射就可以了。
服务器未运行
当我们先启动客户端,不启动服务器端时,发生了什么:
我们从控制台输入一行数据回车,然后客户端将永远阻塞在recvfrom函数上。
底层机制:
数据发送到服务器主机上,发送主机的目的端口并没有开启,所以返回一个端口不可达的ICMP消息,这个消息是不会返回客户端进程的(为何?)。所以客户端用于阻塞在recvfrom上。
异步错误:本例中错误由sendto函数引起,但是sendto成功返回,ICMP到后来才返回错误。这就是异步错误。
一个基本规则:对于一个UDP套接字,由它引发的异步错误不返回给它,除非它已经连接。(注意UDP也有connect函数)。
为何ICMP消息不会返回客户端进程?Unix这样设计的道理是什么?
假设我们使用客户端连续发送3个消息,2个消息的目的服务器正常,最后一个服务器未启动,则会有一个ICMP消息,假设这个消息被recvfrom获取,recvfrom返回一个负值表示错误,然后errno为错误类型。但是注意此时客户端并不知道是哪个目的服务器,哪个目的套接字出错,内核无法告知进程,因为recvfrom此时返回的信息只是errno,所以Unix规定,不返回给进程。
给UDP套接字使用connect
上面提到未连接UDP套接字发生的异步错误,不会返回给进程,这里我们可以使用connect对一个UDP套接字进行连接。
Connect函数的调用和TCP一样,参数指定目的服务器的套接字地址。注意没有三次握手,只是检查对端是否存在立即可知的错误(如目的主机不可达)。
已连接UDP套接字和未连接UDP套接字的不同:
(1) 已连接套接字,直接使用send、write,发送数据报给connect的目的服务器套接字。而不使用sendto。
(2)已连接套接字,直接使用read、recv或者recvmsg,接收来自connect目的服务器套接字的数据报,来自其他目的服务器套接字的数据报不会递交给该套接字、也就意味着,已连接套接字只能和一个对端进行通信。
而未连接套接字显然可以和任何多个对端通信。
(3) 已连接套接字发生异步消息会返回给进程,因为此时已经知道目的套接字。而未连接套接字不会返回给进程。
注意:一般对客户端的UDP套接字进行connect,而服务器端还是sendto和recvfrom,connect只会影响本地套接字。
(1) 指定新的IP地址和端口号
(2) 断开套接字。此时把套接字地址结构的地址族(IPv4的sin_family)设为AF_UNSPEC就可以了。
性能
当我们对一个未连接的UDP套接字连续sendto两次,看看具体步骤:
连接套接字
发送第一个数据报
断开套接字
连接套接字
发送第二个数据报
断开套接字
如果两个数据报是同一个目的套接字,则我们应该使用显然connect,之后会提高效率。因为这样只需要一个连接和断开。
Unix中一个连接需要耗费一次UDP传输的三分之一的开销。
调用connect后调用两次write涉及内核执行如下步骤:
1、连接套接字
2、输出第一个数据报
3、输出低二个数据报
我们修订上面的客户端str_cli函数
void str_cli( int sockfd, FILE* fd,const struct sockaddr* servaddr, socklen_t addrlen)
{
int nbytes;
char buff[MAXLINE],recvbuff[MAXLINE];
conect(sockfd,servaddr,addrlen);
while(fgets(buff,MAXLINE,fd)!=NULL)
{
write(sockfd, buff, strlen(buff));
nbytes=read(sockfd,recvbuff, MAXLINE );
if(nbytes<0)
err_sys("read error");
recvbuff[nbytes]=0;
fputs(recvbuff,stdout);
}
}
如果为启动服务器,此时我们再运行客户端,输入一个未启动的服务器程序的主机Ip地址,然后从控制台输入一行数据,输出的结果就是readerror。这样异步错误返回给进程了。
注意:此时我们connect时,并没有发生错误,直到我们发送一个消息时才返回错误。而如果此时TCP的话,在connect时就会发生错误。原因?
因为UDP的connect不会触发三次握手,而TCP的connect会触发三次握手,发现目的端口不可达,则服务器会返回RST分组。
UDP缺乏流量控制
假设一个客户端连续发送大量的数据,则服务器端使用套接字接收缓冲区排队接收这些数据,但当发送来的数据超出套接字接收缓冲区时,服务器端就会自动丢弃到来的数据报,而此时客户端和服务器端不会有任何的错误。
所以说UDP是没有流量控制的。
UDP中的IP地址和端口号
- 未连接的UDP套接字,如果我们没有bind,
则当sendto时,内核选择一个本地IP地址和端口号,所以同一主机上两次连续的sendto,两个消息的源IP地址和端口号可能都不一样。
而且,服务器端接收recvfrom后,回射消息,sendto时,可能造成回射消息的源IP地址和端口号和recvfrom消息的目的IP地址和端口号不一样。
- 已连接UDP套接字,如果没有bind
同一个客户端,多次connect同一个目的IP端口号时,已连接套接字的本地IP和端口号可能都是不一样的。
我们可以使用getsockname来获取已连接UDP套接字的本地IP和端口号。
我们使用recvmsg来获取未连接的UDP套接字的本地IP和端口号。