一、UDP协议
UDP是User Datagram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议。正式通信前不必与对方先建立连接,直接向接收方发送数据,是一种不可靠的通信协议。正是由于UDP协议不关心网络数据传输的一系列状态,使得UDP协议在数据传输过程中节省了大量的网络状态确认和数据确认的系统资源消耗,大大提高UDP协议的传输效率,传输速度快。TCP(Transport Control Protocol)协议是面向连接的传输协议,通信前需先建立连接,传输时延较大,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、 关闭套接字
- 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();
- 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的子程序可参考:
- 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字节,但是,大多数实现所提供的长度比这个最大值小。
进行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。避免因为接收缓冲太小而丢失数据包。
- 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格式的图像。为了防止因为丢包导致一帧图像无法正常显示,这里采用了丢一个数据包则放弃显示这一帧的做法,这样做似乎有些极端,但在本项目相当优良的局域网条件下,经实际测试,发现丢包的情况非常少。这样也避免了因为某一帧未收全而导致后续帧全部受影响的后果。
- 定义包结构
- struct recvbuf//包格式
- {
- char buf[blocksize];//存放数据的变量
- int flag;//标志
- };
- struct recvbuf data;
部分代码:
- 客户端
- 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的捕获操作
- 服务器端
- 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++;
- }
- }
- }
其他:
2、为了实现提高可靠性,曾经加入校验和重发机制。服务器端每收到一个数据包,则向客户端发回一个确认包。客户端每发送完一个数据包,就开始等待接收确认包,若收到,则立刻发下一个数据包,若经过设定的时间内还没有收到,则立刻重发该数据包。通过多线程实现。这样理论上能百分百保证不丢包。但由于校验机制相当于整体增加了将近一倍的发送次数,不利于提高速度,也实际放弃了UDP传输视频的优点,似乎偏离了利用UDP传输的本质,变成类似TCP协议了。