在看到本文之前,如果讀者沒看過筆者的前文 Java實現(xiàn)Socket網(wǎng)絡(luò)編程(四),請先翻閱。
接下來,筆者對幾個核心點進行剖析:
1、如果讀者是初次進行Socket網(wǎng)絡(luò)編程開發(fā),起初可能會因為端口的使用不當(dāng),而導(dǎo)致Socket無法連接。所以讀者可以在編程前進行測試,這里筆者提供了一個方法:
/**
* 用于檢測能連接到的端口號
*/
public static void scan(String host) {
Socket socket = null;
for (int port = 1024; port < 10055; port++) {
try {
socket = new Socket(host, port);
System.out.println(socket);
} catch (IOException e) {
continue;
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2、當(dāng)發(fā)現(xiàn)程序只能在本機運行,而在其他機器上不能運行時,是因為讀者所使用的ip地址是本機ip地址,如果采用 loop back 地址 "127.0.0.1",則可以在任意機器上運行。(本案例實際上是本機服務(wù)器與本機客戶端的Socket通信)
3、筆者在測試過程中,發(fā)現(xiàn)了這樣一個現(xiàn)象:服務(wù)器檢測客戶端斷開,零延時;而客戶端檢測服務(wù)器斷開,有明顯延時(延時長短和運行機器的速度有關(guān))。
這是什么原因?qū)е碌哪兀?strong>經(jīng)測試發(fā)現(xiàn),服務(wù)器檢測客戶端斷開,心跳包異常的捕獲速度快于讀寫錯誤的捕獲,從而零延時。而客戶端檢測服務(wù)器斷開,讀寫錯誤的捕獲速度快于心跳包異常的捕獲速度,導(dǎo)致有延時。
這種現(xiàn)象的產(chǎn)生,是由于Java底層對”服務(wù)器監(jiān)聽客戶端斷開“及”客戶端監(jiān)聽服務(wù)器斷開“的實現(xiàn)機制不一樣,導(dǎo)致了兩者之間的速度差異。
既然是實現(xiàn)機制的原因,那我們是否就沒有解決的方法呢?顯然不屈不饒的程序員不會就此善罷甘休。
服務(wù)器檢測客戶端斷開,零延時,我們不需要再過多干預(yù);我們要干預(yù)客戶端檢測服務(wù)器斷開,以使得比捕獲讀寫異常的速度更快地發(fā)現(xiàn)服務(wù)器斷開。
筆者經(jīng)過多次嘗試:
①首先是想到自定義一個結(jié)束符,例如"bye",希望通過服務(wù)器傳遞”bye“給客戶端,客戶端就”意識到“服務(wù)器關(guān)閉。這可以做到,但是存在一個漏動,如果”bye“是由使用者在服務(wù)器對話框發(fā)送過去的呢?那樣客戶端就會“以為”服務(wù)器斷開。
既然這樣,筆者就想著改進,把結(jié)束符“bye”改成轉(zhuǎn)義字符,如"\\u0025",讓用戶不容易輸入,經(jīng)過數(shù)次測試發(fā)現(xiàn),用戶毫無一例會成功輸入"\\u0025"結(jié)束符,看起來毫無Bug,但這顯然還是一個漏動,哪怕只有萬分之一的可能?
②為了進一步改進,筆者想著自定義一種數(shù)據(jù)協(xié)議格式:
【服務(wù)器是否啟動標(biāo)志位】【要接收的數(shù)據(jù)】
這看起來不錯,但前面已經(jīng)提到,客服端讀取數(shù)據(jù)要在for循環(huán)里,如果用String類的startsWith("啟動標(biāo)志位")方法判斷,則客戶端顯示數(shù)據(jù)由于無法判斷服務(wù)器一次性發(fā)送內(nèi)容的結(jié)束,會導(dǎo)致如下輸出結(jié)果:
s
sa
say
say h
say he
say hel
say hell
say hello
say hello!
③既然如此,也就是要為數(shù)據(jù)提供結(jié)束符,以判斷數(shù)據(jù)長度,筆者再一次修改數(shù)據(jù)協(xié)議格式:【要接收的數(shù)據(jù)】【服務(wù)器是否啟動標(biāo)志位】
通過String類的endsWith("啟動標(biāo)志位")進行檢測
然而,這又產(chǎn)生了一個新問題,當(dāng)用戶直接輸入”服務(wù)器斷開標(biāo)志位“,客戶端又再一次”認(rèn)為“服務(wù)器已斷開。
④筆者進一步考慮,如果加上長度判斷,當(dāng)輸入內(nèi)容s.length()>"標(biāo)志為長度"且endsWith("啟動標(biāo)志位")進行檢測。
心想理應(yīng)成功,沒料到又出現(xiàn)這樣的一種情況:當(dāng)用戶輸入任意長度字符+”服務(wù)器斷開標(biāo)志位“時,客戶端又再一次”認(rèn)為“服務(wù)器已斷開。
⑤歷經(jīng)多次挫折,筆者最后對比使用C#的Socket網(wǎng)絡(luò)編程實現(xiàn),研究了其 Send()方法和 Receive()方法(Send方法用于發(fā)送數(shù)據(jù),Receive方法用于接收數(shù)據(jù),C#封裝了底層輸入輸出流的實現(xiàn),而Java沒有,所以需要進行輸入輸出流的操作)
總結(jié)出一種方法:模擬C#的Receive函數(shù)(該函數(shù)具有檢測服務(wù)器是否斷開,且能返回接收數(shù)據(jù)長度)
筆者定義了以下的數(shù)據(jù)協(xié)議格式,徹底解決了”客戶端延時發(fā)現(xiàn)服務(wù)器斷開“、”發(fā)送數(shù)據(jù)無邊界“的問題:【服務(wù)器是否啟動標(biāo)志位】【接收數(shù)據(jù)的長度】【要接收的數(shù)據(jù)】
在發(fā)送數(shù)據(jù)前進行包裝
String message = Common.OK; // 代表服務(wù)器正常連接
String t = "server " + Common.IP + ":" + Common.PORT + " "
+ jtaSendMessage.getText();
/**
* 封裝發(fā)送數(shù)據(jù)的長度
*/
String c = "" + t.length();
if (c.length() < 2) {
c = "000" + c;
} else if (c.length() < 3) {
c = "00" + c;
} else if (c.length() < 4) {
c = "0" + c;
}
message += c + t;
在收數(shù)據(jù)時進行解封
// 使用"GBK"編碼讀取中文
brIn = new BufferedReader(new InputStreamReader(
mSocket.getInputStream(), "GBK"));
String s = "";// 記錄每次讀取的內(nèi)容
int count = -10;// 記錄每次讀取內(nèi)容的長度
// 接收內(nèi)容并把內(nèi)容添加到信息接收區(qū)
for (int c = brIn.read(); c != -1; c = brIn.read()) {
s += (char) c + "";
count++;// 讀取的長度
// 如果服務(wù)器連接且一次數(shù)據(jù)接收完成
if (s.startsWith(Common.OK) && s.length() > 10
&& count == Integer.parseInt((s.substring(6, 10)))) {
ClientMain.jtaReceivedMessage.append(s.substring(10)
+ "\n");
count = -10;
s = "";
}// 服務(wù)器斷開且一次數(shù)據(jù)接收完成
else if (s.startsWith(Common.ERROR) && s.length() > 10
&& count == Integer.parseInt((s.substring(6, 10)))) {
ClientMain.jtaReceivedMessage.append(s.substring(10)
+ "\n");
count = -10;
s = "";
ClientMain.jlConnect.setText("Out Of Connect.");
}
// 滾動到底端
ClientMain.jtaReceivedMessage
.setCaretPosition(ClientMain.jtaReceivedMessage
.getText().length());
}
以上為本次案例的全部內(nèi)容,最后,筆者在github上給出了這個案例的完整源碼和可運行文件Socket網(wǎng)絡(luò)編程,供讀者學(xué)習(xí)思考。
謝謝支持!