1. 程式人生 > >Dart Socket 程式設計,通過使用JSON方式,解決業務粘包的問題的最佳實踐

Dart Socket 程式設計,通過使用JSON方式,解決業務粘包的問題的最佳實踐

一、背景

Socket程式設計程式設計主用於資料交換,而粘包的問題,其實本身不是問題,TCP已經對於傳輸的封包進行了很好的處理,業務粘包,只是業務處理上的問題,網路上很多處理方法,最常見的有以下幾種:

  1. 定義業務傳輸頭,在頭裡面描述了開始識別符號,再加資料長度,如0xAA + 資料長度,傳送和接收端都通過固定格式進行讀取處理
  2. 明確傳輸協議,如採用XML段或JSON格式進行傳輸,在接收完成後再進行業務處理
  3. 自定義某種格式,如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: 天氣}