前言
說到開源。恐怕非常少有人不挑大指稱贊。
學生通過開源碼學到了知識,程式猿通過開源類庫獲得了别人的成功經驗及可以按時完畢手頭的project,商家通過開源軟體賺到了錢……,總之是皆大歡喜。
然而開源軟體或類庫的首要缺點就是大多缺乏具體的說明文檔和使用的樣例,或者就是軟體代碼随便你用。就是文檔,樣例和後期服務收錢。這也難怪,畢竟就像某個著名NBA球員說的那樣:“我還要養家。是以千萬美元下面的合同别找我談。否則我甯可待業”。是啊,支援開源的人也要養家,收點錢也隻是分。要想既不花錢又學到知識就僅僅能借助網絡和了,我僅僅是想抛磚引玉,為開源事業做出點微薄共獻,能為你的project解決哪怕一個小問題,也就足夠了。
盡管我的這個系列介紹的東西不是什麼Web架構,也不是什麼開源server,可是我相信,作為一個程式猿,什麼樣的問題都會遇到。有時候越是簡單的問題反而越棘手。越是小的地方就越是找不到稱手的家夥。
僅僅要你不是整天僅僅與“架構”、“構件”、“架構”打交道的話,相信我所說的東西你一定會用到。
1 序列槽通信簡單介紹... 1
1.1 常見的Java序列槽包... 1
1.2 序列槽包的安裝(Windows下)... 1
2 序列槽API概覽... 2
2.1 javax.comm.CommPort2
2.2 javax.comm.CommPortIdentifier3
2.3 javax.comm.SerialPort3
2.4 序列槽API執行個體... 3
2.4.1 列舉出本機全部可用序列槽... 3
2.4.2 序列槽參數的配置... 4
2.4.3 序列槽的讀寫... 4
3 序列槽通信的通用模式及其問題... 5
3.1 事件監聽模型... 5
3.2 序列槽讀資料的線程模型... 6
3.3 第三種方法... 7
4 結束語... 9
1 序列槽通信簡單介紹
嵌入式系統或傳感器網絡的非常多應用和測試都須要通過PC機與嵌入式裝置或傳感器節點進行通信。
當中,最經常使用的接口就是RS-232序列槽和并口(鑒于USB接口的複雜性以及不須要非常大的傳輸資料量。USB接口用在這裡還是顯得過于奢侈。況且眼下除了SUN有一個支援USB的包之外。我還沒有看到其它直接支援USB的Java類庫)。
SUN的CommAPI分别提供了對經常使用的RS232串行port和IEEE1284并行port通訊的支援。
RS-232-C(又稱EIA RS-232-C。下面簡稱RS232)是在1970年由美國電子工業協會(EIA)聯合貝爾系統、數據機廠家及計算機終端生産廠家共同制定的用于串行通訊的标準。
RS232是一個全雙工的通訊協定。它能夠同一時候進行資料接收和發送的工作。
1.1 常見的Java序列槽包
眼下,常見的Java序列槽包有SUN在1998年公布的序列槽通信API:comm2.0.jar(Windows下)、comm3.0.jar(Linux/Solaris);IBM的序列槽通信API以及一個開源的實作。鑒于在Windows下SUN的API比較經常使用以及IBM的實作和SUN的在API層面都是一樣的。那個開源的實作又不像兩家大廠的産品那樣讓人放心。這裡就僅僅介紹SUN的序列槽通信API在Windows平台下的使用。
1.2 序列槽包的安裝(Windows下)
到SUN的站點下載下傳javacomm20-win32.zip。包括的東西例如以下所看到的:
依照其使用說明(Readme.html)的說法,要想使用序列槽包進行序列槽通信,除了設定好環境變量之外,還要将win32com.dll拷貝到<JDK>/bin檔案夾下;将comm.jar拷貝到<JDK>/lib;把javax.comm.properties也相同拷貝到<JDK>/lib檔案夾下。然而在真正執行使用序列槽包的時候,僅作這些是不夠的。由于通常當執行“java MyApp”的時候,是由JRE下的虛拟機啟動MyApp的。而我們僅僅複制上述檔案到JDK對應檔案夾下,是以應用程式将會提示找不到序列槽。解決問題的方法非常easy,我們僅僅須将上面提到的檔案放到JRE對應的檔案夾下就能夠了。
值得注意的是。在網絡應用程式中使用序列槽API的時候。還會遇到其它更複雜問題。有興趣的話,你能夠檢視CSDN社群中“關于網頁上Applet用javacomm20讀取client序列槽的問題”的文章。
2 序列槽API概覽
2.1 javax.comm.CommPort
這是用于描寫叙述一個被底層系統支援的port的抽象類。它包括一些高層的IO控制方法。這些方法對于全部不同的通訊port來說是通用的。SerialPort 和ParallelPort都是它的子類。前者用于控制串行port而後者用于控這并口,二者對于各自底層的實體port都有不同的控制方法。這裡我們僅僅關心SerialPort。
2.2 javax.comm.CommPortIdentifier
這個類主要用于對序列槽進行管理和設定,是對序列槽進行訪問控制的核心類。
主要包含下面方法
l 确定是否有可用的通信port
l 為IO操作打開通信port
l 決定port的全部權
l 處理port全部權的争用
l 管理port全部權變化引發的事件(Event)
2.3 javax.comm.SerialPort
這個類用于描寫叙述一個RS-232串行通信port的底層接口,它定義了序列槽通信所需的最小功能集。通過它。使用者能夠直接對序列槽進行讀、寫及設定工作。
2.4 序列槽API執行個體
大段的文字怎麼也不如一個小樣例來的清晰,以下我們就一起看一下序列槽包自帶的樣例---SerialDemo中的一小段代碼來加深對序列槽API核心類的用法的認識。
2.4.1 列舉出本機全部可用序列槽
void listPortChoices() {
CommPortIdentifier portId;
Enumeration en = CommPortIdentifier.getPortIdentifiers();
// iterate through the ports.
while (en.hasMoreElements()) {
portId = (CommPortIdentifier) en.nextElement();
if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) {
System.out.println(portId.getName());
}
}
portChoice.select(parameters.getPortName());
}
以上代碼能夠列舉出目前系統全部可用的序列槽名稱,我的機器上輸出的結果是COM1和COM3。
2.4.2 序列槽參數的配置
序列槽一般有例如以下參數能夠在該序列槽打開曾經配置進行配置:
包含波特率,輸入/輸出流控制,資料位數。停止位和齊偶校驗。
SerialPort sPort;
try {
sPort.setSerialPortParams(BaudRate,Databits,Stopbits,Parity);
//設定輸入/輸出控制流
sPort.setFlowControlMode(FlowControlIn | FlowControlOut);
} catch (UnsupportedCommOperationException e) {}
2.4.3 序列槽的讀寫
對序列槽讀寫之前須要先打開一個序列槽:
CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier(PortName);
SerialPort sPort = (SerialPort) portId.open("序列槽全部者名稱", 逾時等待時間);
} catch (PortInUseException e) {//假設port被占用就抛出這個異常
throw new SerialConnectionException(e.getMessage());
//用于對序列槽寫資料
OutputStream os = new BufferedOutputStream(sPort.getOutputStream());
os.write(int data);
//用于從序列槽讀資料
InputStream is = new BufferedInputStream(sPort.getInputStream());
int receivedData = is.read();
讀出來的是int型。你能夠把它轉換成須要的其它類型。
這裡要注意的是,由于Java語言沒有無符号類型。即全部的類型都是帶符号的,在由byte到int的時候應該尤其注意。由于假設byte的最高位是1。則轉成int類型時将用1來占位。
這樣,原本是10000000的byte類型的數變成int型就成了1111111110000000,這是非常嚴重的問題。應該注意避免。
3 序列槽通信的通用模式及其問題
最終唠叨完我最讨厭的基礎知識了,以下開始我們本次的重點--序列槽應用的研究。
因為向序列槽寫資料非常easy,是以這裡我們僅僅關注于從序列槽讀資料的情況。通常。序列槽通信應用程式有兩種模式,一種是實作SerialPortEventListener接口,監聽各種序列槽事件并作對應處理;還有一種就是建立一個獨立的接收線程專門負責資料的接收。
因為這兩種方法在某些情況下存在非常嚴重的問題(至于什麼問題這裡先賣個關子J)。是以我的實作是採用第三種方法來解決問題。
3.1 事件監聽模型
如今我們來看看事件監聽模型是怎樣運作的
:
l 首先須要在你的port控制類(比如SManager)加上“implements SerialPortEventListener”
l 在初始化時增加例如以下代碼:
SerialPort sPort.addEventListener(SManager);
} catch (TooManyListenersException e) {
sPort.close();
throw new SerialConnectionException("too many listeners added");
sPort.notifyOnDataAvailable(true);
l 覆寫public void serialEvent(SerialPortEvent e)方法,在當中對例如以下事件進行推斷:
BI -通訊中斷.
CD -載波檢測.
CTS -清除發送.
DATA_AVAILABLE -有資料到達.
DSR -資料裝置準備好.
FE -幀錯誤.
OE -溢位錯誤.
OUTPUT_BUFFER_EMPTY -輸出緩沖區已清空.
PE -奇偶校驗錯.
RI - 振鈴訓示.
一般最經常使用的就是DATA_AVAILABLE--序列槽有資料到達事件。
也就是說當序列槽有資料到達時,你能夠在serialEvent中接收并處理所收到的資料。然而在我的實踐中,遇到了一個十分嚴重的問題。
首先描寫叙述一下我的實驗:我的應用程式須要接收傳感器節點從序列槽發回的查詢資料。并将結果以圖示的形式顯示出來。
序列槽設定的波特率是115200,川口每隔128毫秒傳回一組資料(大約是30位元組左右),周期(即持續時間)為31秒。實測的時候在一個周期内應該傳回4900多個位元組,而用事件監聽模型我最多僅僅能收到不到1500位元組,不知道這些位元組都跑哪裡去了,也不清楚究竟丢失的是那部分資料。值得注意的是。這是我将serialEvent()中全部處理代碼都注掉。僅僅剩下列印代碼所得的結果。資料丢失的如此嚴重是我所不能忍受的,于是我決定採用其它方法。
3.2 序列槽讀資料的線程模型
這個模型顧名思義,就是将接收資料的操作寫成一個線程的形式:
public void startReadingDataThread() {
Thread readDataProcess = new Thread(new Runnable() {
public void run() {
while (newData != -1) {
try {
newData = is.read();
System.out.println(newData);
//其它的處理過程
……….
} catch (IOException ex) {
System.err.println(ex);
return;
}
}
readDataProcess.start();
}
在我的應用程式中,我将收到的資料打包放到一個緩存中。然後啟動還有一個線程從緩存中擷取并處理資料。
兩個線程以生産者—消費者模式協同工作。資料的流向例如以下圖所看到的:
這樣。我就圓滿攻克了丢資料問題。然而,沒高興多久我就又發現了一個相同嚴重的問題:盡管這回不再丢資料了,但是原本一個周期(31秒)之後,傳感器節電已經停止傳送資料了。但我的序列槽線程依舊在努力的執行讀序列槽操作,在控制台也能夠看見收到的資料仍在不斷的列印。原來,因為傳感器節點發送的資料過快,而我的接收線程處理隻是來。是以InputStream就先把已到達卻還沒處理的位元組緩存起來,于是就導緻了明明傳感器節點已經不再發資料了。而控制台卻還能看見資料不斷列印這一奇怪的現象。唯一值得慶幸的是最後收到資料确實是4900左右位元組,沒出現丢失現象。然而當處理完最後一個資料的時候已經快1分半鐘了,這個時間遠遠大于節點執行周期。
這一延遲對于一個實時的顯示系統來說簡直是災難!
後來我想,是不是因為兩個線程之間的同步和通信導緻了資料接收緩慢呢?于是我在接收線程的代碼中去掉了全部處理代碼。僅保留列印收到資料的語句,結果依舊如故。看來并非線程間的通信阻礙了資料的接收速度。而是用線程模型導緻了對于發送端資料發送速率過快的情況下的資料接收延遲。這裡申明一點,就是對于資料發送速率不是如此快的情況下前面者兩種模型應該還是好用的。僅僅是特殊情況還是應該特殊處理。
3.3 第三種方法
痛苦了許久(Boss天天催我L)之後,偶然的機會。我聽說TinyOS中(又是開源的)有一部分是和我的應用程式相似的序列槽通信部分,于是我下載下傳了它的1.x版的Java代碼部分,參考了它的處理方法。
解決這個問題的方法說穿了事實上非常easy。就是從根源入手。根源不就是接收線程導緻的嗎,那好。我就幹脆取消接收線程和作為中介的共享緩存,而直接在處理線程中調用序列槽讀資料的方法來解決這個問題(什麼,為什麼不把處理線程也一并取消?----都取消應用程式界面不就鎖死了嗎?是以必須保留)于是程式變成了這樣:
public byte[] getPack(){
while (true) {
// PacketLength為資料包長度
byte[] msgPack = new byte[PacketLength];
for(int i = 0; i < PacketLength; i++){
if( (newData = is.read()) != -1){
msgPack[i] = (byte) newData;
System.out.println(msgPack[i]);
}
return msgPack;
}
在處理線程中調用這種方法傳回所須要的資料序列并處理之。這樣不但沒有丢失資料的現象行出現。也沒有資料接收延遲了。這裡唯一須要注意的就是當序列槽停止發送資料或沒有資料的時候is.read()一直都傳回-1。假設一旦在開始接收資料的時候發現-1就不要理它,繼續接收。直到收到真正的資料為止。
4 結束語
本文介紹了序列槽通信的基本知識,以及經常使用的幾種模式。通過實踐,提出了一些問題,并在最後加以解決。值得注意的是對于第一種方法。我曾将傳感器發送的時間由128毫秒添加到512毫秒。仍然有非常嚴重的資料丢失現象發生,是以假設你的應用程式須要非常精密的結果。資料傳輸的速率又非常快的話,就最好不要用第一種方法。對于另外一種方法,因為是線程導緻的問題,是以對于不同的機器應該會有不同的表現。對于那些處理多線程比較好的機器來說,應該會好一些。可是我的機器是Inter 奔四3.0雙核CPU+512DDR記憶體。這樣都延遲這麼厲害。還強得多CPU才行啊?是以,對于大量的資料傳輸的,第三種方法是用它。但是,這是非常多的世界難題,還有更多懸而未決的問題比已知問題,也許還有什麼其他問題。通過以下聯系方式歡迎你和我一起學習。