一、 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. 成功返回字节数,失败返回-1
2. 这两个函数相比较于read和write多了三个参数。
(1) flag后面说,这里先置为0
(2) sendto的地址结构指明发送目的地的套接字地址。addrlen指明地址长度,为整数型。相当于TCP的connect中的套接字地址。
(3) recvfrom的地址结构指明发送此数据报的发送端的套接字地址。addrlen为此套接字地址,为整型地址。相当于TCP的accept中的套接字地址。
3. 写一个长度为0的数据报是可行的,会形成一个只包含IP首部(20字节)和UDP首部(8字节)的IP数据报。所以recvfrom返回0,是可接受的。而不是像TCP那样read返回0表示关闭连接。
4. recvfrom的套接字地址参数可以是NULL,表示不关心数据是谁发的。此时addrlen也必须是NULL。
三、使用UDP书写回射服务器
#include "unp.h" #define MAXLINE 1024 #define PORT 13 void err_sys(const char* s) { fprintf(stderr, "%s\n", s); exit(1); } void str_echo(int sockfd, struct sockaddr* cliaddr,socklen_t len) { int nbyte; socklen_t n; char buff[MAXLINE]; while(true) { n=len; nbyte=recvfrom(sockfd, buff, MAXLINE, 0, cliaddr, &n); if(nbyte<0) err_sys("recv error"); sendto(sockfd, buff, nbyte, 0, cliaddr, n); } } int main(int argc, char** argv) { int sockfd; struct sockaddr_in servaddr,cliaddr; sockfd=socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.port=htona(PORT); servaddr.sin_addr.s_addr=htonl(INADDR_ANY); bind(sockfd,(struct sockaddr*) &servaddr, sizeof(servaddr)); str_echo(sockfd,(struct aockaddr*)&cliaddr, sizeof(cliaddr)); }
1. 首先正常情况下,函数永不会终止。它不像TCP连接那样,还有终止连接的四次。
2. 处理函数str_echo,是一个迭代函数,不像TCP那样是并发的。一般TCP服务器都是并发的,而UDP服务器都是迭代的。为何?下面说
3. 每个UDP套接字都会有一个接收缓冲区,类似于一个队列。多个数据报到达UDP服务器,则会排队,调用recvfrom函数,从这个队列头取出数据报给进程。而TCP是为每个客户一个连接fork一个子进程,并且每个连接一个套接字,每个套接字一个接收缓冲区,所以我们要并发监听每个接收缓冲区。而UDP是任何客户发送的数据报放入一个接收缓冲区,所以根本无需什么并发服务器,也不可能做成并发的。
4. str_echo函数是协议无关的。
四、 使用UDP重写回射客户端
#include "unp.h" #define MAXLINE 1024 #define PORT 13 void err_sys(const char* s) { fprintf(stderr, "%s\n", s); exit(1); } void str_cli( int sockfd, FILE* fd,const struct sockaddr* servaddr,socklen_t addrlen) { int nbytes; charbuff[MAXLINE],recvbuff[MAXLINE]; while(fgets(buff,MAXLINE,fd)!=NULL) { sendto(sockfd,buff, strlen(buff), 0, servaddr, addrlen); nbytes=recvfrom(sockfd,recvbuff, MAXLINE, 0, NULL, NULL ); if(nbytes<0) err_sys("recvfrom error"); recvbuff[nbytes]=0; fputs(recvbuff,stdout); } } int main(int argc, char** argv) { int sockfd; struct sockaddr_inservaddr; char buff[MAXLINE+1]; if(argc!=2) err_sys("input error"); if((sockfd=socket(AF_INET,SOCK_DGRAM, 0))<0) err_sys("socket error"); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(PORT); if(inet_pton(AF_INET, argv[1],&servaddr.sin_addr)<=0) err_sys("ip address error"); str_cli(sockfd,stdin,(struct sockaddr*) &servaddr,sizeof(servaddr)); exit(0); }
1. 没有为客户端指定本地端口,则客户端第一次sendto的时候,内核自动分配。
2. 这里recvfrom的套接字地址是空指针,这样做是非常危险的
因为可能任何主机给此客户端的这个临时端口发送一个消息时,此客户端会认为这个消息是从服务器端发送过来的。造成消息混乱。所以这个是有问题的,下面解决。
3. 数据报的丢失问题
如果某次客户端发送给服务器端的数据报丢失了,或者服务器端发给客户端的数据包丢失了,则这都会引起客户端永远阻塞在recvfrom函数上。
我们可以为recvfrom设置一个超时,但是超时还是不能完全解决这个问题。因为如果超时,我们不知道是客户端->服务器端数据报丢失了,还是服务器->客户端数据库丢失了。
4. 针对上面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套接字回射就可以了。
5. 服务器未运行
当我们先启动客户端,不启动服务器端时,发生了什么:
我们从控制台输入一行数据回车,然后客户端将永远阻塞在recvfrom函数上。
底层机制:
数据发送到服务器主机上,发送主机的目的端口并没有开启,所以返回一个端口不可达的ICMP消息,这个消息是不会返回客户端进程的(为何?)。所以客户端用于阻塞在recvfrom上。
异步错误:本例中错误由sendto函数引起,但是sendto成功返回,ICMP到后来才返回错误。这就是异步错误。
一个基本规则:对于一个UDP套接字,由它引发的异步错误不返回给它,除非它已经连接。(注意UDP也有connect函数)。
为何ICMP消息不会返回客户端进程?Unix这样设计的道理是什么?
假设我们使用客户端连续发送3个消息,2个消息的目的服务器正常,最后一个服务器未启动,则会有一个ICMP消息,假设这个消息被recvfrom获取,recvfrom返回一个负值表示错误,然后errno为错误类型。但是注意此时客户端并不知道是哪个目的服务器,哪个目的套接字出错,内核无法告知进程,因为recvfrom此时返回的信息只是errno,所以Unix规定,不返回给进程。
6. 给UDP套接字使用connect
上面提到未连接UDP套接字发生的异步错误,不会返回给进程,这里我们可以使用connect对一个UDP套接字进行连接。
Connect函数的调用和TCP一样,参数指定目的服务器的套接字地址。注意没有三次握手,只是检查对端是否存在立即可知的错误(如目的主机不可达)。
已连接UDP套接字和未连接UDP套接字的不同:
(1) 已连接套接字,直接使用send,发送数据报给connect的目的服务器套接字。而不使用sendto。
(2) 已连接套接字,直接使用write,接收来自connect目的服务器套接字的数据报,来自其他目的服务器套接字的数据报不会递交给该套接字。
也就意味着,已连接套接字只能和一个对端进行通信。
而未连接套接字显然可以和任何多个对端通信。
(3) 已连接套接字发生异步消息会返回给进程,因为此时已经知道目的套接字。而未连接套接字不会返回给进程。
注意:一般对客户端的UDP套接字进行connect,而服务器端还是sendto和recvfrom,connect只会影响本地套接字。
注意:我们可以对一个已连接UDP套接字指向多次connect,而TCP不可以。以下情况多次connect
(1) 指定新的IP地址和端口号
(2) 断开套接字。此时把套接字地址结构的地址族(IPv4的sin_family)设为AF_UNSPEC就可以了。
7. 性能
当我们对一个未连接的UDP套接字连续sendto两次,看看具体步骤:
连接套接字
发送第一个数据报
断开套接字
连接套接字
发送第二个数据报
断开套接字
如果两个数据报是同一个目的套接字,则我们应该使用显然connect,之后会提高效率。因为这样只需要一个连接和断开。
Unix中一个连接需要耗费一次UDP传输的三分之一的开销。
8. 我们修订上面的客户端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地址和端口号
1. 未连接的UDP套接字,如果我们没有bind,
则当sendto时,内核选择一个本地IP地址和端口号,所以同一主机上两次连续的sendto,两个消息的源IP地址和端口号可能都不一样。
而且,服务器端接收recvfrom后,回射消息,sendto时,可能造成回射消息的源IP地址和端口号和recvfrom消息的目的IP地址和端口号不一样。
2. 已连接UDP套接字,如果没有bind
同一个客户端,多次connect同一个目的IP端口号时,已连接套接字的本地IP和端口号可能都是不一样的。
我们可以使用getsockname来获取已连接UDP套接字的本地IP和端口号。
我们使用recvmsg来获取未连接的UDP套接字的本地IP和端口号。
七、我们使用select的TCP和UDP来重写回射服务器
#include "unp" #define MAXLINE 1024 #define PORT 13 #define MAXCON 5 void err_sys(const char* s) { fprintf(stderr, "%s\n", s); exit(1); } void str_echo(int sockfd) { int nbyte; char buff[MAXLINE]; again: while((nbyte=read(sockfd, buff, MAXLINE)>0)) { write(sockfd, buff, nbyte); } if(nbyte<0 &&errno==EINTR) goto again; else if(nbyte<0) err_sys("read error"); } void sig_chld(int); int main(int argc, char** argv) { int listenfd, connfd, udpfd; pid_tchildpid; char recvmsg[MAXLINE]; const int on=1; struct sockaddr_in servaddr,cliaddr; socklen_t len; listenfd=socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.port=htons(PORT); servaddr.sin_addr.s_addr=htonl(INADDR_ANY); setsockopt(listenfd,SOL_SOCKET, SO_REUSEADDR, &on,sizeof(on)); bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)); listen(listenfd,MAXCON); udpfd=socket(AF_INET,SOCK_DGRAM, 0); bind(udpfd, (struct sockaddr*) &servaddr, sizeof(servaddr)); fd_set fset; FD_ZERO(&fset); int maxfdp1=max(listenfd,udpfd)+1; int nready; signal(SIGCHLD,sig_chld); while(true) { FD_SET(listenfd,&fset); FD_SET(udpfd,&fset); nready=select(maxfdp1,fset,NULL,NULL,NULL); if(nready<0) { if(errno==EINTR) continue; else err_sys("select error"); } if(FD_ISSET(listenfd,&fset)) { connfd=accept(listenfd,NULL,NULL); if((childpid=fork())==0){ close(listenfd); str_echo(connfd); exit(0); } close(connfd); } if(FD_ISSET(udpfd, &fset)) { int nbytes; len=siezof(cliaddr); nbytes=recvfrom(udpfd,recvmsg, MAXLINE,0, (struct sockaddr*)&cliaddr,&len); sendto(udpfd,recvmsg,nbytes,0, (struct sockaddr*)&cliaddr,len); } } }
这里涉及select的IO复用,信号处理,fork子进程,TCP服务器,UDP服务器,套接字选项。
这里的str_echo函数和第五章的相同,信号处理函数并没有实现,注意函数里要调用waitpid。
注意这里有意思的地方:就是我们只是使用select监听TCP的监听套接字和UDP套接字。对于已连接的TCP套接字使用子进程来处理。
即TCP的并行服务器和UDP的迭代服务器。
前面我们都是使用select来监听TCP监听套接字和已连接套接字,使得程序完全的单进程。这里并没有这么做,因为那样太麻烦,这里只是展示了使用select同时监听TCP连接和UDP连接。很简洁,很有意思,这段代码仔细看。很多有意思的地方。
注意这里:同一台服务器,TCP套接字和UDP套接字使用同一个端口,这是可以的。