摘 要: 分析了鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)采用多線程技術實現(xiàn)網(wǎng)絡通信存在的不足,闡述了Java非阻塞I/O的基本原理。系統(tǒng)采用非阻塞I/O通信技術只使用一個線程并行實現(xiàn)大量客戶無阻塞的通信,,有效地減少了系統(tǒng)開銷,,較好地提升了系統(tǒng)的性能。
關鍵詞: 非阻塞I/O,;遠程醫(yī)療系統(tǒng),;線程阻塞;通道
1 鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)的應用前景
目前我國廣大的農(nóng)村地區(qū)醫(yī)療條件比較差,,這些地區(qū)的大量患者因為得不到及時有效的治療,,給患者以及家屬帶來了不必要的痛苦。使用網(wǎng)絡多媒體通信技術實現(xiàn)的鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)可以讓高水平的醫(yī)療專家給鄉(xiāng)村的患者進行遠程診病,,以及指導治療與護理,。鄉(xiāng)村衛(wèi)生所、鄉(xiāng)鎮(zhèn)醫(yī)院等醫(yī)療單位可以和具有優(yōu)質醫(yī)療資源的醫(yī)院或直接與專家建立醫(yī)療合作關系,,雙方合作使用可視化醫(yī)療系統(tǒng),,讓專家為病人進行遠程診療服務。在鄉(xiāng)村這一端,,一般由專業(yè)醫(yī)務人員指導病人接受專家遠程診療,,醫(yī)療專家通過可視化醫(yī)療系統(tǒng)實時對病人進行診斷、處方,、治療甚至指導手術等,。患者也可以在家中使用遠程醫(yī)療系統(tǒng)直接接受遠程專家的指導,,尤其在處理突發(fā)性疾病時,,可以在第一時間內得到醫(yī)療專家的幫助。目前隨著生活水平的大幅度提高,,不少需要“私人醫(yī)生”的人員也可以方便地使用該系統(tǒng)得到醫(yī)療專家的服務,。可見,,可視化醫(yī)療系統(tǒng)有著很廣闊的應用市場和較好的發(fā)展前景,。
2 鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)功能簡介
鄉(xiāng)村遠程醫(yī)療系統(tǒng)可以同時提供若干對醫(yī)療服務,每一對服務有2個或2個以上的客戶端進行通信,??蛻舳朔謩e由醫(yī)療專家和患者使用,醫(yī)療專家使用的客戶端稱為專家端,,患者接受診療的客戶端稱為醫(yī)務端,。每個客戶(醫(yī)務端和專家端)首先登錄服務器,服務器將每個登錄客戶的用戶名,、IP地址等相關信息發(fā)送給相關的其他客戶,,然后客戶端之間建立直接連接,,并可進行文件傳送、文字和多媒體等通信,。通過這種綜合的通信方式,,醫(yī)療專家可以遠程為患者提供醫(yī)療服務。為了建立醫(yī)療檔案,,服務器需要保存診療時間,、參與人員、患者檢驗報告,、病情診斷,、處方等信息?;颊哌€可以通過服務器進行專家預約,,專家和患者均可以在服務器上進行診療信息查詢。由于視頻信息量比較大,,且保存意義不是十分大,,因此并不保存在服務器上。系統(tǒng)結構如圖1所示,。
3 使用多線程和阻塞通信模式存在的局限
可視化遠程醫(yī)療服務器需要同時為多個專家端和醫(yī)務端提供服務,在傳統(tǒng)的阻塞通信模式中使用多線程技術來處理多客戶連接,,一般需要服務器為每個客戶端建立一個線程,。但使用多線程技術存在以下局限:
(1)Java虛擬機會為每個線程分配獨立的堆棧空間,,工作線程數(shù)目越多,,系統(tǒng)開銷就越大,而且增加了Java虛擬機調度線程的負擔,,增加了線程之間同步的復雜性,,提高了線程死鎖的可能性,;
(2)當主線程接收客戶連接,,在醫(yī)療服務過程中,服務器從網(wǎng)絡發(fā)送或接收數(shù)據(jù)時,,線程常常進入阻塞狀態(tài),,這就使工作線程的許多時間都浪費在阻塞操作上,,CPU的利用率降低,此外Java虛擬機需要頻繁地轉讓CPU的使用權,。
由此可見,,工作線程并不是越多越好。實驗表明,,適量的工作線程會提高服務器的并發(fā)性能,,但是當工作線程的數(shù)目達到某個極限而超出系統(tǒng)的負荷時,,反而會降低并發(fā)性能,使得許多客戶端得不到服務器的及時響應,。為了改善服務器性能,,在遠程醫(yī)療系統(tǒng)中采用了Java非阻塞I/O通信技術。
4 Java非阻塞I/O工作原理簡介
在傳統(tǒng)的阻塞通信模式中,,服務器在接受客戶連接后創(chuàng)建Socket對象與客戶進行通信,,Socket對象由線程進行管理。當有多個客戶連接時,,就需要多個線程分別處理服務器與各個客戶的通信,。Java非阻塞I/O通信服務器程序只需要一個線程就能夠實現(xiàn)多個客戶端的連接、同時接收多個客戶端發(fā)送的數(shù)據(jù),,以及同時向多個客戶端發(fā)送響應數(shù)據(jù),。這種非阻塞通信采用了基于Channel(通道)、Selector(選擇器) ,、Buffer(緩沖器)的新模式,。
非阻塞I/O通信中Channel是一個接口,功能類似于傳統(tǒng)I/O中的Stream,,但通道具有雙向性,,既可讀入,也可寫出,。SocketChannel和ServerSocketChannel均可實現(xiàn)Channel接口,,它們是Socket和ServerSocket的替代類,但比Socket和ServerSocket具有更多的功能,,它支持非阻塞通信,、可選擇通信、異步通信和套接字對等通信等,。
由于網(wǎng)絡接收和傳送數(shù)據(jù)均比較緩慢,,常常導致線程阻塞,影響系統(tǒng)性能,。在Java非阻塞通信模式中使用Buffer來緩沖數(shù)據(jù),。SocketChannel和ServerSocketChannel對象一端連接著緩沖區(qū),另一端連接著網(wǎng)絡,。
ServerSocketChannel和SocketChannel在Selector中注冊連接就緒事件,、讀就緒事件和寫就緒事件。注冊時創(chuàng)建一個SelectionKey對象,,這個SelectionKey對象用來跟蹤注冊事件的句柄,,其中包含有注冊該對象的通道和緩沖區(qū)等信息。Selector會一直監(jiān)控已經(jīng)注冊的事件,。當通道中有數(shù)據(jù)需要讀取時,,Selector中就有已注冊的讀就緒事件發(fā)生,,這時調用相關的程序可將通道中的數(shù)據(jù)讀到緩沖區(qū)中,然后再提供給程序進行處理,。同樣地,,當有數(shù)據(jù)需要通過網(wǎng)絡發(fā)送時,數(shù)據(jù)先進入緩沖區(qū),,這時Selector中就發(fā)生寫就緒事件,,程序再利用通道向網(wǎng)絡發(fā)送數(shù)據(jù)[1]。
非阻塞通信模式處理流程如下:
while(檢測Selector對象,,等待連接就緒事件,、讀就緒事
件或寫就緒事件發(fā)生) { //阻塞
if(有客戶連接)//非阻塞
接收客戶連接,創(chuàng)建SocketChannel對象與客
戶通信,;
if(某個SocketChannel輸入流中有可讀數(shù)據(jù))
//非阻塞
從輸入流讀數(shù)據(jù),;
if(某個SocketChannel輸出流中有可寫數(shù)據(jù))
//非阻塞
向輸出流寫數(shù)據(jù);
}
上述處理流程采用輪詢工作方式,,當某一種操作就緒時,,執(zhí)行該操作,否則就查看是否還有其他就緒操作可以執(zhí)行,,線程不會因為某一個連接,、讀、寫等操作還沒有就緒,,就進入阻塞狀態(tài),。在非阻塞通信模式中,只需要一個線程管理Selector對象就可以了,,而不需要多線程,。并且只有檢測Selector對象時才有可能導致線程阻塞,,因此可以大大提高系統(tǒng)的性能,。
5 非阻塞通信在鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)中的應用研究
在鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)中,服務器端需要和各個客戶端建立連接,、接受客戶端的操作請求并將操作結果返回給客戶端,。服務器只需要使用一個線程就可以處理客戶端連接請求和向各個客戶端讀寫數(shù)據(jù),服務器自身進行的數(shù)據(jù)庫操作可以使用另外的線程,。
專家端和醫(yī)務端在服務器端的數(shù)據(jù)庫中進行查詢,、插入和修改等操作,這些操作信息是各種SQL語句,,服務器返回給專家端和醫(yī)務端的是查詢,、保存或修改結果,它們可能是一個字符串,、一條記錄,,也可能是多條記錄,。為了方便操作,這些信息均封裝成XML格式,,根標志為<teleMedi></teleMedi>,。這種格式封裝數(shù)據(jù)便于檢驗數(shù)據(jù)的完整性與正確性,也容易解析,。在非阻塞通信中,,除boolean類型以外,每種基本數(shù)據(jù)類型都有對應的緩沖區(qū)類,,在本系統(tǒng)中使用ByteBuffer類對象數(shù)據(jù)緩沖區(qū),。程序中需要向網(wǎng)絡發(fā)送數(shù)據(jù)時,數(shù)據(jù)進入緩沖區(qū)前先要把字符數(shù)據(jù)編碼成字節(jié)數(shù)據(jù),,然后再由通道向網(wǎng)絡發(fā)送,,而從網(wǎng)絡上接收的字節(jié)流數(shù)據(jù)先存放進緩沖區(qū),解碼后再由程序處理,。
在非阻塞通信模式中,,一次完整的數(shù)據(jù)發(fā)送可能需要多次讀緩沖區(qū),這避免了阻塞通信模式中讀取數(shù)據(jù)時間拖得過長而導致的線程阻塞,。每次讀緩沖區(qū)后都要進行數(shù)據(jù)完整性檢查,,由于采用XML格式封裝數(shù)據(jù),所以很容易檢驗是不是讀取到了完整的數(shù)據(jù),。
鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)中每次通過網(wǎng)絡讀寫的數(shù)據(jù)長短不同,。醫(yī)療中需要傳送的數(shù)據(jù)最常見的長度小于1 KB,因此緩沖區(qū)最初設定為1 KB,,當數(shù)據(jù)超過這個長度時,,將緩沖區(qū)的容量擴大一倍,如果超過2 KB,,再將緩沖區(qū)擴大一倍,,如此類推。使用這種可伸縮的方式可以更有效地利用內存資源,。
6 非阻塞通信在鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)中的實現(xiàn)[2]
在非阻塞模式下,,服務器主程序MedicalSever只使用一個線程,使用Selector監(jiān)控接收連接就緒事件,、讀就緒事件和寫就緒事件,。限于篇幅,本文僅介紹幾段主要的代碼,。
(1)MedicalServer類的構造方法負責啟動服務器,,把它綁定到一個本地端口,主要代碼如下:
selector=Selector.open();//創(chuàng)建一個Selector對象
ssChannel=ServerSocketChannel.open();
//創(chuàng)建ServerSocketChannel對象
ssChannel.configureBlocking(false);
//設置ServerSocketChannel為阻塞工作模式
ssChannel.socket().bind(new InetSocketAddress(port));
//綁定到本地端口
(2)MedicalSever類的serve()方法負責輪詢Selector,主要代碼如下:
public void serve() throws IOException{
ssChannel.register(selector,SelectorKey.OP_ACCEPT);
//在Selector中注冊連接就緒事件
while(selector.select()>0){
Set readyKeys=selector.selectedKeys();
//獲取Selector中就緒事件集合
Iterator it=readyKeys.iterator();
while(it.hasNext()){
SelectorKey key=null;
try{
key=(SelectionKey)it.next();
//獲取某個就緒事件
it.remove();//獲取就緒事件后刪除
if(key.isReadable()){處理連接就緒事件}
if(key.isReadable()){處理讀就緒事件}
if(key.isWritable()){處理寫就緒事件}
}catch(IOException e){ }}}
(3)處理連接就緒事件方法中的主要代碼如下:
ServerSocketChannel ssc=(ServerSocketChannel)key.channel();
SocketChannel sChannel=(SocketChannel)ssc.accept();
//獲取關系的SocketChannel
socketChannel.configureBlocking(false);
//將SocketChannel設置為非阻塞模式
ByteBuffer buffer=ByteBuffer.allocate(1024);//分配緩沖區(qū)
socketChannel.register(selector,SelectorKey.OP_READ|
SelectionKey.OP_WRITER,buffer);
//注冊讀就緒和寫就緒事件
(4)處理讀就緒事件方法中的主要代碼如下:
public void receive(SelectionKey key)throws IOException{
ByteBuffer sBuffer=(ByteBuffer)key.attachment();
//獲取與SelectorKey綁定的緩沖區(qū)
SocketChannel sChannel=
(SocketChannel)key.channel();//獲得通道
ByteBuffer buff=ByteBuffer.allocate(1024)
//創(chuàng)建輔助緩沖區(qū)
sChannel.read(buff);//從通道中讀數(shù)據(jù)暫存到buff中
sBuffer.put(buff); }//把buff中的內容拷貝到緩沖區(qū)
在非阻塞模式下,,sChannel.read(buff)方法無法保證一次把全部數(shù)據(jù)都讀完,,因此只好把每次讀到的數(shù)據(jù)先存放到buff中,并判斷每次讀到的數(shù)據(jù)是否以</teleMedi>結尾,,當數(shù)據(jù)讀取完整了,,再復制到sBuffer中提交給程序。
(5)處理寫就緒事件方法中的主要代碼如下:
public void send(SelectionKey key)throws IOExceptio{
ByteBuffer sBuffer=(ByteBuffer)key.attachment();
//獲取需要發(fā)送數(shù)據(jù)的緩沖區(qū)
SocketChannel sChannel=(SocketChannel)key.channel();
synchronized(sBuffer)
{ sBuffer.flip();//將緩沖當前的位置值設為
極限,,將位置設為0,,為下一步復制數(shù)據(jù)做準備
sChannel.write(sBuffer);//發(fā)送緩沖區(qū)中的數(shù)據(jù)
sChannel.compact();}//刪除已發(fā)送的數(shù)據(jù)
(6)當每個緩沖區(qū)容量不夠時,需要擴大緩沖區(qū),,主要代碼如下:
protected void resizeBuffer(ByteBuffer bb)
{ if(bb.remaining()<10){//判斷剩余容量是否
小于10 B
ByteBuffer bBuffer=ByteBuffer.allocate(bb.capacity()*2);
//容量擴大一倍
bb.flip();
bBuffer.put(bb);//把原緩沖區(qū)中數(shù)據(jù)復制到
新緩沖區(qū)中
bb=bBuffer;}}
鄉(xiāng)村可視化遠程醫(yī)療系統(tǒng)中使用Java非阻塞I/O通信技術,,可以只使用一個主線程就能實現(xiàn)網(wǎng)絡連接、讀和寫操作,,避免了多線程中讀或寫引起的線程阻塞,,大幅降低了服務器應用程序的開銷,有效地提高了系統(tǒng)的性能,。
參考文獻
[1] 吳易,,王凌.Java技術在P2P環(huán)境下的應用[J].微計算機信息,2005(3):154-155.
[2] JavaTM Platform Standard Edition 6 API Specification[S]. http://download.oracle.com/javase/6/docs/api/.