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;
}