成帧与解析

阅读 《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. 接触追踪解决方案建立在UWB而不是蓝牙上

    接触追踪解决方案建立在UWB而不是蓝牙上 Contact tracing solution builds on UWB rather than Bluetooth 几个月前,当社会距离明显成为对抗CO ...

  2. pycharm在虚拟机跑深度学习Mac

    1.在PyCharm里配置部署环境 打开PyCharmTools > Deployment > Configuration, 新建一个SFTP服务器,名字自己取: 输入如下图配置,注意这里 ...

  3. 【NX二次开发】Block UI 指定矢量

    属性说明 属性   类型   描述   常规           BlockID    String    控件ID    Enable    Logical    是否可操作    Group    ...

  4. ES6中的新数据类型——Symbol

    今天小编和大家来聊一聊es6中新增的一个原始数据类型Symbol.在es5中原始数据类型(基本数据类型)有以下六种:Undefind.Null.Bool. String.Number.Object.今 ...

  5. vue根据变量值绑定src的路径

    路径必须用require包裹起来才会起作用

  6. Mysql慢SQL分析及优化

    为何对慢SQL进行治理 从数据库角度看:每个SQL执行都需要消耗一定I/O资源,SQL执行的快慢,决定资源被占用时间的长短.假设总资源是100,有一条慢SQL占用了30的资源共计1分钟.那么在这1分钟 ...

  7. noip2006 总结

    T1 能量项链 原题 在Mars星球上,每个Mars人都随身佩带着一串能量项链.在项链上有N颗能量珠.能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数.并且,对于相邻的两颗珠子,前一颗珠子 ...

  8. 20201123 《python程序设计》实验四报告

    20201123 2020-2021-2 <python程序设计>实验三报告 课程:<Python程序设计>班级:2011姓名:晏鹏捷学号:20201123实验教师:王志强实验 ...

  9. 一、JavaSE语言基础之关键字与标示符

    1.关键字   所谓关键字指Java中被赋予了特殊含义的单词或字符,Java中常见的关键字共53个,不需要进行记忆,在写代码的过程中会逐渐接触. 2.标示符   标示符,简单来说就是名字:其最大的作用 ...

  10. 8、inotify和resync的优缺点

    只有对外提供访问的服务需要有端口号,本地服务无端口号: 8.1.inotify的优缺点: 1.优点: 监控文件系统事件变化,通过同步工具实现实时的数据同步 2.缺点: 并发如果大于200个文件(10- ...