在使用TCP协议进行消息发送时,对消息分帧
成帧与解析
阅读 《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协议进行消息发送时,对消息分帧的更多相关文章
- RabbitMQ:消息发送确认 与 消息接收确认(ACK)
默认情况下如果一个 Message 被消费者所正确接收则会被从 Queue 中移除 如果一个 Queue 没被任何消费者订阅,那么这个 Queue 中的消息会被 Cache(缓存),当有消费者订阅时则 ...
- rabbitmq消息队列,消息发送失败,消息持久化,消费者处理失败相关
转:https://blog.csdn.net/u014373554/article/details/92686063 项目是使用springboot项目开发的,前是代码实现,后面有分析发送消息失败. ...
- SpringCloud(六) - RabbitMQ安装,三种消息发送模式,消息发送确认,消息消费确认(自动,手动)
1.安装erlang语言环境 1.1 创建 erlang安装目录 mkdir erlang 1.2 上传解压压缩包 上传到: /root/ 解压缩# tar -zxvf otp_src_22.0.ta ...
- 【转】TCP协议的无消息边界问题
http://www.cnblogs.com/eping/archive/2009/12/12/1622579.html 使用TCP协议编写应用程序时,需要考虑一个问题:TCP协议是无消息边界的, ...
- TCP协议学习总结(上)
在计算机领域,数据的本质无非0和1,创造0和1的固然伟大,但真正百花齐放的还是基于0和1之上的各种层次之间的组合(数据结构)所带给我们人类各种各样的可能性.例如TCP协议,我们的生活无不无时无刻的站在 ...
- TCP 协议中MSS的理解
在介绍MSS之前我们必须要理解下面的几个重要的概念.MTU: Maxitum Transmission Unit 最大传输单元MSS: Maxitum Segment Size 最大分段大小PPPoE ...
- TCP/IP详解学习笔记(9)-TCP协议概述
终于看到了TCP协议,这是TCP/IP详解里面最重要也是最精彩的部分,要花大力气来读.前面的TFTP和BOOTP都是一些简单的协议,就不写笔记了,写起来也没啥东西. TCP和UDP处在同一层---运输 ...
- TCP协议,UDP,以及TCP通信服务器的文件传输
TCP通信过程 下图是一次TCP通讯的时序图.TCP连接建立断开.包含大家熟知的三次握手和四次握手. 在这个例子中,首先客户端主动发起连接.发送请求,然后服务器端响应请求,然后客户端主动关闭连接.两条 ...
- TCP协议具体解释(上)
TCP协议具体解释 3.1 TCP服务的特点 TCP协议相对于UDP协议的特点是面向连接.字节流和可靠传输. 使用TCP协议通信的两方必须先建立链接.然后才干開始数据的读写.两方都必须为该链接分 ...
随机推荐
- Django(55)GenericAPIView源码分析
源码分析 GenericAPIView继承自APIView,也就是在APIView基础上再做了一层封装,源码如下: class GenericAPIView(views.APIView): query ...
- Appium_adb常用命令总结
以下为在工作学习过程中总结的常用的adb命令,用来做后续参考和查阅: 一.常用命令 显示当前运行的全部模拟器: adb devices 安装应用程序: adb install 应用程序.apk 备注 ...
- STS或eclipse中导入新项目出现红色感叹号红色叉叉的问题
maven项目 原因: jar包缺失 没有正确配置Maven仓库 解决: Window->Preferences->Maven->Installations->Add 添加你的 ...
- 从实力的角度出发来思考这道AOP题目
文/楠木大叔 技术更迭,一往无前.技术人总是要不断学习以适应社会的发展和行业对我们的要求.每隔一段时间,就会有纷至沓来的新技术,新知识,新概念,我们应该如何应对,是被逼到墙角,还是主动出击? 导读 从 ...
- Task04:集合运算
4.1 表的加减法 4.1.1 什么是集合运算 集合在数学领域表示"各种各样的事物的总和", 在数据库领域表示记录的集合. 具体来说,表.视图和查询的执行结果都是记录的集合, 其中 ...
- kerberos安装配置
目录 前言 服务端安装 组件安装 配置krb5.conf 配置kdc.conf 配置kadm5.acl 创建kdc数据库 在server端创建一个管理员账号,方便远程登录管理kerberos 正式启动 ...
- 【题解】滑雪 luogu1434 记忆化搜索
记忆化搜索入门题 题目 Michael喜欢滑雪.这并不奇怪,因为滑雪的确很刺激.可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你.Michael想知道在 ...
- 小白学k8s(9)-gitlab-runner实现go项目的自动化发布
gitlab构建CI/CD 准备 docker部署gitlab 使用二进制部署gitlab-runner gitlab-runner注册 配置Variables 简单先来个测试 开始构建 遇到的报错 ...
- Tomcat与spring的类加载器案例
Tomcat与spring的类加载器案例接下来将介绍<深入理解java虚拟机>一书中的案例,并解答它所提出的问题.(部分类容来自于书中原文) Tomcat中的类加载器在Tomcat目录结构 ...
- Win32Api -- 使应用Always on top的几种方法
本文介绍几种使应用一直置于顶层的方法. 问题描述 一般情况下,想要将应用置于顶层,设置其TopMost属性为true即可.对于多个设置了TopMost属性的应用,后激活的在上面. 但有的应用,比如全局 ...