成帧与解析

阅读 《java TCP/IP Socket 编程》第三章笔记

成帧技术(frame)是解决如何在接收端定位消息的首尾位置的问题。在进行数据收发时,必须指定消息接收者如何确定何时消息已经接收完整。

在TCP协议中,消息是按照字节来传输的,而且TCP协议中是没有消息边界的概念的。因为当client和server双方建立TCP连接后,双方可以自由发送字节数据。

为了能够在消息传输中确定消息的边界,需要引入额外的信息来标示消息边界。常用的办法有两种:

基于定界符与基于显式消息长度

基于定界符

我们在消息的末尾添加一个唯一标记作为消息结束符,这个唯一的标记一般是一个字节或者一组字节序列,并且在消息中不能出现这个标记。

基于定界符的方法一般用于以文本方式编码的消息中,定义一个特殊的字符作为分隔符来表示消息结束。但是这个分隔符也有可能作为普通字符可能会出现在消息中,导致消息解析出现错误。为了让消息中不出现分隔符,需要引入填充(stuff)技术,在发送端对消息进行扫描,如果碰到分隔符,就将这个分隔符用一个替换符和其他符号(比如将原始字符二进制中的第三位取反得到一个新的字节作为)替换,同样的,如果扫描中遇到替换符,将替换符也用一个替换付和其他符号替换。在消息的接收端,同样也对接收到的消息进行扫描,当碰到替换符时,说明该字符不是消息中的,要将后面一个字符进行还原得到相应的原始字符,这个才是消息中真正的字符。当遇到分隔符时,说明该消息已经结束

显式消息长度

在消息前面添加一个固定大小的字段(一个字节或者两个字节长度),用于表示消息包含的字节个数(也就是消息的长度)。在消息发送时,计算消息的长度(字节数),作为消息的前缀。如果使用一个字节保存长度,则消息长度最大为\(2^8=256\)个字节,如果是两个字节保存长度,则消息长度最大为\(2^{16}=65536\)个字节

消息成帧与解析的实现

在java中,当client和server之间建立tcp连接后,就可以通过输入输出流(I/O stream)来进行消息传输。发送消息时,将待发送的消息写入OutputStream流中,然后发送到接收端InputStream流;接收端则从InputStream流中读取出消息。如何实现将消息按帧发送与接收,就需要要利用我们上面提到的方法。

我们先定义一个Framer接口,来声明两个方法,消息成帧frameMsg()和消息抽取nextMsg()

package chapter_3.frame;

import java.io.IOException;
import java.io.OutputStream; /**
* @author fulv
* Framer接口声明了两个方法,用于消息成帧和解析将待发送消息封装成帧并输出到指定流
*/
public interface Framer { /**
* 将输入的消息msg封装成帧,然后输出到out流
*
* @param msg 输入的消息
* @param out 消息输出流
*/
void frameMsg(byte[] msg, OutputStream out); /**
* 从指定流中读取下一个消息帧
*
* @return byte[]
*/
byte[] nextMsg() throws IOException;
}

然后分别使用基于分隔符和基于显式消息长度两种方法来实现Framer接口

基于分隔符:

在这里,我们使用字符'\n'作为消息分隔符,它对应的字节为0x0A;使用的替换符为0x7D。替换的策略是:当扫描到待发送的消息byte数组中有0x0A时,将其替换为(0x7D,0x2A),如果遇到0x7D,将其替换为(0x7D,0x5D)。这里面第二个字符通过将待替换字符从左向右数第三位取反获得。

在 接收端,从输入流中读取字节流数据,遇到0x7D时,说明后面一个字节对应的是特殊字节,需要转换得到原始字节。如果遇到0x0A说明到达消息帧末尾,完成了一个消息帧的读取。

package chapter_3.frame;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets; /**
* 采用界定符的方式来实现消息的封装成帧以及消息帧的解析
*
* @author fulv
*/
public class DelimitFramer implements Framer { /**
* 数据输入源,从中解析出消息帧
*/
private InputStream in; /**
* 消息帧的定界符
*/
private static final byte DELIMITER = '\n';
/**
* 替换字符,用于将出现在消息内部的'\n'进行替换,避免出现解析错误
*/
private static final byte REPLACE_CHAR = (byte) 0x7d; private static final byte MASK = (byte) 0x20; public DelimitFramer(InputStream in) {
this.in = in;
} @Override
public void frameMsg(byte[] msg, OutputStream out) {
//向判断传入的消息中是否包含界定符与替换符,如果存在,执行相关字节填充操作
//将对应的界定符和替换符换成两个字符,其中第一个为替换符,第二个为将要替换的字符的从左到右的第二位取反形成的字符
int count = 0;
for (byte b : msg) {
if (DELIMITER == b || REPLACE_CHAR == b) {
count++;
}
}
byte[] extendMsg = new byte[msg.length + count];
for (int i = 0, j = 0; i < msg.length; i++) {
if (DELIMITER == msg[i] || REPLACE_CHAR == msg[i]) {
extendMsg[j++] = REPLACE_CHAR;
extendMsg[j++] = byteStuff(msg[i]);
} else {
extendMsg[j++] = msg[i];
}
}
try {
out.write(extendMsg);
out.write(DELIMITER);
out.flush();
} catch (IOException e) {
e.printStackTrace();
System.out.println("消息写入流失败");
} } /**
* 从消息输入流in中,取出下一个消息帧(以分隔符划分一个消息帧)
*
* @return
*/
@Override
public byte[] nextMsg() throws IOException {
ByteArrayOutputStream msgBuffer = new ByteArrayOutputStream();
int nextByte; while ((nextByte = in.read()) != DELIMITER) {
//已经读完了输入流,这里分两种情况
if (-1 == nextByte) {
//输入流中的字节已经全部读完
if (msgBuffer.size() == 0) {
return null;
} else {
//读取了部分字节,但却没有遇到分隔符,说明输入的消息帧是不完整或者错误的,返回异常
throw new EOFException("读取到了不正确的消息帧");
}
} //当前字符为替换字符,需要读取下一个字符并转换(将第三位取反)得到正确的字符
if (REPLACE_CHAR == nextByte) {
nextByte = in.read() & 0xFF;
nextByte = byteStuff((byte) nextByte);
}
msgBuffer.write(nextByte);
}
return msgBuffer.toByteArray();
} /**
* 字节填充函数,将传入字节的从左到右数的第二位取反
*
* @param originByte
* @return
*/
private static byte byteStuff(byte originByte) {
return (byte) ((originByte | MASK) & ~(originByte & MASK));
}
}

基于显式消息长度方法:

使用两个字节无符号整型来表示待发送消息的长度,最长为65536。将消息长度按照字节大端序写入待发送的消息前,表示消息长度。

接收端,首先从输入流中读出消息长度,然后堵塞的从输入流中读取数据,直到读取出的数据量达到消息长度,整个消息帧才读取结束。

package chapter_3.frame;

import java.io.*;

/**
* 基于显式长度的方法来将实现消息成帧
*
* @author fulv
*/
public class LengthFramer implements Framer { private static final int MESSAGEMAXLENGTH = 65536; private DataInputStream in; public LengthFramer(DataInputStream in) {
this.in = in;
} @Override
public void frameMsg(byte[] msg, OutputStream out) throws IOException {
if (msg.length > MESSAGEMAXLENGTH) {
throw new IOException("传入的消息超出最大长度");
}
int msgLength = msg.length;
//将消息长度按照字节大端序写入输出流中
out.write((msgLength >> 8) & 0xFF);
out.write(msgLength & 0xFF);
//将消息写入输出流
out.write(msg);
out.flush();
} @Override
public byte[] nextMsg() throws IOException {
int length;
byte[] msg = null;
try {
//从输入流中读取两个字节,作为大端序的整型值解释,表示消息长度
length = in.readUnsignedShort();
} catch (EOFException e) {
return null;
}
//存放从输入流中读取出的消息字节数组
msg = new byte[length];
//readFully多次调用read方法直到读取到指定长度的数组消息或者读取到-1返回
in.readFully(msg);
return msg;
}
}

测试

对两种消息分帧方式进行测试,开启两个线程分别表示client与server,测试消息的发送与接收。

package chapter_3.frame;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets; public class TestFramer { private static final String[] messages = {"Hello World!", "Hello China, 你好 中国", "世界人民大团结万岁",
"在消息中发送分隔符\n和替换符}的情况"}; public static void main(String[] args) throws InterruptedException {
Thread clientThread = new Thread(() -> {
Socket socket = null;
try {
socket = new Socket(InetAddress.getLocalHost(), 8888);
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
//Framer framer = new DelimitFramer(in);
DataInputStream dataInputStream = new DataInputStream(in);
Framer framer = new LengthFramer(dataInputStream);
for (String msg : messages) {
byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);
framer.frameMsg(msgBytes, out);
System.out.println(Thread.currentThread().getName() + " 发送消息: " + msg);
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
});
Thread serverThread = new Thread(() -> {
Socket socket = null;
try (ServerSocket serverSocket = new ServerSocket(8888)) {
while (true) {
socket = serverSocket.accept();
System.out.println("获取到来自" + socket.getRemoteSocketAddress() + "的tcp连接");
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
//Framer framer = new DelimitFramer(in);
DataInputStream dataInputStream = new DataInputStream(in);
Framer framer = new LengthFramer(dataInputStream);
byte[] recvMsgBytes = null;
do {
recvMsgBytes = framer.nextMsg();
//System.out.println(Arrays.toString(recvMsgBytes));
if (recvMsgBytes != null) {
System.out.println(Thread.currentThread().getName() + " 接收到的消息: " + new String(recvMsgBytes, StandardCharsets.UTF_8));
}
} while (recvMsgBytes != null);
}
} catch (IOException e) {
e.printStackTrace();
}
});
serverThread.setName("server");
clientThread.setName("client");
serverThread.start();
Thread.sleep(3000);
clientThread.start();
}
}

输出结果:

获取到来自/10.0.75.1:2462的tcp连接
server 接收到的消息: Hello World!
client 发送消息: Hello World!
client 发送消息: Hello China, 你好 中国
server 接收到的消息: Hello China, 你好 中国
client 发送消息: 世界人民大团结万岁
server 接收到的消息: 世界人民大团结万岁
client 发送消息: 在消息中发送分隔符
和替换符}的情况
server 接收到的消息: 在消息中发送分隔符
和替换符}的情况

在使用TCP协议进行消息发送时,对消息分帧的更多相关文章

  1. RabbitMQ:消息发送确认 与 消息接收确认(ACK)

    默认情况下如果一个 Message 被消费者所正确接收则会被从 Queue 中移除 如果一个 Queue 没被任何消费者订阅,那么这个 Queue 中的消息会被 Cache(缓存),当有消费者订阅时则 ...

  2. rabbitmq消息队列,消息发送失败,消息持久化,消费者处理失败相关

    转:https://blog.csdn.net/u014373554/article/details/92686063 项目是使用springboot项目开发的,前是代码实现,后面有分析发送消息失败. ...

  3. SpringCloud(六) - RabbitMQ安装,三种消息发送模式,消息发送确认,消息消费确认(自动,手动)

    1.安装erlang语言环境 1.1 创建 erlang安装目录 mkdir erlang 1.2 上传解压压缩包 上传到: /root/ 解压缩# tar -zxvf otp_src_22.0.ta ...

  4. 【转】TCP协议的无消息边界问题

    http://www.cnblogs.com/eping/archive/2009/12/12/1622579.html   使用TCP协议编写应用程序时,需要考虑一个问题:TCP协议是无消息边界的, ...

  5. TCP协议学习总结(上)

    在计算机领域,数据的本质无非0和1,创造0和1的固然伟大,但真正百花齐放的还是基于0和1之上的各种层次之间的组合(数据结构)所带给我们人类各种各样的可能性.例如TCP协议,我们的生活无不无时无刻的站在 ...

  6. TCP 协议中MSS的理解

    在介绍MSS之前我们必须要理解下面的几个重要的概念.MTU: Maxitum Transmission Unit 最大传输单元MSS: Maxitum Segment Size 最大分段大小PPPoE ...

  7. TCP/IP详解学习笔记(9)-TCP协议概述

    终于看到了TCP协议,这是TCP/IP详解里面最重要也是最精彩的部分,要花大力气来读.前面的TFTP和BOOTP都是一些简单的协议,就不写笔记了,写起来也没啥东西. TCP和UDP处在同一层---运输 ...

  8. TCP协议,UDP,以及TCP通信服务器的文件传输

    TCP通信过程 下图是一次TCP通讯的时序图.TCP连接建立断开.包含大家熟知的三次握手和四次握手. 在这个例子中,首先客户端主动发起连接.发送请求,然后服务器端响应请求,然后客户端主动关闭连接.两条 ...

  9. TCP协议具体解释(上)

     TCP协议具体解释 3.1 TCP服务的特点 TCP协议相对于UDP协议的特点是面向连接.字节流和可靠传输. 使用TCP协议通信的两方必须先建立链接.然后才干開始数据的读写.两方都必须为该链接分 ...

随机推荐

  1. CodeGen处理Synergy方法目录

    CodeGen处理Synergy方法目录 如果Synergy应用程序开发环境包括使用Synergy/DE xfServerPlus,则可以基于Synergy方法目录中包含的元数据生成代码.要启用此功能 ...

  2. FPGA与ASIC:它们之间的区别以及使用哪一种?

    FPGA与ASIC:它们之间的区别以及使用哪一种? FPGA Vs ASIC: Differences Between Them And Which One To Use? VL82C486 Sing ...

  3. VB 老旧版本维护系列---迷之集合- dataTable

    迷之集合- dataTable '定义一个datatable,并声明一个空对象 Dim data As DataTable = New DataTable() '获取行数 Dim rows As In ...

  4. MySQL笔记01(黑马)

    一.数据库基本介绍 目标:了解数据库的功能和常见数据库分类.数据库产品 数据库基本知识 数据库分类 SQL简介 MySQL访问 1.数据库基本知识 目标:了解数据库的概念和数据库的作用 概念 数据库: ...

  5. 在线CUR转换器

    在线CUR转换器 在线将文件与cur相互免费转换 鼠标光标cur格式可以利用这网站在线免费转换成jpg,png等任意一种格式,方便快速! 转换格式请点击在线CUR转换

  6. VBS脚本编程(4)——流程控制语句

    分支结构--If .. Then .. Else .. 根据表达式的值有条件地执行一组语句. If condition Then statements [Else elsestatements ] 或 ...

  7. token & refresh token 机制总结

    token & refresh token 机制总结 废话 我在项目上写了个配置页面,之前很简单直接登录,毕竟配置页面自己人用就没有做token机制,后来公司的安全审核不过,现在要加上toke ...

  8. SpringBoot数据访问(三) SpringBoot整合Redis

    前言 除了对关系型数据库的整合支持外,SpringBoot对非关系型数据库也提供了非常好的支持,比如,对Redis的支持. Redis(Remote Dictionary Server,即远程字典服务 ...

  9. Unity 按空格一直触发Button点击事件的问题

    #解决 这是由于Button中Navigation(导航)功能导致的. 将导航设置为None即可. 真是气死我了,我说为什么点击完按钮界面,按空格就一直触发界面,难搞

  10. hive学习笔记之七:内置函数

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...