第二篇,前面都是閑扯 其實正文現在才開始,這次是把壓箱底的東西都拿出來了。 首先我們今天要干的事是實現一個echo響應測試工具 也就是echo 的scu,不是實現打印作業管理么。同學我告訴你還早着呢。本來標題取的就是《dicomviewer 第二彈 之 實現打印管理》名字多霸氣,最后我又改回來了。
首先你得把數據組織方式搞懂 那就是pdu 和dimse 元素 數據元素。然后基於這之上你得把協商連接這塊搞懂 ,協商連接都沒通過不用說后面的了。然后你得把實現一個功能 比如打印 ,scu跟scp之間你來我往的 過程和概念搞懂 也就是dimse 然后才是服務類。最夠你全都理解了 並且寫出東西來了能跟醫院的設備正常 連接和操作了,那么恭喜你 差不多了。
最后要說的是: 解析dicom文件那篇你們都已經看過了,dicom網絡通訊跟解析文件是一樣的 只不過解析的是socket數據流里的 元素 數據結構本身是一樣的,然后他有一些規范和標准 ,這就是dimse 和服務類 這些好像都在dicom標准的第四章 第八章 第七章 。
實現這一大坨的東西 有點望而卻步了吧,其實總結起來就一句話 概括 按照dicom標准 封裝數據 處理數據 ,然后根據特殊的參數和應用場景 依規范響應數據,。
好廢話少說,開工 看過 標准簡介那篇博客 的都知道:
PDU是一種數據結構 dataElement是一種數據結構
pdu結構總共7種 其中用於連接控制的就占了6種
A-Associate—RQ PDU
連接請求協議數據單元,用於關聯請求。
A-Associate.AC PDU
基於DICOM標准的醫學圖像通信過程的實現
連接接受協議數據單元,
A.Associate—RJ PDU
連接拒絕協議數據單元,
A-Release-RQ PDU
用於對關聯請求的應答。
用於拒絕關聯請求。
連接釋放請求協議數據單元,
A-Release.RSP PDU
連接釋放響應協議數據單元,
A.Abort PDU
傳輸內容的pdu只有一種P.DATA.TF PDU,
當通訊雙方建立了關聯之后,就可以使用P.DATA-TF所提供的傳輸服務來實現不同的通信功能了。
總之你在進行后面的dimse發送之前先得建立連接,否則你什么也搞不了。
好下面就協商連接的pdu進行分析:
你問這圖是怎么來的 dicom 第八章 31頁。是不是跟上面說的是一樣的 開始兩字節 然后4字節表示長度,只不過這個更詳細了。協商連接pdu說起有6種 其實有好多是大同小異 比如Associate pdu, 他分rq 和ac rq是請求 ac是響應。
我把協商連接概述一下
概述之前,什么是通訊:
還是炒剩飯 我又得把以前說過的話像背書一樣的背一遍了 其實他確實是那么回事。什么是通訊 :
命令tag +數據tag 一起組成sop ,就好象說一句這樣的話:“把這根蘿卜拿去給我切了” “喏 ,蘿卜” 。其實這就是通訊 跟人與人之間傳達意思一樣。說話的時候太熟練了 沒察覺到 你要仔細去想 你自己是一台電腦 ,會是一個什么樣的步驟。
網絡傳輸 跟文件組織 是一樣的格式 。不過有命令tag 。很多命令tag組合到一堆這稱之為dimse。 echo n-create c-find c-store 這些都稱之為dimse ,記住沒有c-print 大哥
打印管理是由很多組dimse 包括n-create 那些 你來我往的一套組成 比如 先n-create 什么東西 再 n-set什么東西,他有一種邏輯規范 什么參數錯誤則不進行n-set。這很多組dimse稱之為服務類 ,比如打印管理 就是一個服務類。這些規范在dicom標准的第八章有說明。總之在dimse傳遞之前 你必須得協商連接。
dicom標准的地址是這個http://medical.nema.org/standard.html,英文的 。看也比較困難 裝裝面子,主要是理解就行了 我看的也是別人翻譯的中文的。不過官方的就是官方的 沒辦法 某些地方你找不到原因 想參照最標准的指示 你還是得硬着頭皮去看英文文檔。
Associate pdu 協商連接的過程:
又說多了 不論如何在進行dimse之前必須得進行連接協商 因為你與別人進行通訊首先你得確定幾個東西。 ,談話的主題是什么,你是用哪國語言。這兩個東西一個稱之為虛擬語法 abssyntax 一個稱之為傳輸語法 transfersyntax 傳輸語法其實主要確定兩個東西 字節序 和 vr表示方式 ,如果你不知道字節序是什么 請自己百度 vr表示方式 跟文件解析一樣的,他們兩個一起被稱之為表達上下文。注意表達上下文有多個 每個都有id。如果你是scp端 那么連接協商響應 也就是association-ac的時候你要告知 以你scp程序的服務能力可以完成哪些表達上下文的服務 傳輸語法語法是什么,如果服務不了也要給出對應的上下文id 並進行告知。這樣的話scu端知道你服務不了就知難而退 主動斷開連接。 其次還有些其他東西比如pdu最大數據長度 一般是0x4000。好了講完了 這就是協商連接的過程 對照上面的圖理解了否。
這是官方的解釋:
官方的解釋 網絡協議是分層的,Dicom ul p ,稱之為dicom上層協議。 也就是上圖的dicom ul service provider。 反正要按照osi的標准來, 也就是說要定義一個associate-rq 或者 ac的數據結構來,一切的數據序列化或者反序列化都由 dicom ul service provider 來進行,反正只怕忽悠不死你。反正他說是那樣說 我們自己按照自己的方式來。
好終於要動代碼了 ,我喜歡的事情來了 噢啦啦啦。其實這是一個抽象化的過程,把你的想法付諸行動 代碼化.就像某人說過的 主要的不是技術 而是思路。分成兩步 根據文檔定義 associate Pdu的數據結構,遵循上面說的原則 一個associate pdu有多高 pst Item,我們把pst Item定義為子項,然后serial()是associate pdu的網絡序列化函數:
1 public enum PDUTypes 2 { 3 AssociateRQ = 0x01, 4 AssociateAC = 0x02, 5 AssociateRJ = 0x03, 6 DataTransfer = 0x04, 7 AssociateReleaseRQ = 0x05, 8 AssociateReleaseRP = 0x06, 9 AssociateAbort = 0x07, 10 11 ApplicationContext = 0x10, 12 PresentationContext = 0x20, 13 UserInformation = 0x50, 14 } 15 16 struct PDUAssociate { 17 //header 18 public byte pduType; 19 public uint length; 20 public ushort ProteocalVersion; 21 public string CallEdAE ;//length=16 22 public string CallingAE ; 23 24 //10 25 public byte appType; 26 public ushort appLength; 27 public string appName; 28 29 //20 30 public IList<PstItem> pstItems; 31 //50 userinfo 32 public byte userinfoType; 33 public ushort userinfoLength; 34 public byte maxnumType; 35 public ushort maxnumLength; 36 public uint maxnum;//DATA-TF PDU的可變字段的最大長度 一般為0x400 即1024 37 public byte impType;//關於實現類的 38 public ushort impLength; 39 public string impUID; 40 public byte impVersionType; 41 public ushort impVersionLength; 42 public string impVersion; 43 44 public byte[] serial() 45 { 46 if (length == 0) 47 return null; 48 MemoryStream _stream = new MemoryStream((int)length + 6); 49 WarpedStream stream = new WarpedStream(_stream); 50 #region 序列化aassociateAC PDU 51 //header 52 stream.writeByte(pduType); 53 stream.skip_write(1); 54 stream.writeUint(length);//最低要94 我去 這是為什么呢 55 stream.writeUshort(ProteocalVersion); 56 stream.skip_write(2); 57 stream.writeString(CallEdAE, 16); 58 stream.writeString(CallingAE, 16); 59 stream.skip_write(32); 60 61 //10 62 stream.writeByte(appType); 63 stream.skip_write(1); 64 stream.writeUshort(appLength); 65 stream.writeString(appName, 0); 66 //21 67 68 for (int i = 0; i < pstItems.Count; i++) 69 { 70 if (pstItems[i].used) 71 { 72 stream.writeByte(pstItems[i].pstType); 73 stream.skip_write(1); 74 stream.writeUshort(pstItems[i].pstLength); 75 stream.writeByte(pstItems[i].pstID); 76 stream.skip_write(3); 77 //if (pstItems[i].used) 78 //stream.writeBytes(new byte[] { 0x00, 0x00, 0x00 }); 79 //else 80 //30 81 if (pstItems[i].pstType == 0x20)//如果是20則為printSCU 82 { 83 stream.writeByte(pstItems[i].absType); 84 stream.skip_write(1); 85 stream.writeUshort(pstItems[i].absLength); 86 stream.writeString(pstItems[i].absStr, 0); 87 } 88 89 stream.writeByte(pstItems[i].tsfType); 90 stream.skip_write(1); 91 if (pstItems[i].used) 92 stream.writeUshort(pstItems[i].tsfLeghth); 93 else 94 stream.writeUshort(0); 95 if (pstItems[i].used) 96 stream.writeString(pstItems[i].tsfStr, 0); 97 } 98 else 99 { 100 stream.writeByte(pstItems[i].pstType); 101 stream.skip_write(1); 102 stream.writeUshort(0x08); 103 stream.writeByte(pstItems[i].pstID); 104 stream.writeBytes(new byte[] { 0x00, 0x04, 0x00 }); 105 stream.writeBytes(new byte[] { 0x40, 0x00, 0x00, 0x00 }); 106 } 107 } 108 109 110 //50 111 stream.writeByte(userinfoType); 112 stream.skip_write(1); 113 stream.writeUshort(userinfoLength); 114 115 stream.writeByte(maxnumType); 116 stream.skip_write(1); 117 stream.writeUshort(maxnumLength); 118 stream.writeUint(maxnum); 119 120 stream.writeByte(impType); 121 stream.skip_write(1); 122 stream.writeUshort(impLength); 123 stream.writeString(impUID, 0); 124 125 stream.writeByte(impVersionType); 126 stream.skip_write(1); 127 stream.writeUshort(impVersionLength); 128 stream.writeString(impVersion, 0); 129 #endregion 130 131 _stream.Flush(); 132 byte[] data = _stream.GetBuffer(); 133 stream.close(); 134 _stream.Close(); 135 return data; 136 } 137 } 138 139 struct PstItem 140 { 141 //20 abstractsyntax transfersyntax傳輸語法 142 public byte pstType; 143 public ushort pstLength; 144 public byte pstID; 145 public bool used; 146 //public byte pstRec; //保留字節 有效項是00 00 00 無效項是00 04 00 147 public byte absType;//20的子項 30 40 讀取的時候應該跟20一並讀出來 148 public ushort absLength; 149 public string absStr; 150 public byte tsfType; //傳輸語法項 本來也有多個 為了方便只寫一個 151 public ushort tsfLeghth; 152 public string tsfStr; 153 }
構建一個associate-rq的pdu 並發送:
1 public bool associateRQ()//請求建立連接 2 { 3 PDUAssociate pdu_ac = new PDUAssociate(); 4 #region 構造associateAC PDU 5 //10 6 pdu_ac.appType = 0x10; 7 pdu_ac.appLength = (ushort)UIDs.DICOMApplicationContextName.Length;//pdu_associate_rq.appLength 8 pdu_ac.appName = UIDs.DICOMApplicationContextName;//pdu_associate_rq.appName 9 10 //20 11 //30 abs 12 //40 transfer syntax 13 pdu_ac.pstItems = new List<PstItem>(); 14 15 PstItem pst_ac = new PstItem(); 16 17 pst_ac.absType = 0x30; 18 pst_ac.absLength = (ushort)UIDs.Verification.Length; 19 pst_ac.absStr = UIDs.Verification; 20 21 pst_ac.tsfType = 0x40; 22 pst_ac.tsfLeghth = (ushort)UIDs.ImplicitVRLittleEndian.Length;//pdu_associate_rq.pstItems[i].tsfLeghth; 23 pst_ac.tsfStr = UIDs.ImplicitVRLittleEndian;//pdu_associate_rq.pstItems[i].tsfStr; 24 25 pst_ac.pstType = 0x20; 26 pst_ac.pstLength = (ushort)(4 + (4 + pst_ac.tsfLeghth) + (4 + pst_ac.absLength)); 27 pst_ac.pstID = 0x01;//表達上下文ID,多個表達上下文的時候以作區分。這里我們為發送方 主動控制為01 28 pst_ac.used = true; 29 pdu_ac.pstItems.Add(pst_ac); 30 31 //50 32 pdu_ac.userinfoType = 0x50; 33 pdu_ac.maxnumType = 0x51; 34 pdu_ac.maxnumLength = 0x04; 35 pdu_ac.maxnum = 0X4000;//16384 36 37 pdu_ac.impType = 0x52; 38 pdu_ac.impLength = (ushort)UIDs.ImplementionUid.Length; 39 pdu_ac.impUID = UIDs.ImplementionUid; 40 41 pdu_ac.impVersionType = 0x55; 42 pdu_ac.impVersionLength = 11; 43 pdu_ac.impVersion = "ASSASSMedic"; 44 45 pdu_ac.userinfoLength = (ushort)(4 * 3 + pdu_ac.maxnumLength + pdu_ac.impVersionLength + pdu_ac.impLength); 46 47 //header 48 pdu_ac.pduType = 0x01; 49 pdu_ac.ProteocalVersion = 0x01; 50 pdu_ac.CallEdAE = calledAET; 51 pdu_ac.CallingAE = callingAET; 52 pdu_ac.length = (uint)((74 - 6) + (pdu_ac.appLength + 4) + 53 (pdu_ac.userinfoLength + 4)); 54 55 for (int i = 0; i < pdu_ac.pstItems.Count; i++) 56 { 57 if (pdu_ac.pstItems[i].used) 58 pdu_ac.length += (ushort)(pdu_ac.pstItems[i].pstLength + 4); 59 else 60 pdu_ac.length += 12; 61 } 62 63 #endregion 64 //序列化 65 stream.writeBytes(pdu_ac.serial()); 66 67 Console.WriteLine(string.Format("associate create success,CalledAET:{0}", calledAET)); 68 69 return false; 70 }
在這之前你還是得連接到SCP端:
1 public void run(string ipStr, int port) 2 { 3 TcpClient _client = new TcpClient(); 4 IPAddress ipdrs = IPAddress.Parse(ipStr); 5 _client.Connect(ipdrs, port); 6 7 if (_client.Connected == false) 8 { 9 Console.WriteLine("與所指定主機連接失敗"); 10 Console.WriteLine("連接斷開"); 11 return; 12 } 13 WarpedStream stream = new WarpedStream(_client.GetStream()); 14 15 echo(stream); 16 stream.close(); 17 _client.Close(); 18 } 19 20 public void echo(WarpedStream _stream) 21 { 22 stream = _stream; 23 //第一步協商連接 24 associateRQ(); 25 PDUTypes PduType = (PDUTypes)stream.readByte(); 26 stream.skip(1); 27 uint pduLen = stream.readUint(); 28 stream.skip((int)pduLen); 29 Console.WriteLine("協商連接成功"); 30 //第二步 進行echo 請求 31 Verification_CECHORQ(); 32 PduType = (PDUTypes)stream.readByte(); 33 stream.skip(1); 34 pduLen = stream.readUint(); 35 stream.skip((int)pduLen); 36 release(); 37 Console.WriteLine("echo測試成功"); 38 }
注意我們並沒有對收到的associate-ac數據進行解碼驗證,直接偷懶略過了 。我們默認對方都是好人 都是按照套路來的 並且能夠承擔我們所請求的echo服務。