在使用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协议通信的两方必须先建立链接.然后才干開始数据的读写.两方都必须为该链接分 ...
随机推荐
- 基于区域的CNN(R-CNN)
基于区域的CNN(R-CNN) Region-based CNNs (R-CNNs) 基于区域的卷积神经网络或具有CNN特征的区域(R-CNN)是一种将深度模型应用于目标检测的开创性方法.在本节中,将 ...
- 【NX二次开发】获取相邻面UF_MODL_ask_adjac_faces
获取箭头指示的面的相邻面 源码: 1 extern DllExport void ufsta(char *param, int *returnCode, int rlen) 2 { 3 UF_init ...
- 【NX二次开发】分析曲线某位置的信息 UF_MODL_ask_curve_props
分析曲线某位置的信息:点.切线.主副法线.半径等 extern DllExport void ufsta(char *param, int *returnCode, int rlen) { UF_in ...
- Kubernetes集群安装
一.环境介绍 主机名 IP地址 master 192.168.0.100 node1 192.168.0.101 node2 192.168.0.102 1.1.操作系统: CensOS8.4.210 ...
- windows 上 OpenSSH 服务 启用秘钥登录(微软真心逆天)
windows 上 OpenSSH 服务 启用秘钥登录(微软真心逆天) windows 安装 OpenSSH 服务 最近需要在windows 服务器上部署自动发布程序,那么就需要用到 scp 和 ss ...
- Centos7一键配置阿里云yum源脚本
Centos7一键配置阿里云yum源脚本 工作中linux系统经常要配置网络yum,故写了一个简单的配置阿里云yum源的的脚本可以单独使用也可以在做自动化部署的时候调用. #!/bin/bash # ...
- 重新整理 .net core 实践篇————polly失败重试[三十四]
前言 简单整理一下polly 重试. 正文 在开发程序中一般都有一个重试帮助类,那么polly同样有这个功能. polly 组件包: polly 功能包 polly.Extensions.Http 专 ...
- CosId 1.1.0 发布,通用、灵活、高性能的分布式 ID 生成器
CosId 通用.灵活.高性能的分布式 ID 生成器 介绍 CosId 旨在提供通用.灵活.高性能的分布式系统 ID 生成器. 目前提供了俩大类 ID 生成器:SnowflakeId (单机 TPS ...
- excel VBA数组运用
Sub a()Dim i人数 As Integer'定义变量Dim i考试成绩() As Integer'定义数组Dim i As Integer'定义变量i人数 = InputBox("输 ...
- 8.QSharedPointer
QSharedPointer 是一个共享指针, 同时是引用计数型的智能指针 ,也就是说,QSharedPointer可以被自由地拷贝和赋值,在任意的地方共享它. QSharedPointer内部会对拥 ...