Dart Socket 程式設計,通過使用JSON方式,解決業務粘包的問題的最佳實踐
一、背景
Socket程式設計程式設計主用於資料交換,而粘包的問題,其實本身不是問題,TCP已經對於傳輸的封包進行了很好的處理,業務粘包,只是業務處理上的問題,網路上很多處理方法,最常見的有以下幾種:
- 定義業務傳輸頭,在頭裡面描述了開始識別符號,再加資料長度,如0xAA + 資料長度,傳送和接收端都通過固定格式進行讀取處理
- 明確傳輸協議,如採用XML段或JSON格式進行傳輸,在接收完成後再進行業務處理
- 自定義某種格式,如Redis的協議,主要用於多次業務互動
實際工作過程中,根據實際需要進行選擇即可,沒有特別的說明,重要的是要對SOCKET的業務傳輸要明確其機理,否則會有很多坑等著你,包括但不限於:
- 編碼
- 資料格式
- 服務端緩衝
- 讀寫順序
結合來講,做為業務應用,我的建議是,不要採用多種資料型別,一個是不好理解,二是很難除錯,所有的傳輸都採用某一種編碼的字串進行,業務操作等傳送接收完成後再進行處理,不要在傳輸層卡住。
本文主要通過JSON進行封包傳輸,對於SOCKET程式設計進行描述,方便讀者閱讀和理解。
二、Socket 程式設計理論簡介
Socket 分為服務端和客戶端,要發起一個互動,服務端要先啟動,客戶端請求連線,連線成功,服務端和客戶端即可進行資訊傳輸,相當於架設了一個管道,資訊即可在這個管裡進”流動“,這個資訊傳輸是一個叫做”流“的東西,一般程式語言中,都稱為Stream,如下圖所示
而管道里的內容就如同水流一樣:
一個服務端,可同時支撐一個或多個客戶端連線,完成資訊的互動。
從開發程式設計的角度來看,接收的資訊是連續不斷的,每次接收的資訊,不一定完全按照你實際業務過程一次性傳輸完成,你只有根據實際業務需要進行讀取,解析後按業務進行組合或拆分,才能得到你實際要的資料。
一個TCP包,我們最多可以傳輸8K的資料,理論上講,SOCKET傳輸,只要資料不超過8K,就可以一次性傳輸完成。對於超過8K的資料,底層就要進行拆分後再傳輸,這時就出現了多次接收(觸發),就要進行組合。如下圖所示的業務資料分成了3個包。
如果業務資料每個都很小,可能會出現一個TCP包裡包含了多個業務資料,這個就是粘包
無論使用的是哪一種,我們都要對已經接收的資料快取起來,找到業務段的開始和結束位置,然後再進行處理。
三、使用JSON進行業務傳輸
資料互動協議前面已經描述,不再多說,說說JSON的好處:
- 格式易讀,資訊可見
- 除錯方便,所見即所得,不用轉來轉去,各種語言都有內建直接轉換的方法
- 通用性強,幾乎所有的新系統都支援
- UTF8編碼,沒那麼多費話,少扯淡
有了以上幾點,可省去前面所說的幾乎所有麻煩,開發時用心寫多一點,健壯一點就可以了。如果做一個通用的互動,傳輸的問題一步就解決了,讓你的重心專注在業務上。封包好後,供其它模組使用
四、Dart 實現程式設計的程式碼例項
Dart 是一種全類似(C、Java、JavaScript)的面向物件的語,主要用於跨平臺開發,本文不對Dart進行深入講解,有興趣的同學自行前往 https://www.dartlang.org/
服務端:
import 'dart:io';
import 'dart:convert';
/**
* Author: Jonny Zheng [email protected]
*
* 啟動Socket服務,我們假設傳輸的協議都是JSON,所以解析時以JSON進行解析
* 本例子僅用於演示目標,實際應用中,需考慮:
* 1、端口占用
* 2、傳輸超時重置、客戶端不正常造成資料混亂重置等
*/
void startServer(){
ServerSocket
.bind('127.0.0.1', 4041) //繫結埠4041,根據需要自行修改,建議用動態,防止端口占用
.then((serverSocket) {
serverSocket.listen((socket) {
var tmpData="";
socket.transform(utf8.decoder).listen((s) {
tmpData = doParseResultJson(socket, tmpData, s);
});
}
);
}
);
print(DateTime.now().toString() + " Socket服務啟動,正在監聽埠 4041...");
}
/**
* 按JSON格式進行解析收到的結果,無論是否粘包,都是可進行解析
* sData:為已經收到的臨時資料
* s:為當前收到的資料
* 返回結果為未處理的所有資料。
*/
String doParseResultJson(Socket socket, String sData, String s){
var tmpData = sData + s;
//log(socket, "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
log(socket, s);
log(socket, "-----------------------------------------");
log(socket, tmpData);
log(socket, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
// 找這個串裡有沒有相應的JSON符號
// 沒有的話,將資料返回等下一個包
var bHasJSON = tmpData.contains("{") && tmpData.contains("}");
if (!bHasJSON) {
return tmpData;
}
//找有類似JSON串,看"{"是否在"}"的前面,
//在前面,則解析,解析失敗,則繼續找下一個"}"
//解析成功,則進行業務處理
//處理完成,則對剩餘部分遞迴解析,直到全部解析完成(此項一般用不到,僅適用於一次發兩個以上的JSON串才需要,
//每次只傳一個JSON串的情況下,是不需要的)
int idxStart = tmpData.indexOf("{");
int idxEnd = 0;
while (tmpData.contains("}", idxEnd)) {
idxEnd = tmpData.indexOf("}", idxEnd) + 1;
log(socket, '{}=>' + idxStart.toString() + "--" + idxEnd.toString());
if (idxStart >= idxEnd) {
continue;// 找下一個 "}"
}
var sJSON = tmpData.substring(idxStart, idxEnd);
log(socket, "解析 JSON ...." + sJSON);
try{
var jsondata = jsonDecode(sJSON); //解析成功,則說明結束,否則丟擲異常,繼續接收
log(socket, "解析 JSON OK :" + jsondata.toString());
///此處加入你要處理的業務方法,一般呼叫另外一個方法進行下一步處理
doCommand(socket, jsondata);
tmpData = tmpData.substring(idxEnd); //剩餘未解析部分
idxEnd = 0; //復位
if (tmpData.contains("{") && tmpData.contains("}")) {
tmpData = doParseResultJson(socket, tmpData, "");
break;
}
} catch(err) {
log(socket, "解析 JSON 出錯:" + err.toString() + ' waiting for next "}"....'); //丟擲異常,繼續接收,等下一個}
}
}
return tmpData;
}
/**
* 舉例,支援的幾個命令 current time, XX, 天氣
* current time:問當前時間,就看一下是北京的還是倫敦的
* xx:返回YY
* 天氣:返回固定多雲轉陰天,有大雨!
*/
void doCommand(Socket clientsocket, jsonData) {
var command = jsonData['cmd'].toString().toUpperCase();
switch (command) {
case 'CURRENT TIME':
var region = jsonData['params']['region'];
if (region == '北京') {
clientsocket.write (region + '時間:' + DateTime.now().toString());
} else if (region == '倫敦') {
clientsocket.write(region + '時間:' + DateTime.now().add(Duration(hours:-8)).toString());
} else {
clientsocket.write (region + '時間:' + DateTime.now().toString());
}
break;
case 'XX':
clientsocket.write(command + " result YY");
break;
case '天氣':
clientsocket.write(command + ":多雲轉陰天,有大雨!");
break;
default:
clientsocket.write("不認識:command " + command);
}
}
void log(Socket socket, logdata) {
print(DateTime.now().toString() + "[" + socket.remoteAddress.address.toString() + ":" + socket.remotePort.toString() + "]" + logdata);
}
/**
* 主方法入口
*/
void main(){
startServer();
}
啟動很簡單,將以上程式碼儲存為sockserver.dart,然後使用:dart sockserver.dart即可啟動:
大概就是這樣了:
為了測試以上服務是否有效果,我們做了一個簡單的客戶端,模擬了合包和拆包的兩種情形:
import 'dart:async';
import 'dart:io';
import 'dart:convert';
/**
* Author: Jonny Zheng [email protected]
*
* 測試客戶端,傳送一個JSON串到伺服器,為模擬真實環境,採用分步傳送的方式進行
* 每隔1秒就傳送一小段程式碼
*/
void connectserver() {
Socket.connect('127.0.0.1', 4041).then((socket) async{
socket.transform(utf8.decoder).listen(print);
socket.write('{"cmd":"current time"');
await Future.delayed(const Duration(seconds: 1));
socket.write(',"params":{"region":"北京"}}');
await Future.delayed(const Duration(seconds: 1));
socket.write('{"cmd":"current time"');
await Future.delayed(const Duration(seconds: 1));
socket.write(',"params":{"region":"倫敦"}}{"cmd":"XX"}');
await Future.delayed(const Duration(seconds: 1));
socket.write('{}}{');
await Future.delayed(const Duration(seconds: 1));
socket.write('"cmd":"天氣"}');
});
}
void main(){
connectserver();
}
客戶端啟動:
服務端的處理結果:
PS C:\Users\gear1\blog> dart .\sockserver.dart
2018-11-03 20:14:42.648695 Socket服務啟動,正在監聽埠 4041...
2018-11-03 20:15:19.887908[127.0.0.1:14507]{"cmd":"current time"
2018-11-03 20:15:19.889902[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:19.889902[127.0.0.1:14507]{"cmd":"current time"
2018-11-03 20:15:19.889902[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:20.890266[127.0.0.1:14507],"params":{"region":"北京"}}
2018-11-03 20:15:20.890266[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:20.891226[127.0.0.1:14507]{"cmd":"current time","params":{"region":"北京"}}
2018-11-03 20:15:20.891226[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:20.891226[127.0.0.1:14507]{}=>0--46
2018-11-03 20:15:20.891226[127.0.0.1:14507]解析 JSON ....{"cmd":"current time","params":{"region":"北京"}
2018-11-03 20:15:20.899208[127.0.0.1:14507]解析 JSON 出錯:FormatException: Unexpected end of input (at character 47)
{"cmd":"current time","params":{"region":"北京"}
^
waiting for next "}"....
2018-11-03 20:15:20.900205[127.0.0.1:14507]{}=>0--47
2018-11-03 20:15:20.900205[127.0.0.1:14507]解析 JSON ....{"cmd":"current time","params":{"region":"北京"}}
2018-11-03 20:15:20.904223[127.0.0.1:14507]解析 JSON OK :{cmd: current time, params: {region: 北京}}
2018-11-03 20:15:21.890885[127.0.0.1:14507]{"cmd":"current time"
2018-11-03 20:15:21.891559[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:21.891559[127.0.0.1:14507]{"cmd":"current time"
2018-11-03 20:15:21.892552[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:22.892879[127.0.0.1:14507],"params":{"region":"倫敦"}}{"cmd":"XX"}
2018-11-03 20:15:22.893876[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:22.893876[127.0.0.1:14507]{"cmd":"current time","params":{"region":"倫敦"}}{"cmd":"XX"}
2018-11-03 20:15:22.893876[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:22.893876[127.0.0.1:14507]{}=>0--46
2018-11-03 20:15:22.893876[127.0.0.1:14507]解析 JSON ....{"cmd":"current time","params":{"region":"倫敦"}
2018-11-03 20:15:22.893876[127.0.0.1:14507]解析 JSON 出錯:FormatException: Unexpected end of input (at character 47)
{"cmd":"current time","params":{"region":"倫敦"}
^
waiting for next "}"....
2018-11-03 20:15:22.894871[127.0.0.1:14507]{}=>0--47
2018-11-03 20:15:22.894871[127.0.0.1:14507]解析 JSON ....{"cmd":"current time","params":{"region":"倫敦"}}
2018-11-03 20:15:22.894871[127.0.0.1:14507]解析 JSON OK :{cmd: current time, params: {region: 倫敦}}
2018-11-03 20:15:22.896868[127.0.0.1:14507]
2018-11-03 20:15:22.896868[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:22.896868[127.0.0.1:14507]{"cmd":"XX"}
2018-11-03 20:15:22.896868[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:22.897865[127.0.0.1:14507]{}=>0--12
2018-11-03 20:15:22.897865[127.0.0.1:14507]解析 JSON ....{"cmd":"XX"}
2018-11-03 20:15:22.897865[127.0.0.1:14507]解析 JSON OK :{cmd: XX}
2018-11-03 20:15:23.894204[127.0.0.1:14507]{}}{
2018-11-03 20:15:23.895201[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:23.895201[127.0.0.1:14507]{}}{
2018-11-03 20:15:23.896198[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:23.896198[127.0.0.1:14507]{}=>0--2
2018-11-03 20:15:23.896198[127.0.0.1:14507]解析 JSON ....{}
2018-11-03 20:15:23.896198[127.0.0.1:14507]解析 JSON OK :{}
2018-11-03 20:15:23.898277[127.0.0.1:14507]
2018-11-03 20:15:23.898277[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:23.898277[127.0.0.1:14507]}{
2018-11-03 20:15:23.899192[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:23.899192[127.0.0.1:14507]{}=>1--1
2018-11-03 20:15:24.895530[127.0.0.1:14507]"cmd":"天氣"}
2018-11-03 20:15:24.896528[127.0.0.1:14507]-----------------------------------------
2018-11-03 20:15:24.897523[127.0.0.1:14507]}{"cmd":"天氣"}
2018-11-03 20:15:24.898521[127.0.0.1:14507]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2018-11-03 20:15:24.898521[127.0.0.1:14507]{}=>1--1
2018-11-03 20:15:24.899515[127.0.0.1:14507]{}=>1--13
2018-11-03 20:15:24.899515[127.0.0.1:14507]解析 JSON ....{"cmd":"天氣"}
2018-11-03 20:15:24.900512[127.0.0.1:14507]解析 JSON OK :{cmd: 天氣}