您的位置:首页 > 其它

基于UDP协议的简单基本视频传输程序的编写

2014-09-30 10:37 633 查看

一、UDP协议

UDP是User Datagram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议。正式通信前不必与对方先建立连接,直接向接收方发送数据,是一种不可靠的通信协议。正是由于UDP协议不关心网络数据传输的一系列状态,使得UDP协议在数据传输过程中节省了大量的网络状态确认和数据确认的系统资源消耗,大大提高UDP协议的传输效率,传输速度快。TCP(Transport
Control Protocol)协议是面向连接的传输协议,通信前需先建立连接,传输时延较大,TCP的确认和重发机制、流量控制机制虽能保证数据的可靠传输,但处理过程复杂,效率不高,对于音频和视频流,频繁的确认和重传无法保证数据的实时传送,所以相对不适合视频图像的传输。


二、简单视频传输特点

视频图像传输有以下几个特点:1) 要求传输延时小,实时性高; 2) 传输流量大,要求传输效率高;3) 在一定程序上允许传输错误或数据丢失。根据以上特点知,使用UDP协议来传输视频相对TCP协议更理想。

三、基本传输网络搭建



因为未对图像数据进行压缩编码,所以数据量很大,尽量采用千兆网卡,交换机为千兆以太网交换机,连接线缆为六类线。(网线的品质直接影响到数据传输的速度和稳定性,对于千兆网来说,一般都选择超五类线或六类线)。

四、VC程序编写

新建基于对话框的MFC工程时,在创建向导第二步要注意在Would you like to include WOSA support?下勾选Windows Sockets,这样实际在程序的初始化函数中自动初始化并加载了套接字库。其余默认即可。

程序的初始化函数

BOOL CClientApp::InitInstance()

{

if (!AfxSocketInit())

{

AfxMessageBox(IDP_SOCKETS_INIT_FAILED);

return FALSE;

}



}



1.编写网络程序步骤
服务器端

1、 创建套接字

2、 将套接字绑定到一个本地地址和端口上

3、 等待接收数据

4、 关闭套接字

客户端

1、 创建套接字

2、 向服务器发送数据

3、 关闭套接字

客户端代码实现:

[cpp]
view plaincopy





m_sockClient=socket(AF_INET,SOCK_DGRAM,0);//创建套接字,AF_INET表示该套接字在Internet域中进行通信。SOCK_DGRAM指创建的数据报套接字,参数0表示默认的网络协议;

SOCKADDR_IN addrSrv;//定义接收地址的结构体
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);//6000为端口
addrSrv.sin_addr.S_un.S_addr=htonl(dwIP);//dwIP为服务器的IP
sendto(m_sockClient,(char*)(&data),sizeof(data),0,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
//发送数据报函数,第一个参数为套接字,第二个参数为一个指向缓冲区的指针,该缓冲区包含将要发送的数据,第三个参数指定缓冲区数据的长度,第四个参数影响函数的行为,一般设为0即可,第五个指定目标套接字的地址,最后一个指定地址的长度。
closesocket(m_socket);//关闭套接字
WSACleanup();

服务器端代码实现:

[cpp]
view plaincopy





m_socket=socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN addrSock;
addrSock.sin_family=AF_INET;
addrSock.sin_port=htons(6000);
addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);// INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”

bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));//绑定套接字
recvfrom(m_socket,(char *)(&data),blocksize+4,0,(SOCKADDR*)&addrClient,&len);
closesocket(m_socket);
WSACleanup();

获取本地IP的子程序可参考:

[cpp]
view plaincopy





CString CServeDlg::GetLocalIP(int ipIndex)
{
//获取版本和本机IP地址 获取本机第ipIndex个IP地址
char r_iplist[16][256];
int i=0;

WORD wVersionRequested;
WSADATA wsaData;
char name[255];
PHOSTENT hostinfo;
wVersionRequested = MAKEWORD( 2, 0 );
if ( WSAStartup( wVersionRequested, &wsaData ) == 0 )
{
if( gethostname ( name, sizeof(name)) == 0)
{
if((hostinfo = gethostbyname(name)) != NULL)
{

for (i=0;i<16; i++ )
{
_tcscpy(r_iplist[i], inet_ntoa( *(IN_ADDR*)hostinfo->h_addr_list[i] ));
if ( hostinfo->h_addr_list[i] + hostinfo->h_length >= hostinfo->h_name )
break;
}
//ip = inet_ntoa (*(struct in_addr *)*hostinfo->h_addr_list);
}
}
WSACleanup();
}
return r_iplist[ipIndex];
}

2.UDP数据分包大小的原则与一些思考

编写程序时不可避免遇到一个疑惑,一次发多大的数据包为宜?理论上IP数据报最大长度是65535字节,这是由IP首部16比特总长度字段所限制,去除20字节的IP首部和8个字节的UDP首部,UDP数据报中用户数据的最长长度为65507字节,但是,大多数实现所提供的长度比这个最大值小。



MTU结构
以太网IP 层的最大传输单元(Maximum Transmission Unit,MTU)为1500 字节。如果以分包大小应该使在IP 层不再进行分包为宜作为原则,那么因为UDP数据报的首部8字节,IP首部20字节,所以UDP数据报的数据区最大长度为1472字节。如果说IP数据报大于1500字节,大于 MTU.这个时候发送方IP层就需要分片.把数据报分成若干片,使每一片都小于MTU.而接收方IP层则需要进行数据报的重组.这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便无法重组数据报.将导致丢弃整个UDP数据报。因此,在普通的局域网环境下,建议将UDP的数据控制在1472字节以下为好,以降低丢包率。

进行Internet编程时则不同,因为Internet上的路由器可能会将MTU设为不同的值,鉴于 Internet上的标准MTU值为576字节,建议在进行Internet的UDP编程时。最好将UDP的数据长度控件在548字节 (576-8-20)以内。

总结:我们设定包的大小对于UDP和TCP协议是不同的,关键是看系统性能和网络性能,网络是状态很好的局域网,那么UDP包分大点,提高系统的性能。不好,就分小于1472,这样可以减低丢包率。但是在本传输的千兆以太局域网,性能较好的前提下,在设置接收端接收缓存区足够大的前提下,经过我的多次尝试,数据包越大,传输速度越大。数据包分得尽量大些还是提高了整体的性能。这里因为视频一帧大小为720×576×24(bit)=720×576×24÷8(BYTE)=
1244160 (BYTE),分为32个数据包,则每个数据包大小1244160÷32=38880(BYTE),整体传输效果还是很理想。所以我认为,分包大小应该要结合网络环境和系统整体性能来决定。
3.设置服务器端接收缓存区大小

Setsockopt()函数用来设置套接字状态,这里因为每一帧图像大小约为1.18MB,可以适当将缓冲区大小设大些,这里设成10MB。避免因为接收缓冲太小而丢失数据包。

[cpp]
view plaincopy





intnRecvBuf=1024*1024*10;//接收缓存10M
setsockopt(m_socket,SOL_SOCKET,SO_RCVBUF,(constchar*)&nRecvBuf,sizeof(int));

4.VC程序设计
传输的视频为非压缩的AVI格式视频。利用openCV的视频图像处理函数顺序取帧,读取这一帧图像的数据矩阵,并分为整数个大小相同的数据包,顺序发送。这里以720*576*3的非压缩AVI视频为例。为了提高传输和显示的可靠性,定义结构体,包含一个标志位和一个数组,便于服务器端验证数据包是否已收满是否能正确地显示这一帧。客户端发送程序中设置一帧中最后一个数据块的flag为2,其余皆为1。发送端运用定时器,通过定时时间来控制发送的速度,SetTimer(1,25,NULL);
则为设置每隔25ms取并发出一帧图像。

服务器端开辟单独工作线程来接收,利用cvCreateImageHeader()和cvSetData()函数把收到的数据用来创建一帧IplImage格式的图像。为了防止因为丢包导致一帧图像无法正常显示,这里采用了丢一个数据包则放弃显示这一帧的做法,这样做似乎有些极端,但在本项目相当优良的局域网条件下,经实际测试,发现丢包的情况非常少。这样也避免了因为某一帧未收全而导致后续帧全部受影响的后果。

[cpp]
view plaincopy





定义包结构
struct recvbuf//包格式
{
char buf[blocksize];//存放数据的变量
int flag;//标志

};
struct recvbuf data;

部分代码:

[cpp]
view plaincopy





客户端
IplImage *frame, *frame_copy=0;
frame=cvQueryFrame(capture); //取帧操作
if(frame)
{ char* img=frame->imageData; //指向该帧的数据矩阵
for(int i=0;i<32;i++) //720*576*3=1244160
{ for(int k=0;k<blocksize;k++)
{ data.buf[k]=img[i*blocksize+k];
}
if(i==31) //标识一帧中最后一个数据包
{
data.flag=2;
}
else
{
data.flag=1;
}
sendto(m_sockClient,(char*)(&data),sizeof(data),0,
(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
}
cvReleaseImage( &frame_copy ); // 退出之前结束底层api的捕获操作

[cpp]
view plaincopy





服务器端
while(true)
{
for( int i=0;i<32;i++)
{
recvfrom(m_socket,(char *)(&data),blocksize+4,0,(SOCKADDR*)&addrClient,&len);
COUNT=COUNT+data.flag;
for(int k=0;k<blocksize;k++)
{img[i*blocksize+k]=data.buf[k];
}

if(data.flag==2) //data.flag==2是一帧中的最后一个数据块
{ if(COUNT==33)
{frame = cvCreateImageHeader(cvSize(720,576),IPL_DEPTH_8U,3);
cvSetData(frame,img,720*3);//由收到的数据建立一帧图像
}
else
{
COUNT=0;
i=-1;
}
j++;
}

}
}



设计的界面:





其他:
1、曾经尝试过使用openCV中的cvSaveImage函数把每一帧都压缩成JPG格式图片存入硬盘,再把这张JPG图片发送,服务器端接收并显示。这样做的初衷是因为JPG格式实现了每一帧图像的压缩,每张压缩后的图像仅有110KB左右,而压缩前的每帧图像为1215KB,希望可以提高传输速率,而实际编程尝试时,发现存JPG压缩也需一定时间,总的传输时间并未大幅减少,且需要在硬盘内建立专门的文件夹来存放图片,冗余操作多。

2、为了实现提高可靠性,曾经加入校验和重发机制。服务器端每收到一个数据包,则向客户端发回一个确认包。客户端每发送完一个数据包,就开始等待接收确认包,若收到,则立刻发下一个数据包,若经过设定的时间内还没有收到,则立刻重发该数据包。通过多线程实现。这样理论上能百分百保证不丢包。但由于校验机制相当于整体增加了将近一倍的发送次数,不利于提高速度,也实际放弃了UDP传输视频的优点,似乎偏离了利用UDP传输的本质,变成类似TCP协议了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: