背景:
如前一篇专栏博文所述,借助于CGI或FastCGI技术转发浏览器发送过来的用户请求,启动本地的DCMTK和CxImage库响应。然后将处理结果转换成常规图像返回到浏览器来实现Web PACS。本博文通过实际的代码測试来验证这一模式的可行性,同一时候对C语言编写CGI脚本提出了一些问题。
难题:
计划參照DCMTK自带工具dcm2pnm.exe的源代码。通过DicomImage将DCM文件转换成BMP文件,然后利用CGI技术返回到浏览器。实现一次简单的WEB PACS的影像传输模拟。详细的代码例如以下,
// dcmtk-save-test.cpp : 定义控制台应用程序的入口点。 // #include "dcmtk/config/osconfig.h" #include "dcmtk/dcmdata/dctk.h" #include "dcmtk/dcmdata/dcpxitem.h" #include "dcmtk/dcmjpeg/djdecode.h" #include "dcmtk/dcmjpeg/djencode.h" #include "dcmtk/dcmjpeg/djcodece.h" #include "dcmtk/dcmjpeg/djrplol.h" #include "dcmtk/dcmimgle/diutils.h" #include "dcmtk/dcmimgle/dcmimage.h" void SendImageDcmtk(char* filename) { DcmFileFormat mDcm; mDcm.loadFile(filename); E_TransferSyntax xfer = mDcm.getDataset()->getOriginalXfer(); unsigned long mode = CIF_MayDetachPixelData | CIF_TakeOverExternalDataset; DicomImage *di = new DicomImage(&mDcm,xfer,mode,0,0); if(di == NULL) { Print2Web("Can not open DCM file by DicomImage!"); } printf("Content-Type:image/bmp\n\n"); di->writeBMP(stdout,24,0); } int main(int argc ,char* argv[]) { char* filename="c:\\test.dcm"; SendImageDcmtk(filename); return 0; }
编译生成dcm2bmp.exe的CGI程序,将其复制到站点的CGI文件夹(我本机地址为c:\wamp\www\c-cgi)中。通过在浏览器中输入http://localhost/c-cgi/dcm2bmp.exe启动服务端的CGI程序。
尽管程序启动顺利,可是并未获得我们想要的结果——输出了一幅奇怪的图像,例如以下所看到的:左图是在PACS看图端中看到的真实DCM图像,右图是我传输到浏览器的失败的图像。
验证測试:
获得了错误的结果,起初并未想到非常好的排除错误的方法。遂决定首先确认问题出现的大致范围。由于介绍CGI技术的书籍大多都採用Perl或者PHP来实现。因此仿照书籍中的实例。利用Perl和PHP来实现一次正常的传输图像到浏览器的功能,验证一下该机制是否可行。
以下是实际的測试过程,
(1)Perl版本号的CGI
#!c:/Perl64/bin/perl.exe use warnings; use strict; binmode STDOUT; print "Content-type:image/bmp\n\n"; open FILE,'<','c:\test.bmp' or die "Can't open file"; while (my $buf = <FILE> ){ print $buf; } close(FILE);
经过測试。能够输出正确的图像。
(2)PHP版本号的CGI
<? php $filename="c:/test.bmp"; $size=getimagesize($filename); $fp=fopen($filename,"r"); #echo $size['mime']; if($size && $fp) { header("Content-type:image/bmp\n\n"); fpassthru($fp); exit; } ?>
经过測试。也能够输出正确的图像。
结果分析:
通过上面的两次測试,足以说明WAMP+CGI/FastCGI的环境搭建没有问题。因此能够断定问题出如今C语言编写的CGI脚本程序中。由于CGI脚本是服务端的控制台程序。能够再命令行中直接调试,可是我们是利用DicomImage的writeBMP函数将转换后的bmp图像输出到了stdout中,实际调试中会输出一堆乱码,由于stdout默认是ASCII格式的。所以在命令行中调试CGI脚本的思路行不通。所以决定从最底层入手,利用RawCap.exe工具。抓取浏览器与server端的CGI程序之间的数据包。通过分析数据包期望找到问题出现的地方。
1)RawCap+Wireshark本地抓包+分析
RawCap的操作在早前的博文中介绍过了,这里不做具体介绍。在命令行输入RawCap.exe后选择[2]接口。即本地回路127.0.0.1的数据包。就可以開始抓取本地回路数据包。相同依照博文前面測试CGI的方法,分别调用用C语言编写的输出结果错误的CGI程序和用PHP编写的输出结果正确的CGI程序,抓取的数据包分别为wrongimage.pcap和rightimage.pcap。想结束抓取能够输入CTRL+C。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenNzdXJlcWg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >
抓取完毕后,在Wireshark中打开wrongimage.pcap和rightimage.pcap。此处由于仅仅关心图像传输的数据包问题,所以直接使用Wireshark中的统计分析工具。详细操作例如以下,单击菜单条中的“Statistic”,选择会话——Conversations,打开会话窗体:
随后单击TCP协议。选择当中数据量大的会话。单击窗体下方的Follow Stream。能够打开CGI脚本传输图像到服务端的真实数据流。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvenNzdXJlcWg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >
同一时候使用Follow Stream跟踪wrongimage.pcap和rightimage.pcap中的数据流,对照结果例如以下:
从上图中能够看到真实的图像数据传输流,对照左(正确图像)和右(错误图像),能够看出两个数据流中都表明自己是BMP文件,具有0X42 4D的类型标记符。
依据BMP文件结构可知,随后是颜色表。如上图中大的红色矩形框所看到的。可是细致观察能够看到错误图像中的0a 0a 0a 00颜色表项变成了0d
0a 0d 0a 0d 0a 00。通过搜索错误数据流发现,凡是原数据流中出现0a的地方都被替换成了0d 0a。
因此断定这就应该是图像传输失败的原因。
为了非常好的理解上述错误出现的原因。以下补充一些基础知识,详情可參见博文后的网址。
2)知识点补充:
(1)文本文件 VS 二进制文件
众所周知。计算机非常二,仅仅认识0和1。不论什么内容在计算机内都是以0和1的方式存储的,既然如此为何还区分文本文件和二进制文件?我是这么理解的。尽管计算机的底层都是由二进制格式来存储的,可是我们能够定制不同的解读标准。相同的0和1序列。解读方式不同。表达的含义就不同。事实上这样的应用不同标准来解读相同序列的现象在计算机领域是非经常见的。在32位机器中,相同的四字节01序列。可能表示无符号整数或者有符号整数(在C/C++语言中),也可能表示一个IP地址(在socket编程中)。也可能表示标签或分隔符(在DICOM协议中的对象的标签都是採用四字节格式,如0x0002
0010代表的是TransferSyntax UID)。丰富多彩、变幻无穷的信息世界源于不同的解读标准或解读规则。所以学习过程中要了解标准,了解实际的应用场景
文本文件和二进制文件能够理解为应用不同标准存储的01序列,文本文件指的是全部信息都以ASCII格式存储。每一个字节都相应到一个ASCII字符——ASCII是人们可直接读出来的(当然这个我们能够识别的文字也是在计算机内部经过了多次转换而得来的。能够简单地理解为针对不同的01序列,电脑向屏幕绘制相应的图形——图形的生成能够简单的理解为多个相邻的晶体发光来实现的);而二进制文件指的是将实际的01序列原封不动的存储,而不加不论什么处理(这也算一种解读方式吧)。所以之所以要区分文本文件和二进制文件就是一种声明,一种告知01序列被解读方式的声明。打个不恰当的比喻,01序列就像是敌方发送的电报,而“文本文件”和“二进制文件”分别表示两本password本,相同的电报用不同的password本翻译。出来的结果和意思自然就不同(当然通常情况下有一种解读方式是失败的,无法提供给我们有效的信息)。
(2)CRLF
在编程语言中,文本文件和二进制文件代表的就是不同的操作方式,或者简单的能够理解为使用不同的函数。通过上述的解说,能够觉得不同的函数内部就是依照不同的标准(文本文件标准和二进制文件标准)对01序列进行操作,比如读取、写入等等。
——有些时候不是必需纠结于一个函数的结果为什么会是这样子,仅仅要记住这是函数背后定义的标准所致就可以。至于标准的制定就不是必需深究了,总之是一波牛人定的。
上面出现错误的两个字节——0x0d 0x0a——是计算机中非常特殊的两个字节,他们分别代表回车(CR=Carriage Return)和换行(LF=Line Feed)。
不同的系统对CRNL的解释不同。最早的UNIX系统中仅仅用换行(即\n)来表示数据的另起一行;Windows系统使用回车+换行来表示;而Mac系统却仅仅使用回车。即\r。
同一个文件从磁盘读取文件到内存(程序数据区或者缓存区)时,在文本和二进制方式下,内存中的内容一般不同样,这就是两种打开方式的实质性区别。
由于CRLF的不同。在windows下,它会做一个处理。就是写文件时,换行符会被转换成回车+换行符存在磁盘文件上,而读磁盘上的文件时,它又会进行逆处理。就是把文件里连续的回车+换行符转换成换行符。因此,在读取一个磁盘文件时,文本方式读取到文件内容非常有可能会比二进制文件短,由于文本方式读取要把回车和换行两个字符变成一个字符,相当于截短了文件。可是为什么不过可能呢?由于可能文中中不存在连着的0x0d,0x0a这两个字节(0X0A是CR回车的ASCII码。0X0D是换行符CL的ASCII码),也就不存在“截短”操作了,因此读到的内容是一样的。详细的来说,文件文件(以文本方式写的),最好以文本方式读。二进制文件(以二进制方式写的)。最好以二进制方式读。
(3)stdin、stdout
从(2)知识点就能够大致推断出,windows系统在向stdout写入BMP数据流时。将遇到的0x0a都替换成了0x0d 0x0a,他觉得这里改换行了。那么为什么在向stdout写入数据流时会将0x0a转换成0x0d 0x0a呢?有没有不转换的方法?这里简单的介绍一下C语言中的标准输入输出流。我们都知道stdin默认绑定到键盘;stdout默认绑定到显示器。事实上stdin和stdout跟我们操作文件经常使用的FILE*是同样的类型。能够简单的觉得是程序与键盘和显示屏信息交互的缓冲区。比較特殊的是在CGI架构中,stdin和stdout担负着浏览器与服务端的信息交互。
既然stdin和stdout与普通的FILE*没有差别,依据我们对文本格式和二进制格式的理解,能否够控制写入stdout的方式来限制系统将0xa转换成0x0d 0x0a呢?由于显示屏默认是字符类型的输出,不方便调试。我们用一个文件FILE*来取代stdout,然后通过不同的写入方式来验证一下我们刚才的猜想。
測试的输入文件(即我们首先读入到内存的数据)是利用dcm2pnm.exe工具转换而来的bmp图像。我们在读取文件的时候选择了"rb”二进制模式,目的就是为了限制windows系统对CRLF的转换。測试代码例如以下:
如上图所看到的,二进制方式写入时能够得到正确的图像。文本格式写入时恰恰得到的就是我们前面遇到的错误结果。
因此能够说明在向stdout写入数据的过程中DicomImage使用的是文本格式。应该使用二进制方式写入stdout,想必能够得到正确的结果。
3)尝试改动C语言版本号的CGI程序:
既然找到了问题的根源。那么我们就又一次改动C语言的CGI程序。已知stdout与FILE*同样。那么直接利用常见的C语言文件操作函数。用二进制方式来向stdout输出数据。验证一下我们的想法。
測试代码例如以下:
// dcmtk-save-test.cpp : 定义控制台应用程序的入口点。 // #include "dcmtk/config/osconfig.h" #include "dcmtk/dcmdata/dctk.h" #include "dcmtk/dcmdata/dcpxitem.h" #include "dcmtk/dcmjpeg/djdecode.h" #include "dcmtk/dcmjpeg/djencode.h" #include "dcmtk/dcmjpeg/djcodece.h" #include "dcmtk/dcmjpeg/djrplol.h" #include "dcmtk/dcmimgle/diutils.h" #include "dcmtk/dcmimgle/dcmimage.h" #include <stdio.h> #include <iostream> #include <iomanip> #include <bitset> #include <windows.h> using std::cout; using std::bitset; using std::hex; void Print2Web(char* msg) { printf("Content-Type:text/html\n\n"); printf("<HTML>\n"); printf("<HEAD>\n<TITLE >DCM to BMP Test</TITLE>\n</HEAD>\n"); printf("<BODY>\n"); printf("<div style=\"font-size:12px\">\n"); printf("<div style=\"COLOR:RED\">%s</div>\n",msg); printf("</div>\n"); printf("</BODY>\n"); printf("</HTML>\n"); } void SendImageDcmtk(char* filename) { DcmFileFormat mDcm; mDcm.loadFile(filename); E_TransferSyntax xfer = mDcm.getDataset()->getOriginalXfer(); unsigned long mode = CIF_MayDetachPixelData | CIF_TakeOverExternalDataset; DicomImage *di = new DicomImage(&mDcm,xfer,mode,0,0); if(di == NULL) { Print2Web("Can not open DCM file by DicomImage!"); } printf("Content-Type:image/bmp\n\n"); di->writeBMP(stdout,8,0); ; } void SendImage(char* filename) { FILE* fp=fopen(filename,"rb"); printf("Content-Type:image/bmp\n\n"); fclose(stdout); freopen("CON","wb",stdout); int r=getc(fp); while(!feof(fp)) { putc(r,stdout); r=getc(fp); } fclose(fp); } void SendImage2(char* filename) { FILE* fp=fopen(filename,"rb"); fseek(fp,0,SEEK_END); int length=ftell(fp); printf("Content-Length:%d\n",length); printf("Content-Type:image/bmp\n\n"); fseek(fp,0,SEEK_SET); char buf[1024]; memset(buf,0,sizeof(buf)); if(length>1024) { while(length>1024) { fread(buf,sizeof(buf),1,fp); fwrite(buf,sizeof(buf),1,stdout); memset(buf,0,sizeof(buf)); length-=1024; } fread(buf,length*sizeof(char),1,fp); fwrite(buf,length*sizeof(char),1,stdout); } else { fread(buf,length*sizeof(char),1,fp); fwrite(buf,length*sizeof(char),1,stdout); } fclose(fp); } void SendImage3(char* filename) { FILE* fp=fopen(filename ,"rb"); printf("Content-Type:image/bmp\n\n"); char buf[1024]; memset(buf,0,sizeof(char)*1024); int size=0; fclose(stdout); freopen("CON","wb",stdout); while(size = fread(buf,sizeof(char),1024,fp)) { fwrite(buf,sizeof(char),size,stdout); fflush(stdout); } fflush(stdout); fclose(fp); } void SendImage4(char* filename) { printf("Content-Type:image/bmp\n\n"); HANDLE hStdout=GetStdHandle(STD_OUTPUT_HANDLE); HANDLE hFile=CreateFile(filename,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_FLAG_SEQUENTIAL_SCAN,NULL); if(hFile == INVALID_HANDLE_VALUE) return; DWORD dwHighSize; unsigned long size=GetFileSize(hFile,&dwHighSize); char *data=new char[size]; unsigned long readsize=0; ReadFile(hFile,data,size,&readsize,NULL); if(size!=readsize) { delete data; return; } unsigned long writesize=0; while(writesize<size) { unsigned long wsize=0; if(size-writesize>1024) WriteFile(hStdout,data+writesize,1024,&wsize,NULL); else { WriteFile(hStdout,data+writesize,size-writesize,&wsize,NULL); } fflush(stdout); writesize+=wsize; } fflush(stdout); CloseHandle(hStdout); } int main(int argc ,char* argv[]) { char* filename="c:\\test.bmp"; SendImage(filename); return 0; }
经过了上述多种尝试后。发现数据依旧是有问题。因此推測C语言的文件操作函数内部可能对stdout的写入有特殊的操作,无法实现二进制格式写入。至此该问题在C语言环境下还是未解决。假设哪位朋友知道原因。还请指教。兴许我也会继续进行分析,希望尽快找到原因。
未完待续……
參考资料:
[1]http://blog.csdn.net/silyvin/article/details/7275037
[2]http://blog.csdn.net/lanbing510/article/details/8183343
兴许专栏博文介绍:
利用DCMTK搭建WMLserver
利用oracle直接操作DICOM数据
C#的异步编程模式在fo-dicom中的应用
VMWare三种网络连接模式的实际測试
作者:[email protected]时间:2014-10-27