UDP浅析!!!(客户-服务通信源码)

UDP协议的特点:

1,无连接,也就是说:UDP内部并没有维护端到端的状态,这跟TCP是不一样的。UDP不需要三次握手,

2,基于消息的数据传输服务,传输的是数据报,跟TCP基于字节流是不一样的,不会出现所谓的粘包问题,就是这些

      数据报是有边界的,而TCP是没有边界的

3,不可靠,表现在数据报可能会丢失,也可能会重复,也可能会乱序,缺乏流量控制


所以说:一般情况下,UDP是比TCP更加高效的

有些场合更适合使用UDP,使用UDP编写的一些常见的应用程序有:DNS(域名系统),NFS(网络文件系统)和

SNMP(简单网络管理协议)


基本模型:

也是需要服务端和客户端的

服务端创建socket,bind,接下来调用recvfrom(因为不需要建立连接)一直阻塞,直到收到来自客户的数据

对于客户端而言:创建socket,接下来也是直接调用sendto函数给服务器发送数据报,其中必须指定目的地(即服务

器)的地址作为参数,这时recvfrom才会收到数据,并且对数据进行处理,recvfrom将与所接受的数据报一道返回客

户的协议地址,因此服务器可以把响应发送给正确的客户。服务器这时会调用sendto函数,然后返回,客户端调用

recvfrom接受,最后调用close函数


UDP的简单回射客户/服务器


当然,这里面需要用到recvfrom,sendto函数,我们来看看:

 #include 
       #include 

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

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

//第一个:套接字socket
//第二个:缓冲区
//第三个:接收的长度
//第四个:flags,我们用0来表示
//第五个,第六个:指定对方的地址信息



UDP服务端:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERR_EXIT(m)\
	do\
	{\
		perror(m);\
		exit(EXIT_FAILURE);\
	}while(0)


void echo_srv(int sock)
{
	char recvbuf[1024] = {0};
	struct sockaddr_in peeraddr;
	socklen_t peerlen;
	int n;                      //接受到的字节数

	while(1)
	{
		peerlen = sizeof(peeraddr);
		memset(recvbuf, 0, sizeof(recvbuf));
		n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&peeraddr, &peerlen);
		
		if(n == -1)
		{
			if(errno == EINTR)
				continue;
			ERR_EXIT("recvfrom");	
		}

		else if(n > 0)
		{
			fputs(recvbuf, stdout);
			sendto(sock, recvbuf, n, 0, (struct sockaddr*)&peeraddr, peerlen);		
		}
	}

	close(sock);
}


int main(void)
{
	//UDP通信的第一步创建一个套接字
	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(5188);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//INADDR_ANY本机的任意地址

	if(bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
		ERR_EXIT("bind");

	echo_srv(sock);

	return 0;
}

UDP客户端:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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(5188);
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	
	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};
	while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
	{
			sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
			recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
			//表示对等方的地址可以不要了

			fputs(recvbuf, stdout);
			memset(sendbuf, 0, sizeof(sendbuf));
			memset(recvbuf, 0, sizeof(recvbuf));
	}
	close(sock);
}

int main(void)
{
	//UDP通信的第一步创建一个套接字
	int sock;
	if((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
		ERR_EXIT("socket");

	echo_cli(sock);
	
	return 0;
}


那么客户端的地址,是在什么时候绑定的呢???

是在第一次sendto的时候,

因为我们前面已经知道了我客户端要跟谁(服务端)连接,我sendto的时候,会将服务端的信息加入到socket,所以我们调用sendto的时候,也就间接的知道啦跟谁进行连接,,,,

socket的结构体中:是知道客户端/服务端的端口号,IP地址


套接口:

两个属性:本地地址(通过getsockname来获取),远程地址(通过getpeername来获取)

(前提是:这个套接字是一个已连接套接字)

当前我们的套接字是一个未连接的套接字,那么我们就可以通过getsockname来获取本地地址

也就是说:对于客户端,我们的本地地址不是通过bind函数来进行绑定的,我们是通过sendto函数来绑定的,第一次

调用的时候就会绑定,接下来的操作调用是不会再去绑定的,因为我们不会去做多余的操作。很简单



如上,就是我们的UDP通信的过程


注意点:

1,UDP报文可能会丢失,重复,乱序,也就是说:如果我们要确保一个比较可靠的传输的话,我们处理丢失,重复

     这一些问题(针对丢失,那么发送端/接收端就需要超时重传机制,启动定时器)(针对重复,我们应用层要维护

     数据报之间的序号,)

2,UDP缺乏流量控制,UDP也有自己的缓冲区,如果缓冲区满的话,再次接收到的数据不会丢失,而是会覆盖缓冲

     区中的数据,并没有流量控制的机制,不像TCP有滑动窗口协议,如果我们也要实现的话,那么我们可以在应用

     层实现一个模拟的滑动窗口,来简单的控制流量

3,UDP协议数据报文端可能也会截断,因为接收到的数据可能大于缓冲区,多余的数据会被直接丢弃

      我们来看一下这个程序:UDP中,发送的数据大于缓冲区的情况(为了简单方便,我们将客户端和服务端写到

      啦一块)

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERR_EXIT(m)\
	do\
	{\
		perror(m);\
		exit(EXIT_FAILURE);\
	}while(0)


int main(void)
{
	//UDP通信的第一步创建一个套接字
	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(5188);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//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 i;
	int n;
	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[i]);
	}	
	return 0;
}

运行结果:

可以看到,数据后面的bcd都被截断了,所以说:为了避免截断现象,我们必须缓冲区大于所要发送的数据


4,recvfrom可以返回0,因为返回0并不代表连接关闭,因为udp是无连接的

      返回为0,表示不发送任何一个数据,类似于这样:

      sendto(sock, "abcd", 0, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
      这时候,实际发送的数据长度就是一个tcp的头部,IP的头部(都是20个字节),那么总共是40个字节,当然,这

      里不算数字链路的头部,那么对方收到的时候,就代表返回为0,并不代表连接的关闭。。。。

5,ICMP的异步错误

      这里,我们可以简单说一下:

      如上的三个程序:

     

          我们先启动客户端echocli,并且这个客户端从键盘接收输入数据(aaa),调用sendto将数据发送出去,接着

      阻塞到啦recvfrom函数中:

     

      那么实际上对等方并没有启动,但是我们的sendto并没有捕捉到这个信息,仍然阻塞到recvfrom函数:

      为什么呢????

      因为,此刻产生了一个异步错误。。。。异步错误是不会返回给套接口的。那么,为什么说产生了异步错误,首

      先:sendto是不会出错的,因为UDP是无连接的,sendto不需要维护状态,sendto仅仅是将应用层的数据拷贝到

      套接口的缓冲区中,仅仅只是拷贝,并不代表数据已经发送给对方了,只要缓冲区中有数据,那么套接口就可以

      拷贝,但是,但是,数据是不会到达对等方的,这时就会产生一个ICMP的错误。也就是说:TCP/IP协议栈会有

      一个报文的应答。。。报文应答给我们的客户端。。。。而这个错误,是在接受的时候才会产生的(调用

      recvfrom)

      我们称之为异步的iCMP错误。。。


      这个错误被延迟到啦recvfrom才会被通知,,,但是,但是,recvfrom也不会被通知,因为TCP/IP规定,这个异

      步错误,是不能发送给未连接的套接字的。。。。


      recvfrom会一直阻塞

      我们来将上面的程序改改(echocli.c):

        int ret;
        
        char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};
        while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
                        sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
                        ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
                        //表示对等方的地址可以不要了

                        if(ret == -1)
                        {
                                if(errno == EINTR)
                                        continue;
                                ERR_EXIT("recvfrom");                           
                        }

//其他程序没有改变 
          可以预测到:如果recvfrom能接受到应答,那么就会提示出错,并且退出程序。。。。

          我们再次运行结果:

         

          程序还是没有退出。。。

          recvfrom继续阻塞。。。


6,如何处理上面这个问题呢???

      UDP也可以调用connect函数,,,

     

        int ret;        

        connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
        char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};

        运行结果:

       

        可以发现,这个套接字可以返回给已连接套接字,表示连接被拒绝,,,,

        但是,UDP建立的也不是跟TCP一样的连接啊,,,

        UDP在调用connect的时候,实际上并不做三次握手,,,并没有跟对方在传递数据,只是维护了套接字的一种

        状态,,,通过这个数据能够发送数据给对等方。。

         通过这个数据能够发送数据给对等方,只能发送给这个对等方(相当于:远程地址得到了一个绑定,远程地址只

         能是我们所能连接的地址,这个时候,这个套接字就不能发送数据给其他地址了)


         通过运行(先打开服务端),我们可以知道客户端还是可以正常收发数据的。。。


         上面的意思,也就是sendto的时候,已经指定了对方的地址,所以我们可以不用指定对方的地址了,,,

        

          sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);   //不指定地址
         也能成功,也能正常的收发,,,


         同时,这里的sendto也是可以该为send和write的,,,

        

         send(sock, sendbuf, strlen(sendbuf), 0);
         同时,recvfrom也可以通过recv来替换,,,,


7,UDP的外出接口

      假如客户端有多个IP的时候(192.168.1.23, 192.168.2.31)服务端有(192.168.1.33,192.168.2.33)

      那么当我们的sendto连接192.168.1.33,就会自动的跟192.268.1.23进行通信

      sendto(sock, 192.168.1.33,,,,,,,,),sock自动的连接到啦192.268.1.23


      当然,也可以调用connect连接地址,但是并没有绑定端口,端口的绑定是在sendto

      如:connect(sock, 192.168.1.33);那么数据是从192.168.1.23这张网卡出去的,但是地址可就限定了


     

你可能感兴趣的:(网络编程)