Flutter中基于TCP的Socket使用

前段时间由于业务需要,使用到socket与PC端建立链接并传输文件,本篇文章主要记录在使用过程中涉及到的问题,包括本地socket服务、连接、消息发送与接收、拆包、大小端转换、文件切片、心跳、重连等内容。

启动本地socket服务

如果需要移动端作为socket服务端,可以使用 dart:io 库中的ServerSocket,通过IP和端口进行链接

startServer() async {
    try {
      _serverSocket =
          await ServerSocket.bind(InternetAddress.anyIPv4, serverPort);
      _serverSocket.listen(serverOnReceive);
    } catch (e, stackTrace) {
      LoggerUtil.e(e);
      LoggerUtil.e(stackTrace);
    }
  }

  serverOnReceive(Socket socket) {
    _socket = socket;

    _socket?.listen(serverReceiveMsg);
  }

serverReceiveMsg方法中收到消息进行拆包,这个下边会讲到。

Flutter连接其他Socket服务

这里我们需要使用到 dart:io 库中的socket.dart这个类中的Socket对象,使用.connect方法进行连接。

    Socket.connect(address, port,
            timeout: Duration(seconds: socketTimeout))
        .then((socket) async {
      _socket = socket;

      _socket?.listen(
        onReceivedMsg,
        onError: onError,
        onDone: onDone,
        cancelOnError: false,
      );
    }).catchError((error) {
      if (error is SocketException) {
        LoggerUtil.e(error);
      }
    });

接收socket消息并处理粘包

粘包简单来说就是多个socket消息连在了一起,每个socket消息前四个字节代表了这条消息的长度,我们根据前四个字节的内容来读取相应的长度,如此循环来读取所有的Socket消息内容

  //接收到socket消息
  onReceivedMsg(event) async {
    receiveList = receiveList + event;
    //当接收到的数据长度大于8读取消息头
    if (isPackReaded) {
      while (receiveList.length > 4) {
        isPackReaded = false;
        int headerLength = 4;
        //读取消息体长度
        int msgLength = byteToNum(receiveList.sublist(0, 4));

        //当收到的消息超过消息头描述的消息体长度时取出消息体并解码
        if (receiveList.length >= headerLength + msgLength) {
          List<int> bodyList =
              receiveList.sublist(headerLength, headerLength + msgLength);
          String bodyStr = utf8.decode(bodyList);
          //这里处理已经读取的Socket消息内容 进行解base64或者解密
          await analysisStr(bodyStr);
          //读取后删除已读取的消息
          receiveList = receiveList.sublist(headerLength + msgLength);
          if (receiveList.isEmpty) {
            isPackReaded = true;
          }
        } else {
          isPackReaded = true;
          break;
        }
      }
    }
  }

大小端转换

由于C中使用大端模式 所以要进行大小端的转换

  //小端转大端
  Uint8List int32BigEndianBytes(int value) {
    return Uint8List(4)..buffer.asByteData().setInt32(0, value, Endian.big);
  }

  //大端转小端
  int byteToNum(List<int> list) {
    Uint8List resultList = Uint8List.fromList(list);

    ByteData byteData = ByteData.view(resultList.buffer);

    return byteData.getInt32(0);
  }

消息发送

  //发送socket消息
  sendMsg(String msg) {
    try {
      Codec<String, String> stringToBase64 = utf8.fuse(base64);
      String base64Str = stringToBase64.encode(msg);

      //先告知消息长度-须转换为大端模式
      _socket?.add(int32BigEndianBytes(base64Str.length));

      //发送消息
      _socket?.write(base64Str);
      _socket?.flush();
    } catch (e) {
      debugPrint('========发送socket消息失败========$e');
    }
  }

消息类型

两端可自行约定socket消息类型

enum PackTypeEnum {
  packTypeUnknow,
  packTypeHeart,
  packTypeDisConnect,
}

心跳与重连

心跳可由服务端发起每隔30秒发送一次心跳消息,客户端收到后进行回应并将本地心跳变量重置。

        heartBeat = 0;
        String sendStr = jsonEncode({
          'packtype': PackTypeEnum.packTypeHeart.index,
          'data': 'pang',
        });
        debugPrint('===========接收到心跳=====');
        await sendMsg(sendStr);

本地定时任务进行检测,如果某一时间段内没有收到心跳,可能表示已经断开连接,尝试重新连接

  startHeartBeat() {
    _timer ??= Timer.periodic(const Duration(seconds: 1), (time) {
      heartBeat++;
      if (heartBeat > 40) {
        heartBeat = 0;
        //重连
        if (socketAddress != null && socketPort != null) {
          connectByAddress(socketAddress!, socketPort!);
        }
      }
    });
  }

文件切片传输

一些涉及业务的代码已经删除,核心就是拿到文件句柄,循环读取固定长度然后进行发送,所有片段发送完成后关闭

  //切片传输文件
  fileSlice(DocumentModel docModel, int docIndex, int docCount,
      int uploadTotal) async {
    debugPrint('=============发送文档===当前第-$fileIndex---共=$uploadTotal');
    try {
      for (DocumentFileModel docFile in docModel.fileList!) {
        String docFilePath = docFile.localFilePath!;

        File file = File(docFilePath);

        var handle = await file.open();
        var current = 0;
        var size = file.lengthSync();
        var chunkSize = 4096;
        int chunkIndex = 0;
        while (current < size) {
          var len = size - current >= chunkSize ? chunkSize : size - current;
          var section = handle.readSync(len); 
          current = current + len;
          // 处理数据块
          Map sendMap = {};
          chunkIndex += 1;
          String jsonStr = jsonEncode(sendMap);

          Codec<String, String> stringToBase64 = utf8.fuse(base64);
          String base64Str = stringToBase64.encode(jsonStr);

          _socket?.add(int32BigEndianBytes(base64Str.length));
          _socket?.write(base64Str);
          // 立即发送并清空缓冲区
          await _socket?.flush();
        }

        await handle.close();
        fileIndex += 1;
        EasyLoading.showProgress(
            (fileIndex / uploadTotal) > 1 ? 1 : fileIndex / uploadTotal,
            status: '已发送%s个'.trArgs(['$fileIndex/$uploadTotal']));
        if (fileIndex >= uploadTotal) {
          fileIndex = 0;
          EasyLoading.showSuccess('成功'.tr);
        }
      }
    } catch (e) {
      debugPrint('================${e.toString()}');
    }
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容