成帧与解析

阅读 《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. 35 张图带你 MySQL 调优

    这是 MySQL 基础系列的第四篇文章,之前的三篇文章见如下链接 138 张图带你 MySQL 入门 47 张图带你 MySQL 进阶!!! 炸裂!MySQL 82 张图带你飞 一般传统互联网公司很少 ...

  2. 最全JVM知识点思维导图,看这一篇就够了

    此处是转发别人的,别人花了二个月, 我花一天时间看完, 觉得很有用 https://www.processon.com/view/link/5eea141cf346fb1ae56a44e7

  3. 1738. 找出第 K 大的异或坐标值

    2021-05-19 LeetCode每日一题 链接:https://leetcode-cn.com/problems/find-kth-largest-xor-coordinate-value/ 标 ...

  4. 『居善地』接口测试 — 13、Moco框架的使用

    目录 1.Moco框架第一个练习 2.Get方法的Mock实现 3.Post方法的Mock实现 4.请求中加入Cookies 5.请求中加入Header 6.Moco模拟重定向 7.综合练习 8.总结 ...

  5. PTA题目集7-9总结

    PTA题目集7-9总结 一.前言 题目集七:该题集为轮到图形卡片排序游戏题,主要考查的知识点有类的继承,ArrayList泛型的使用,Compabale接口的运用,多态的使用方法以及接口的应用,难度较 ...

  6. 1.5w字 + 24张图肝翻 TCP。

    TCP 是一种面向连接的单播协议,在 TCP 中,并不存在多播.广播的这种行为,因为 TCP 报文段中能明确发送方和接受方的 IP 地址. 在发送数据前,相互通信的双方(即发送方和接受方)需要建立一条 ...

  7. SpringCloud Alibaba实战(6:nacos-server服务搭建)

    源码地址:https://gitee.com/fighter3/eshop-project.git 持续更新中-- 大家好,我是三分恶. 这一节我们来学习SpringCloud Alibaba体系中一 ...

  8. 从 Nginx 优秀的核心架构设计,揭秘其为何能支持高并发?

    目录: 1. Nginx的整体架构 2. Nginx的模块化设计 3. Nginx的请求方式处理 4. Nginx事件驱动模型 5. Nginx进程处理模型 写在前面 Nginx 是一个 免费的,开源 ...

  9. 【模板】关于vector的lower_bound和upper_bound以及vector基本用法 STL

    关于lower_bound和upper_bound 共同点 函数组成: 一个数组元素的地址(或者数组名来表示这个数组的首地址,用来表示这个数组的开头比较的元素的地址,不一定要是首地址,只是用于比较的& ...

  10. csp-s模拟测试「9.14」A·B·C(三分,贪心)

    博客大概咕了很久了......... T1 A 大概推下式子就好了,考试时数据点分治DFS前30点T了,然后后70分因为两数相乘爆long long然后本来可以A掉,就WA零了....... 式子推出 ...