UDP协议及通信编程

1 概述

Internet 的传输层有两个主要协议,一个是TCP,一个是UDP:
TCP协议:传输控制协议(TCP,Transmission Control Protocol);
UDP协议:用户数据包协议(UDP,User Datagram Protocol)。
两者的主要区别:

  1. 连接:TCP面向连接,即必须首先建立与对方的连接,才可以收发数据;而UDP无需建立连接就可以发送封装的 IP 数据包。
  2. 可靠性:TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议;而UDP提供了无连接通信,且不对传送数据包进行可靠性保证,适合于一次传输少量数据,UDP传输的可靠性由应用层负责。UDP报文没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可以保障可靠性的应用程序,如DNS、TFTP、SNMP等。
  3. 数据格式:TCP传输的是数据流,没有包的概念,而UDP是报文格式,即一个一个包的形式。

2 UDP编程简介

2.1 编程接口

UDP编程有很多方法和接口,这里仅介绍Windows下的编程,其他系统类似。
Windows API 提供了SOCKET编程函数库,可用于进行UDP编程实现。

2.2 UDP编程类型

做过TCP编程的应该都知道,TCP编程要分为两类:服务器和客户端。
服务器和客户端是一种针对一个通信事务来说的双方角色的定义,开好端口等待其他程序来连接(listen)的那一方就是服务器;而主动去连接(connect)一个特定IP和端口的那一方就是客户端。一个程序到底是服务器,还是客户端,或者即是服务器,又是客户端,需要根据自己程序的功能要求来确定。
同样,UDP编程也分为服务器和客户端两类,也是这个规则:开好监听端口等待其他程序往此端口发送数据的那一方就是服务器,而主动往一个特定IP和端口发送数据的那一方就是客户端。

2.3 UDP程序流程

一般来说,一个UDP程序流程如下:

2.3.1 UDP服务器程序流程

  1. 创建通信套接字SOCKET:使用函数socket();
  2. 设定监听端口及IP:使用函数bind();
  3. 接收客户端数据:使用函数recvfrom();
  4. 向客户端发送数据:使用函数sendto();
  5. 通信结束后,关闭套接字:使用函数closesocket();

2.3.2 UDP客户端程序流程

  1. 创建通信套接字SOCKET:使用函数socket();
  2. 向服务器发送数据:使用函数sendto();
  3. 接收服务器数据:使用函数recvfrom();
  4. 通信结束后,关闭套接字:使用函数closesocket();

3 UDP编程实例及注意事项

举例:两个计算机进行UDP通信,服务器一方IP为192.168.1.100,UDP监听端口为2000,客户端一方IP为192.168.1.200。

3.1 服务器程序

1) 创建通信套接字SOCKET

SOCKET m_sockUdpRecv;
int m_nError;
if(m_sockUdpRecv==INVALID_SOCKET)
{
    
    
	m_sockUdpRecv = socket(AF_INET, SOCK_DGRAM, 0);
	if (m_sockUdpRecv == INVALID_SOCKET)
	{
    
    
		m_nError = WSAGetLastError();
		return -1;
	}
}

socket函数的第二个参数为SOCK_DGRAM,表示要创建一个UDP通信的套接字。

2) 设定监听端口及IP

SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(2000); //UDP监听端口为2000
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);	//INADDR_ANY表示允许任何IP进入
//如果希望仅允许特定IP进入,如192.168.1.200,那么需要修改为:
//addrSrv.sin_addr.S_un.S_addr = htonl(inet_ntoa(“192.168.1.200”));
int retVal = bind((SOCKET)m_sockUdpRecv, (const sockaddr*)(LPSOCKADDR)&addrSrv,
	(int)sizeof(SOCKADDR_IN));
if(retVal == SOCKET_ERROR)
{
    
    
	m_nError = WSAGetLastError();
	closesocket(m_sockUdpRecv);
	m_sockUdpRecv=INVALID_SOCKET;
	return -1;
}

这样服务器端程序就在端口2000开了个UDP监听,用来客户端向这个端口发送数据。

3) 接收客户端数据

char pBuf[1024];	//接收缓冲区
int nLen=1024;	//希望接收的最大数据长度
struct sockaddr_in m_addrUdpFrom;	//用来接收对方信息
int addrlen = sizeof( struct sockaddr_in);
CString sREmoteInfo;
int ret;
ret = recvfrom(m_sockUdpRecv,(char*)pBuf,nLen,0,
	(struct sockaddr*)&m_addrUdpFrom,&addrlen);
if(ret>=0)
{
    
    
	//此时sRemoteInfo中记录了客户端的IP及使用端口,我们可以如下所示来查看
	sRemoteInfo.Format("%s:%d",
		inet_ntoa(m_addrUdpFrom.sin_addr),ntohs(m_addrUdpFrom.sin_port));
}

这里要注意,通常情况下,recvfrom函数是阻塞的,即除非接收到了客户端发来的数据,或者出现网络错误,否则程序会永远卡在recvfrom这一句。
如果不想recvfrom阻塞,一种办法是设置接收超时,即:在调用recvfrom前,执行下列语句

struct timeval tv_out;
tv_out.tv_sec = 1;//设置recvfrom超时时间为1秒
tv_out.tv_usec = 0;
setsockopt(m_sockUdpRecv,SOL_SOCKET,SO_RCVTIMEO,(const char*)&tv_out, sizeof(tv_out));

这样,recvfrom在设定的超时时间内如果没有等到数据到来或者网络错误,函数也会执行结束,并返回一个网络错误。
以上使用的setsockopt函数是一个用来设置通信选项和参数的函数,比如我们要接收的数据比较多,可能超出UDP缺省缓冲区大小,此时,我们就可以在recvfrom前用这个函数设置一下接收缓冲区大小

int nSendBuf=32*1024;//设置为32K
setsockopt(m_sockUdpRecv,SOL_SOCKET,SO_RCVBUF,(const char*)&nSendBuf,sizeof(int));

要注意的是:UDP通信的包字节数最大为64K。除去各种头信息,最大可用数据大概为65507(感兴趣的自行搜索相关文章)。

4) 向客户端发送数据

如果程序需要向客户端发送数据,那么我们刚才已经通过recvfrom获得了对方的IP以及端口(记录在m_addrUdpFrom中),我们可以直接向这个IP和端口回送数据,即直接使用m_addrUdpFrom参数调用sendto,这里我们记为发送模式一。
另外一点是:发送用的SOCKET可以用上面那个m_sockUdpRecv,也可以socket()创建一个来使用,如下面的m_sockUdpSend,很简单,这里不再给出创建socket的代码。

char* pData;	//要发送的数据
int nLen=123;	//要发送的数据长度
int addrlen = sizeof( struct sockaddr_in);
int ret = sendto(m_sockUdpSend, (char*)pData, nLen, 0, (struct sockaddr*)&m_addrUdpFrom, addrlen);

如果客户端不希望服务器向它的数据来源端口发送,而是向另一个指定的端口(例如3000)回送数据,我们记为发送模式二,这样:

SOCKADDR_IN addrTo;
addrTo.sin_family = AF_INET;
addrTo.sin_port = htons(3000);
addrTo.sin_addr.s_addr = inet_addr( inet_ntoa(m_addrUdpFrom.sin_addr) );
int addrlen = sizeof( struct sockaddr_in);
int ret = sendto(m_sockUdpSend, (char*)pData, nLen, 0, (struct sockaddr*)&addrTo, addrlen);

同样,如果发送的数据量比较大,可以提前设置一下发送缓冲区大小:

int nSendBuf=32*1024;//设置为32K
setsockopt(m_sockUdpSend,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));

5) 通信结束后,关闭套接字

所有通信结束后,要关闭套接字,释放资源

if(m_sockUdpRecv!=INVALID_SOCKET)
	closesocket(m_sockUdpRecv);
if(m_sockUdpSend!=INVALID_SOCKET)
	closesocket(m_sockUdpSend);

3.2 客户端程序

1) 创建通信套接字SOCKET

与上文相同,这里不再论述。

2) 向服务器发送数据

客户端要向服务器发送数据时,必须预先知道服务器的IP,以及服务器的UDP监听端口,比如我们这个例子的192.168.1.100:2000:

SOCKADDR_IN addrTo;
addrTo.sin_family = AF_INET;
addrTo.sin_port = htons(2000);	//服务器UDP监听端口
addrTo.sin_addr.s_addr = inet_addr( inet_ntoa("192.168.1.100") );	//服务器IP
int addrlen = sizeof( struct sockaddr_in);
int ret = sendto(m_sockUdpSend, (char*)pData, nLen, 0, (struct sockaddr*)&addrTo, addrlen);

3) 接收服务器数据

前文所述服务器发送数据时,我们记了两个模式,对应这两种模式的客户接收端程序如下:
模式一,服务器直接向客户端的发送端口回送,则客户端的接收程序为:

char pBuf[1024];	//接收缓冲区
int nLen=1024;	//希望接收的最大数据长度
struct sockaddr_in m_addrUdpFrom;	//用来接收对方信息
int addrlen = sizeof( struct sockaddr_in);
CString sREmoteInfo;
int ret;
ret = recvfrom(m_sockUdpSend,(char*)pBuf,nLen,0,
	(struct sockaddr*)&m_addrUdpFrom,&addrlen);
if(ret>=0)
{
    
    
	//此时sRemoteInfo中记录了服务器端回送的IP及端口,我们可以如下所示来查看
	sRemoteInfo.Format("%s:%d",
		inet_ntoa(m_addrUdpFrom.sin_addr),ntohs(m_addrUdpFrom.sin_port));
}

模式二,服务器向客户端的特定端口发送数据,那么这种模式,我们的客户端就是服务器了,采用以上的服务器开启UDP监听端口等待接收发来数据的模式即可。
与上文相同,这里不再论述。

4) 通信结束后,关闭套接字

与上文相同,这里不再论述。

4 UDP通信的应用层可靠性措施讨论

如前所述,UDP属于非连接的不可靠的数据传输,UDP传输的可靠性由应用层负责,也就是说,发送端只负责发送数据,而不管接收端能不能收到,这样就需要我们自己在应用程序中来保证这种可靠性。
一般来说,如果偶尔发出一个不大的包,UDP的接收发送还是相对可靠的,比如聊天程序,用UDP就非常适合。UDP传输的不可靠性多数发生在需要连续发送接收多个包的情况下,比如,我们现在有一个1M字节的数据(如果我来定,1M字节我不会采用UDP来发送,我会选TCP,但是有时候用户可能提出必须UDP)要发送给对方,按照以上所述,一个UDP包最大只能发送60多K,也就是这1M字节数据,最少也要分成大约18个包来发,如果每次UDP发送数据为1K的话,那么就需要分成更多包,比如1024个包来发送。
按照个人经验,应用程序要保证UDP的数据传输可靠,要注意以下几点:

  1. 发送端如果有连续多个UDP包发送时,中间要加延时,否则对方很可能会丢包,比如我一般在多个连续的UDP的发送中间加入Sleep(2),这个延时就是给对方一个接收到包后的处理时间,至于这个延时多少合适,还是需要自己来摸索;
  2. 接收端在接收到一个包后,要尽快处理,或尽快交给其他线程来处理,如果接收包后处理的耗时过多,会导致后续来的UDP包来不及接收,就会丢包。

当然,为了传输可靠,有个很明显的方法,就是采用一问一答模式,即发送过去后,对方在一定时间内回复一个是否接收成功的包,成功了再发下一个包,不成功重发,但是,不建议这样,因为这样就失去了UDP协议传输效率高这个特点了,不如就直接采用TCP来传输就可以了,可靠,同时效率一定比这种一问一答式UDP要高的多。所以,到底采用“可靠的TCP”还是采用“不可靠的UDP+程序保证可靠性”,完全看用户要求或者你的程序设计思想。

猜你喜欢

转载自blog.csdn.net/hangl_ciom/article/details/106969920