Web版PACS开发纪要二:解决文件传输数据“丢失”问题

来源:互联网 发布:金融领域国产密码算法 编辑:程序博客网 时间:2024/05/21 18:44

Web版PACS开发纪要二:DCM文件的网络传输

——解决文件传输数据“丢失”问题

目录

背景介绍

问题搜索

问题分析

问题解决


0背景介绍

该工程是上个月博文的延续。在利用“完成端口”完成了文件自动归档的基础上,本次需要利用socket套接字进行文件的远距离传输。虽然socket编程的参考书籍很多,但是在具体实现过程中还是遇到了各种各样的问题。下面继续记录一下工作中遇到的问题,一方面作为工作纪要,为后续工程的扩展做准备,另一方面作为自己学习的笔记,加深网络编程方面的知识。

首先说一下socket套接字传输文件部分的基本流程:首先利用上次完成的“文件夹实时监控”,将文件从分散的文件夹中归档到统一的目录下,默认为:e:\MedicalImages;然后利用socket套接字结合完成端口,将e:\MedicalImages文件夹下的文件传输到远端服务器。基本结构图如下:


1问题搜索


从图中可以看出,原始字节流(即源文件)比目标字节流(接收到的文件)多出了850字节。但是在调试过程中并未发现有send或recv函数发出错误,那么这“丢失”的850字节去哪里了呢?对此问题进行搜索

尝试一:send和recv函数是否是阻塞的?

send

客户端和服务端都通过send函数来向TCP连接的另一端发送数据

客户端用send向服务端发送请求;

服务端用send向客户端发送应答recv客户端和服务端都通过recv函数接收来自TCP连接另一端的数据

1.1简言之,阻塞就是send和recv必须完成其相应的任务才返回,将控制权交给自己编写的程序;非阻塞就是send和recv未等其任务完成就直接返回,将控制权交给调用者程序;

1.2阻塞时刻

send和recv函数的阻塞与accept函数的阻塞时刻略有不同,这是由于函数实现的功能以及TCP协议造成的。send函数的工作是将给定缓冲区(此缓冲区是程序中由程序员自己开辟的,存在于程序的堆或栈中)中的数据拷贝到TCP/IP协议栈的缓冲区(传输层开辟的接收/发送缓冲区,不同于程序员自己开辟的堆栈空间),在协议栈缓冲区未满时,阻塞和非阻塞的send函数效果相同都会直接返回,代表数据发送成功,其实此时数据依然存在于send端的TCP/IP协议栈的缓冲区中,并未真正发送出去;当协议栈缓冲区已满,阻塞和非阻塞的send函数就表现出了不同,阻塞send函数会将调用进程(线程)挂起,直到协议栈缓冲区中有足够的空间来存储send函数中指定的程序缓冲区数据,有时为了防止程序阻死会设置超时时间,当超过此时间阻塞send函数也会返回。非阻塞send函数发现协议栈缓冲区没有足够空间时,尽能力的拷贝,返回成功拷贝的大小;如缓存区可用空间为0,会立刻返回,并提示WSAEWOULDDBLOCK给程序员,告知“协议栈缓冲区已满,数据无法拷贝”——你自己想办法处理吧o(╯□╰)o”

【小结】:单步调试分项目,发现send和recv函数并未出现阻塞现象,也就是说在传送过程中TCP/IP协议栈的缓冲区空间充裕。那么为何会“丢失”数据呢?

参考博文:

http://blog.csdn.net/xiaofei0859/article/details/6037814    (Windows下情形)

http://blog.sina.com.cn/s/blog_4462f8560100tvu4.html      (Linux下的情形)

http://blog.sina.com.cn/s/blog_4d8498800100ddhb.html    (Linux下的情形)

http://www.360doc.com/content/12/0211/16/1317564_185797704.shtml

尝试二:send和recv是否一一对应?

send与recv从接收和发送角度来说是一一对应的,即有send就有recv,否则send就没有意义了(当然对发送的数据视而不见也是可以的);但是从函数调用次数角度考虑,send和recv并非是一一对应的,即send和recv函数的个数未必相同,两者可分别自行设置接收的字节长度,例如调用一次send函数发送100B数据,可以调用10次recv,每次接收10个字节。(那么怎样知道用10次recv来接收1次send发来的数据,而不是用9次或者8次呢?这就牵扯到TCP协议两端之间的信息交互,后面我会提到)

参考博文:

http://bbs.csdn.net/topics/60264256

尝试三:TCP/IP协议栈缓冲区

TCP协议利用滑动窗机制,来保证数据传输的可靠性和流控特性,TCP的窗口是一个16Bits的位字段,代表的窗口的字节容量,最大值为2^16-1=65535。TCP连接的两端分别维护各自的“发送窗口”和“接收窗口”(如下图所示),也就是说发送端和接收端分别独自拥有两个独立的缓冲区域——发送缓冲区和接收缓冲区。


参考博文:

http://www.netis.com.cn/flows/2012/08/tcp-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%9A%84%E7%AE%80%E4%BB%8B/

2 问题分析

通过以上的资料搜索和分析,猜测“丢失”的字节可能是由于接收端程序编写的bug所导致的:原本以为阻塞模式下一次send调用对应一次recv,在接收端第一次调用recv时本意是 “只”接收客户端发送过来的用户和其影像文件的基本信息,信息格式约定为:用户ID&用户姓名&用户年龄&用户性别&用户出生年月&全路径名&文件名&文件大小

recv函数的原型为:

int recv(

 _In_   SOCKET s,

 _Out_  char *buf,

 _In_   int len,

 _In_   int flags

);

其中len指定的是buf缓冲区的大小;如果接收顺利,recv函数会返回其真实接收的字节长度。项目中我们固定buf缓冲区长度为1024,这远大于我们要接收的基本信息字节流的长度。单步调试进程序,发送端第一次send的基本信息为,长度为174;继续调试发现当程序中第一次recv的返回值不是基本信息长度174而是1024时,最终接收的文件会出现“丢失”字节现象,丢失的数目为850(=1024-174)。

分析:由于发送端多次send的结果,在接收端的接收缓冲区中已经存在了足够的数据,且长度大于为1024B,当接收端第一次调用recv时,系统并不知道基本信息字节流的具体长度,只能尽量拷贝足够多的字节到程序中的缓存buffer中,此处是1024B。在接收到基本信息后,接收端就想当然的调用ParseInformationString函数对基本信息进行解析,并未对此次recv接收的具体字节数进行判别,只简单的检验了是否成功,进而导致将接收的“基本信息”后的有效文件信息丢弃,并未写入到文件中。——到此终于找到了“丢失”的字节,原来所谓的“丢失”字节并未在TCP传输工程中丢失,而是由于接收端程序编写bug所导致,这也是文中一直对“丢失”添加引号的初衷。

3 问题解决

3.1 对recv接收的字节进行判别,如果长度等于1024,那么在解析基本信息完成后,将剩余字节写入到文件中。


修改服务端代码,

              //2013-06-08修改:

              //对第一次recv的返回值进行判别,如果长度小于,那么说明此次接受内容只包含

              //需要的文件基本信息,如果长度等于,在解析完基本信息字符串后,将剩余的字

              //节单独拷贝出来,写入到文件中

              int rcv = recv(s, buffer, 1024,0);

 

              //1)计算用户基本信息字符串长度

              intBasicInfLen=7;

                                                 //     7——代表个&符号,这是我们能够确定的基本信息字符串必然包含的字符

              BasicInfLen= BasicInfLen+UserID.GetLength()+UserName.GetLength()+UserAge.GetLength()+UserBirth.GetLength()+

                                   UserSex.GetLength()+FileFullName.GetLength()+FileName.GetLength()+FileSizeStr.GetLength();

 

 

              if(rcv>BasicInfLen)

              {

                     file.Write(buffer+BasicInfLen,rcv-BasicInfLen);

                     iTemp+=rcv-BasicInfLen;

              }

 

具体分析如下:

客户端第一次send发送的信息如下:12345&TEST&2&男&2013-06-0814:06&e:\MedicalImages\12345\TEST\US\1.2.156.112601.1.4.8323329.2232.1368501262.762411.dcm&1.2.156.112601.1.4.8323329.2232.1368501262.762411.dcm&2361024

其长度为179.

服务端第一次接收recv的返回值rcv为1024.查看buffer内存如下:


buffer+179内存结构如下:


通过对比以上两个内存分布,可以看出首次接收到的1024字节中除去179字节的基本信息,剩余的恰巧是原本误认为“丢失”的DCM文件数据。至此证明方案一能够顺利解决问题。

3.2 设法同步send和recv,尽量做到一次send对应一次recv

由上文TCP/IP协议栈缓冲区结构图可知,阻塞socket下的send和recv在缓冲区已满或者缓冲区为空时,会发生阻塞(此处的阻塞指未释放控制权给程序员)。那么看能够巧妙的利用这一点来同步send和recv,达到在服务端接收并解析“基本信息”时,客户端不进入while(1);循环内开始图像的发送

原本我们只利用了TCP/IP协议两端的两个缓冲区,即服务端的接收缓冲区和客户端的发送缓冲区。客户端先向其发送缓冲区写入“基本信息”,然后逐次写入单位长度为1024B的文件信息块,利用TCP/IP协议传输到服务端的接收缓冲区,如下图中红线所示:


此时由于两端的缓冲区不存在“无数据”和“数据已满”的情况,因此客户端的send和服务端的recv都不会出现阻塞现象。调用时能够直接获取足够的数据(项目中设定为1024B)。也正因为此才导致了原本的程序中不小心丢弃了部分数据。那么怎样才能尽量使客户端的send和服务端的recv同步,一一对应呢?这是就要利用其它两个没使用到的缓冲区:服务端的发送缓冲区和客户端的接收缓冲区。具体流程如下:

1)        在客户端发送完“基本信息”后,添加一次recv调用,由于此时客户端的“接收缓冲区”为空,那么recv函数会阻塞客户端程序;

2)        当服务端接收了“基本信息”后,对其进行解析后处理,待处理完毕后,添加一次send调用,发送确认消息给客户端,例如“READY_REC”字符串;

3)        客户端对recv函数的接收结果进行判别,看是否是“READY_REC”,如果是那么客户端进入while(1);开始文件有效数据的发送

4)        待客户端进入while(1);发送数据后,服务端也进入while(1);开始接收数据

至此利用客户端“接收缓冲区为空”来同步客户端与服务端的目的已经实现。示意图如下:



Author:ZSSURE

Date   :2013-06-08

E-Mail zssure@163.com

原创粉丝点击