1. UDP的特點
無連接
基於消息的數據傳輸服務
不可靠
一般情況下UDP更加高效
不粘包但丟包
UDP客戶/服務基本模型
- UDP注意點
UDP報文可能會丟失、重複
UDP報文可能會亂序
UDP缺乏流量控制
udp緩衝區寫滿以後,沒有流量控制機制,會覆蓋緩衝區。即緩衝區寫滿後,再寫會覆蓋!!
UDP協議數據報文截斷
如果接收到的數據報,大於緩衝區;報文可以被截斷;後面的部分會丟失。
recvfrom返回0,不代表連接關閉,因爲udp是無連接的。
sendto可以發送數據0包。。。只含有udp頭部。
ICMP異步錯誤
觀察現象: 關閉udp服務端,若啓動udp客戶端,從鍵盤接受數據後,再發送數據。udp客戶端阻塞在recvfrom位置;
//說明1:udp發送報文的時,只把數據copy到發送緩衝區。在服務器沒有起來的情況下,可以發送成功。
說明2:所謂ICMP異步錯誤是指:發送的報文的時候,沒有錯誤,接受報文recvfrom的時候,回收到ICMP應答
說明3:異步的錯誤,是無法返回未連接的套接字。udp也可以調用connect
UDP connect
說明1://udp調用connet,並沒有三次握手,只是維護了一個狀態信息(和對等方的)。。。
說明2)//一但調用connect,就可以使用send函數
例1 不啓動服務端,運行客戶端也發送成功
#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>
#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(8002);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
//fgets 自動加上\n
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
////sendto第一次發送的時候,會綁定地址
ret = sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
if(ret != -1)
printf("1-發送成功:%d\n", ret);
//ret = sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
///printf("--step2--\n");
if (-1 == ret)
{
if (errno == EINTR)
continue;
ERR_EXIT("recvfrom");
}
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
}
int main()
{
int sock;
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
echo_cli(sock);
return 0;
ICMP異步錯誤
觀察現象: 關閉udp服務端,若啓動udp客戶端,從鍵盤接受數據後,再發送數據
。udp客戶端阻塞在recvfrom位置;
說明1:
udp發送報文的時,只把數據copy到發送緩衝區。在服務器沒有起來的
情況下,可以發送成功。
說明2:
所謂ICMP異步錯誤是指:發送的報文的時候,沒有錯誤,
接受報文recvfrom的時候,回收到ICMP應答。
這個ICMP錯誤是異步錯誤,由sendto引起的,但是sendto本身卻返回成功!
但是該錯誤是不返回給用戶進程的!!因爲根本沒有連接,
說明3:
異步的錯誤,是無法返回未連接的套接字。udp也可以調用connect
運行結果:第一次發送數據顯示成功,再次發送數據kkk時,程序已經阻塞在recvfrom上了,等待一個永遠不會到達的服務器應答
例1 不啓動服務端,運行客戶端也發送成功,此時使用connect,對等方會返回一個ICMP包,注意
沒有三路握手過程,內核只是檢查是否存在立即可知的錯誤
/*
結論:客戶端調用connet和不調connet的區別。
1) udp也可以調用connet
2) udp客戶端調用了connect以後,不會阻塞在recvfrom函數這裏。
3) 一但調用connect,就可以使用send函數
*/
void echo_cli(int sock)
{
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8002);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//3 udp 也可以 調用connet
//udp調用connet,並沒有三次握手,只是維護了一個狀態信息(和對等方的)。。。
connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//2如果 connect 已經指定了對方的地址。
//send可以這樣寫 sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);
//1sendto第一次發送的時候,會綁定地址
sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
/*sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);*/
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);
}
運行結果:
此時只運行客戶端
例3 測試發送的數據長度爲0
/*
udp是無連接的,
udp發送長度爲0的包,此時會形成
ip首部(ipv4是20字節,ipv6是40字節) + 8字節的udp首部
而沒有數據的IP數據報
接收數據長度爲0也是可以的,不像tcp那樣表示對端關閉
*/
void echo_cli3(int sock)
{
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8002);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//發送長度爲0的包
ret = sendto(sock, sendbuf, 0, 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
printf("sendto發送的長度爲0的數據報:%d\n", ret);
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
printf("recvfrom接收的數據長度爲:%d\n", ret);
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);
}
運行結果:
例4 測試數據包不可靠的,缺乏流量控制
#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>
/*
UDP協議數據報文截斷
如果接收到的數據報,大於緩衝區;報文可以被截斷;後面的部分會丟失
UDP缺乏流量控制
udp緩衝區寫滿以後,沒有流量控制機制,會覆蓋緩衝區。
UDP協議數據報文截斷
如果接收到的數據報,大於緩衝區;報文可以被截斷;後面的部分會丟失。
*/
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
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(8003);
servaddr.sin_addr.s_addr = htonl(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 n;
int i;
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[0]);
}
return 0;
}
運行結果
測試代碼
udpcl2.c
#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>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
/*
結論:客戶端調用connet和不調connet的區別。
1) udp也可以調用connet
2) udp客戶端調用了connect以後,不會阻塞在recvfrom函數這裏。
3) 一但調用connect,就可以使用send函數
*/
void echo_cli(int sock)
{
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8002);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//3 udp 也可以 調用connet
//udp調用connet,並沒有三次握手,只是維護了一個狀態信息(和對等方的)。。。
connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//2如果 connect 已經指定了對方的地址。
//send可以這樣寫 sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);
//1sendto第一次發送的時候,會綁定地址
sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
/*sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);*/
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);
}
void echo_cli2(int sock)
{
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8002);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//3 udp 也可以 調用connet
//udp調用connet,並沒有三次握手,只是維護了一個狀態信息(和對等方的)。。。
connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
#if 0
//2如果 connect 已經指定了對方的地址。
sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);
#else
//一但調用connect,就可以使用send函數
send(sock, sendbuf, strlen(sendbuf), 0);
#endif
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);
}
/*
udp是無連接的,
udp發送長度爲0的包,此時會形成
ip首部(ipv4是20字節,ipv6是40字節) + 8字節的udp首部
而沒有數據的IP數據報
接收數據長度爲0也是可以的,不像tcp那樣表示對端關閉
*/
void echo_cli3(int sock)
{
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8002);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//發送長度爲0的包
ret = sendto(sock, sendbuf, 0, 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
printf("sendto發送的長度爲0的數據報:%d\n", ret);
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
printf("recvfrom接收的數據長度爲:%d\n", ret);
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);
}
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
//echo_cli(sock);
//echo_cli2(sock);
echo_cli3(sock);
return 0;
}
udpsrv3.c
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include "commsocket.h"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0);
#include <signal.h>
void handle(int signum)
{
int pid = 0;
printf("recv signum:%d \n", signum);
//避免殭屍進程
while ((pid = waitpid(-1, NULL, WNOHANG) ) > 0)
{
printf("退出子進程 pid:%d \n", pid);
fflush(stdout);
}
}
int main(void)
{
int ret = 0;
int listenfd;
signal(SIGCHLD, handle);
signal(SIGPIPE, SIG_IGN);
ret = sckServer_init(8001, &listenfd);
if (ret != 0)
{
printf("sckServer_init() err:%d \n", ret);
return ret;
}
while(1)
{
int ret = 0;
int wait_seconds = 5;
int connfd = 0;
ret = sckServer_accept(listenfd, &connfd, wait_seconds);
if (ret == Sck_ErrTimeOut)
{
continue;
}
int pid = fork();
if (pid == 0)
{
unsigned char recvbuf[1024];
int recvbuflen = 12;
close(listenfd);
while(1)
{
memset(recvbuf, 0, sizeof(recvbuf));
ret = sckServer_recv(connfd, recvbuf, &recvbuflen, 5);
if (ret != 0)
{
printf("%s line %d func sckServer_rev() err:%d \n", __FILE__, __LINE__, ret);
break;
}
printf("recvbuflen = %d recvbuf = %s\n ",recvbuflen, recvbuf );
#if 1
ret = sckServer_send(connfd, recvbuf, recvbuflen, 6);
if (ret != 0)
{
printf("%s line %d func sckServer_send() err:%d \n", __FILE__, __LINE__, ret);
break;
}
#endif
}
close(connfd);
exit(ret);
}
else if (pid > 0)
{
close(connfd);
}
}
return 0;
}