TCP 协议是面向连接的基于流的,可靠的传输服务。UDP是无连接的,基于数据报的,不可靠的传输服务,UDP没有粘包,但是会产生丢包。
UDP模型如下:
可以看到,服务器端不用listen,也不用accept。而客户端,也不用connect。
总结UDP的特点如下:
1、无连接
2、基于消息的数据传输服务
3、不可靠
4、一般情况下UDP更加高效
注意点:
1、UDP报文可能会丢失重复
2、UDP报文可能会乱序
3、UDP缺乏流量控制
4、UDP缓冲区写满后,没有流量控制机制,会覆盖缓冲区
5、UDP协议数据报文截断
如果接收到的数据报,大于缓冲区,报文可以被截断,后面的部分会丢失
6、recvfrom返回0,不代表连接关闭,因为UDP是无连接的
例如:sendto可以发送数据0包,只包含UDP头部,这时候recvfrem就会返回0
7、ICMP异步错误
观察现象:
关闭UDP服务端,如启动UDP客户端,从键盘接收数据后,再发送数据。UDP客户端会阻塞在recvfrom位置(因为没有对端给本机发),sendto是将数据写到
套接字缓冲区,UDP协议栈会选择时机发送。
说明:
1、UDP发送报文时,只把数据copy到数据缓冲区,在服务器没有起来的情况下可以发送成功。
2、所谓ICMP异步错误是指:发送报文的时候,没有错误,recvfrom接收报文的时候,会收到ICMP应答。
3、异步错误,是无法返回未连接的套接字,UDP也可以调用connect。
8、UDP connect
UDP调用connect,并没有三次握手,只是维护了一个状态信息(和对等方的)
一旦调用connect,就可以使用send函数
简单的UDP回射服务器程序如下:
服务器:
1 #include <netinet/in.h> 2 #include <arpa/inet.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <errno.h> 6 #include <string.h> 7 8 9 void echo_srv(int sock) 10 { 11 char recvbuf[1024] = {0}; 12 struct sockaddr_in peeraddr; 13 socklen_t peerlen; 14 int n; 15 16 while(1) 17 { 18 peerlen = sizeof(peeraddr); 19 memset(recvbuf, 0, sizeof(recvbuf)); 20 21 n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&peeraddr, 22 &peerlen); 23 24 if(n == -1) 25 { 26 if(errno == EINTR) 27 continue; 28 else 29 { 30 perror("recvfrom error"); 31 exit(0); 32 } 33 } 34 else if(n > 0) 35 { 36 int ret = 0; 37 fputs(recvbuf, stdout); 38 ret = sendto(sock, recvbuf, n, 0, (struct sockaddr*)&peeraddr, peerlen); 39 } 40 } 41 42 close(sock); 43 } 44 45 46 int main() 47 { 48 int sock; 49 50 sock = socket(AF_INET, SOCK_DGRAM, 0); 51 52 if(sock < 0) 53 { 54 perror("socket error"); 55 exit(0); 56 } 57 58 struct sockaddr_in servaddr; 59 memset(&servaddr, 0, sizeof(servaddr)); 60 61 servaddr.sin_family = AF_INET; 62 servaddr.sin_port = htons(8002); 63 64 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 65 66 if(bind(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) 67 { 68 perror("bind error"); 69 exit(0); 70 } 71 72 echo_srv(sock); 73 return 0; 74 }
客户端:
1 #include <netinet/in.h> 2 #include <arpa/inet.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <errno.h> 6 #include <string.h> 7 8 9 10 void echo_cli(int sock) 11 { 12 struct sockaddr_in servaddr; 13 memset(&servaddr, 0, sizeof(servaddr)); 14 15 servaddr.sin_family = AF_INET; 16 servaddr.sin_port = htons(8002); 17 18 servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 19 20 int ret = 0; 21 char sendbuf[1024] = {0}; 22 char recvbuf[1024] = {0}; 23 24 while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) 25 { 26 sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, 27 sizeof(servaddr) 28 ); 29 ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL); 30 31 if(ret == -1) 32 { 33 if(errno == EINTR) 34 continue; 35 else 36 { 37 perror("recvfrom error"); 38 exit(0); 39 } 40 } 41 42 fputs(recvbuf, stdout); 43 memset(sendbuf, 0, sizeof(sendbuf)); 44 memset(recvbuf, 0, sizeof(recvbuf)); 45 46 } 47 48 close(sock); 49 } 50 51 52 int main() 53 { 54 int sock; 55 sock = socket(AF_INET, SOCK_DGRAM, 0); 56 57 if(sock < 0) 58 { 59 perror("socket error"); 60 exit(0); 61 } 62 63 echo_cli(sock); 64 65 return 0; 66 }
运行结果如下:
用netstat - na看网络状态如下:
UDP和TCP不一样,不存在11种状态,因此,我们只能看到一个服务器端的套接字,服务器端执行了bind,所以会显示这个套接字。
报文截断:
如果接收到的数据报,大于缓冲区,报文可以被截断,后面的部分会丢失
实验程序如下:
1 #include <unistd.h> 2 #include <sys/types.h> 3 #include <sys/socket.h> 4 #include <netinet/in.h> 5 #include <stdlib.h> 6 #include <stdio.h> 7 #include <errno.h> 8 #include <string.h> 9 10 #define ERR_EXIT(m) 11 do 12 { 13 perror(m); 14 exit(EXIT_FAILURE); 15 } while(0) 16 17 int main(void) 18 { 19 int sock; 20 if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0) 21 ERR_EXIT("socket"); 22 23 struct sockaddr_in servaddr; 24 memset(&servaddr, 0, sizeof(servaddr)); 25 servaddr.sin_family = AF_INET; 26 servaddr.sin_port = htons(8003); 27 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 28 29 if (bind(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) 30 ERR_EXIT("bind"); 31 32 33 sendto(sock, "ABCD", 4, 0, (struct sockaddr*)&servaddr, sizeof(servaddr)); 34 35 //数据报方式。。。。不是字节流 36 //如果接受数据时,指定的缓冲区的大小,较小; 37 //剩余部分将要截断,扔掉 38 char recvbuf[1]; 39 int n; 40 int i; 41 for (i=0; i<4; i++) 42 { 43 n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL); 44 if (n == -1) 45 { 46 if (errno == EINTR) 47 continue; 48 ERR_EXIT("recvfrom"); 49 } 50 else if(n > 0) 51 printf("n=%d %c ", n, recvbuf[0]); 52 } 53 return 0; 54 }
这是一个自己发自己收的UDP程序,第38行我们定义的缓冲区为1字节大小,而33行发送的大小是4字节,recvfrom接收时会一次取出4字节,但是只放一字节到recvbuf中,其他的三字节被丢弃。 我们想看到的现象是recvfrem一个字节一个字节的接收,但是UDP是数据报协议,recvfrom一次接收一个数据报。跟TCP不一样。
下面做一个只启动客户端,不启动服务器的实验,程序如下:
1 #include <netinet/in.h> 2 #include <arpa/inet.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <errno.h> 6 #include <string.h> 7 8 9 10 void echo_cli(int sock) 11 { 12 struct sockaddr_in servaddr; 13 memset(&servaddr, 0, sizeof(servaddr)); 14 15 servaddr.sin_family = AF_INET; 16 servaddr.sin_port = htons(8002); 17 18 servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 19 20 int ret = 0; 21 char sendbuf[1024] = {0}; 22 char recvbuf[1024] = {0}; 23 24 while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) 25 { 26 ret = sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, 27 sizeof(servaddr) 28 ); 29 printf("sendto %d bytes ", ret); 30 printf("send success "); 31 32 ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL); 33 34 if(ret == -1) 35 { 36 if(errno == EINTR) 37 continue; 38 else 39 { 40 perror("recvfrom error"); 41 exit(0); 42 } 43 } 44 45 fputs(recvbuf, stdout); 46 memset(sendbuf, 0, sizeof(sendbuf)); 47 memset(recvbuf, 0, sizeof(recvbuf)); 48 49 } 50 51 close(sock); 52 } 53 54 55 int main() 56 { 57 int sock; 58 sock = socket(AF_INET, SOCK_DGRAM, 0); 59 60 if(sock < 0) 61 { 62 perror("socket error"); 63 exit(0); 64 } 65 66 echo_cli(sock); 67 68 return 0; 69 }
只启动客户端运行,结果如下:
UDP也可以调用connect,但是并没有三次握手,只是维护了一个状态信息(和对等方的),实验程序如下:
1 #include <unistd.h> 2 #include <sys/types.h> 3 #include <sys/socket.h> 4 #include <netinet/in.h> 5 #include <arpa/inet.h> 6 #include <stdlib.h> 7 #include <stdio.h> 8 #include <errno.h> 9 #include <string.h> 10 11 #define ERR_EXIT(m) 12 do 13 { 14 perror(m); 15 exit(EXIT_FAILURE); 16 } while(0) 17 18 void echo_cli(int sock) 19 { 20 struct sockaddr_in servaddr; 21 memset(&servaddr, 0, sizeof(servaddr)); 22 servaddr.sin_family = AF_INET; 23 servaddr.sin_port = htons(8002); 24 servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 25 26 //3 udp 也可以 调用connet 27 //udp调用connet,并没有三次握手,只是维护了一个状态信息(和对等方的)。。。 28 connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)); 29 30 int ret; 31 char sendbuf[1024] = {0}; 32 char recvbuf[1024] = {0}; 33 while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) 34 { 35 //2如果 connect 已经指定了对方的地址。 36 //send可以这样写 sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0); 37 38 //1sendto第一次发送的时候,会绑定地址 39 sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr)); 40 /*sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);*/ 41 42 //一但调用connect,就可以使用send函数 43 //send(sock, sendbuf, strlen(sendbuf), 0); 44 ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL); 45 if (ret == -1) 46 { 47 if (errno == EINTR) 48 continue; 49 ERR_EXIT("recvfrom"); 50 } 51 52 fputs(recvbuf, stdout); 53 memset(sendbuf, 0, sizeof(sendbuf)); 54 memset(recvbuf, 0, sizeof(recvbuf)); 55 } 56 57 close(sock); 58 59 60 } 61 62 int main(void) 63 { 64 int sock; 65 if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0) 66 ERR_EXIT("socket"); 67 68 echo_cli(sock); 69 70 return 0; 71 }
一旦调用了connect,就可以使用send函数发送数据了,不在必须使用sendto。使用send发送数据时,目标地址是connect中绑定的地址。只启动客户端,执行结果如下:
上述程序我们只是在28行加上了connect,如果不加这个函数,客户端会阻塞在recvfrem处。调用了connect后,情况就有点不一样了,sendto还是正常发送数据,但是执行到recvfrom处接收到了ICMP报文,UDP接收到这个异常报文后,给recvfrom返回错误,显示连接拒绝,直接退出客户端。