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/,Java实现以下功能也很简单,请同学们自己上网。

以下代码实现了,客户端发送命令到服务器,服务器接收到,解析,根据请求,返回需要的结果。

服务端:

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: 天气}

以上本文结束,有疑问建议,请直接QQ:270406

猜你喜欢

转载自blog.csdn.net/gear1023/article/details/83689447
今日推荐