一、下图是典型的UDP客户端/服务器通讯过程
下面依照通信流程,我们来实现一个UDP回射客户/服务器
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
当套接字处于“已连接”的状态时,才可以使用send,当flags = 0 时 send 与 write 一致。
且 send(sockfd, buf, len, flags); 即 sendto(sockfd, buf, len, flags, NULL, 0);
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);
recv 与 recvfrom 的关系与 send 与 sendto 的关系一致。
<nobr>1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br> 10<br> 11<br> 12<br> 13<br> 14<br> 15<br> 16<br> 17<br> 18<br> 19<br> 20<br> 21<br> 22<br> 23<br> 24<br> 25<br> 26<br> 27<br> 28<br> 29<br> 30<br> 31<br> 32<br> 33<br> 34<br> 35<br> 36<br> 37<br> 38<br> 39<br> 40<br> 41<br> 42<br> 43<br> 44<br> 45<br> 46<br> 47<br> 48<br> 49<br> 50<br> 51<br> 52<br> 53<br> 54<br> 55<br> 56<br> 57<br> 58<br> 59<br> 60<br> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br> 69<br> 70<br> 71<br> 72<br> 73<br> 74<br> 75<br></nobr>
|
/************************************************************************* >FileName:echoser_udp.c >Author:Simba >Mail:[email protected] >CreatedTime:Sun03Mar201306:13:55PMCST ************************************************************************/ #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<string.h> #defineERR_EXIT(m)\ do{\ perror(m);\ exit(EXIT_FAILURE);\ } while( 0) voidecho_ser( intsock) { charrecvbuf[ 1024]={ 0}; structsockaddr_inpeeraddr; socklen_tpeerlen; intn; while( 1) { peerlen= sizeof(peeraddr); memset(recvbuf, 0, sizeof(recvbuf)); n=recvfrom(sock,recvbuf, sizeof(recvbuf), 0, ( structsockaddr*)&peeraddr,&peerlen); if(n==- 1) { if(errno==EINTR) continue; ERR_EXIT( "recvfromerror"); } else if(n> 0) { fputs(recvbuf,stdout); sendto(sock,recvbuf,n, 0, ( structsockaddr*)&peeraddr,peerlen); } } close(sock); } intmain( void) { intsock; if((sock=socket(PF_INET,SOCK_DGRAM, 0))< 0) ERR_EXIT( "socketerror"); structsockaddr_inservaddr; 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,( structsockaddr*)&servaddr, sizeof(servaddr))< 0) ERR_EXIT( "binderror"); echo_ser(sock); return 0; } |
<nobr>1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br> 10<br> 11<br> 12<br> 13<br> 14<br> 15<br> 16<br> 17<br> 18<br> 19<br> 20<br> 21<br> 22<br> 23<br> 24<br> 25<br> 26<br> 27<br> 28<br> 29<br> 30<br> 31<br> 32<br> 33<br> 34<br> 35<br> 36<br> 37<br> 38<br> 39<br> 40<br> 41<br> 42<br> 43<br> 44<br> 45<br> 46<br> 47<br> 48<br> 49<br> 50<br> 51<br> 52<br> 53<br> 54<br> 55<br> 56<br> 57<br> 58<br> 59<br> 60<br> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br></nobr>
|
/************************************************************************* >FileName:echocli_udp.c >Author:Simba >Mail:[email protected] >CreatedTime:Sun03Mar201306:13:55PMCST ************************************************************************/ #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> #defineERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ } while( 0) voidecho_cli( intsock) { structsockaddr_inservaddr; 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"); intret; charsendbuf[ 1024]={ 0}; charrecvbuf[ 1024]={ 0}; while(fgets(sendbuf, sizeof(sendbuf),stdin)!= NULL) { sendto(sock,sendbuf,strlen(sendbuf), 0,( structsockaddr*)&servaddr, sizeof(servaddr)); 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); } intmain( void) { intsock; if((sock=socket(PF_INET,SOCK_DGRAM, 0))< 0) ERR_EXIT( "socket"); echo_cli(sock); return 0; } |
编译运行server,在两个终端里各开一个client与server交互,可以看到server具有并发服务的能力。用Ctrl+C关闭server,然后再运行server,此时client还能和server联系上。和前面TCP程序的运行结果相比较,我们可以体会无连接的含义。
二、UDP编程注意点
1、UDP报文可能会丢失、重复
2、UDP报文可能会乱序
3、UDP缺乏流量控制
4、UDP协议数据报文截断
5、recvfrom返回0,不代表连接关闭,因为udp是无连接的。
6、ICMP异步错误
7、UDP connect
8、UDP外出接口的确定
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很多保证通讯可靠性的机制需要在应用层实现,即123点所提到的。
对于第4点,可以写个小程序测试一下:
<nobr>1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br> 10<br> 11<br> 12<br> 13<br> 14<br> 15<br> 16<br> 17<br> 18<br> 19<br> 20<br> 21<br> 22<br> 23<br> 24<br> 25<br> 26<br> 27<br> 28<br> 29<br> 30<br> 31<br> 32<br> 33<br> 34<br> 35<br> 36<br> 37<br> 38<br> 39<br> 40<br> 41<br> 42<br> 43<br> 44<br> 45<br> 46<br> 47<br> 48<br> 49<br> 50<br> 51<br> 52<br></nobr>
|
#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> #defineERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ } while( 0) intmain( void) { intsock; if((sock=socket(PF_INET,SOCK_DGRAM, 0))< 0) ERR_EXIT( "socket"); structsockaddr_inservaddr; 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,( structsockaddr*)&servaddr, sizeof(servaddr))< 0) ERR_EXIT( "bind"); sendto(sock, "ABCD", 4, 0,( structsockaddr*)&servaddr, sizeof(servaddr)); charrecvbuf[ 1]; intn; inti; for(i= 0;i< 4;i++) { /*udp是报式协议,即若一次性接收的空间小于发来的数据,有可能造成报文截断, *但一定没有tcp的粘包问题*/ 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[ 0]); } return 0; } |
上述程序是自己发送数据给自己,发送了4个字节,但我们只提供1个字节的缓冲区recvbuf,第一次recvfrom 读取一个字节,但接下去循环却读不到剩下的数据了,因为udp 是报式协议,如果一次性接收的缓冲区小于发来的数据,有可能造成报文截断,反观tcp流式协议,可以一次读取一个数据包的一部分,也可以一次性读取多个数据包,但这也正是其会造成粘包问题的来源,所以也说udp 协议不会有粘包问题,因为一次就接收一个消息。输出如下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./trunc
n=1 A
............
接收了一个字符之后,再次recvfrom 就阻塞了。
对于第5点,如果我们使用sendto 发送的数据大小为0,则发送给对方的是只含有各层协议头部的数据帧,recvfrom 会返回0,但并不代表对方关闭连接,因为udp 本身没有连接的概念。
第678点合起来一起讲,可以看到我们的客户端程序现在没有调用connect,不运行服务器程序,直接运行客户端程序,查看现象:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_udp
dfsaf
................
当我们在键盘敲入几个字符,sendto只是把Buf的数据拷贝到sock对应的缓冲区中,此时服务器未开启,协议栈返回一个ICMP异步错误,但因为前面没有调用connect“建立”一个连接,则recvfrom时不能收到这个错误而一直阻塞。现在我们在while 循环的外面添加一句:connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)); 再次测试一下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_udp
dfsaf
recvfrom: Connection refused
此时recvfrom 就能接收到这个错误而返回了,并打印错误提示。
其实connect并没有真正建立一个连接,即没有3次握手过程,只是维护了一种状态,绑定了远程地址,因为如此在调用sendto 时也可以不指定远程地址了,如sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0); 甚至也可以使用send 函数
send(sock, sendbuf, strlen(sendbuf), 0);
假设现在客户端有多个ip地址,由connect 或 sendto 函数提供的远程地址的参数,系统会选择一个合适的出口,比如远程ip 是192.168.2.10, 而客户端现在的ip 有 192.168.1.32 和 192.168.2.75 那么会自动选择192.168.2.75 这个ip 出去。
参考:
《Linux C 编程一站式学习》
《TCP/IP详解 卷一》
《UNP》