基于 TCP 协议的自定义数据帧的形式来进行长数据(图片)的传输

通过上一个博客,我们可以正确地在实现客户端正常连接服务器端之后,客户端可以通过向服务器端发送具体指令来获得服务器端的相应数据。然而, 事实上,网络的通讯还包括图像的通讯,同时还需要考虑各种异常情况,这其中包括程序的自动断线重连、大数据的粘包过程等。

与上一个博客类似,我先规定指令所代表的含义如下:

| 指令 | 含义 |

| — | — |

| 1818 | 请求获取实验室目前的图像 |

| 1819 | 请求实验室人员的数量 |

| 1820 | 请求服务器中实验室人员的平均补贴 |

2 可能出现的异常

============================================================================

2.1 Socket 网络通讯中的粘包、半包


TCP通讯为何存在粘包呢?主要原因是: TCP 是以流的方式来处理数据,再加上网络上 MTU 的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。

处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。

2.1.1 解决途径

  1. 消息定长:报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分;

  2. 包尾添加特殊分隔符:例如每条报文结束都添加回车换行符(例如 FTP 协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分;

  3. 将消息分为消息头和消息体:消息头中包含表示信息的总长度(或者消息体长度)的字段;

2.1.2 自定义数据包协议


上一个博客中,我通过使用将消息分为消息头和消息体来解决粘包问题,那么现在,我将使用在包尾添加特殊分隔符来解决粘包问题。

首先根据指令所代表的含义,定义好 Socket 的数据包协议如下:

在这里插入图片描述

详细关于解决 TCP 粘包、半包的问题可以查阅这篇博客。【传送门

2.1.3 代码实现

通过逐位判断当前接收到的字节数据是否为结束符来收发数据。

// 连接服务器

Socket socket = new Socket(“127.0.0.1”, 5612);

// Java 中用于接收数据的流

InputStream in = socket.getInputStream();

// 该字节数组用来存放 Socket 的 IO 流中的数据

byte[] buf = new byte[1024];

int len = 0;

// 因为数组的不可变性,所以需要一个 Java 集合用来临时存放数据

ArrayList arr = new ArrayList();

flag:

// 先将 IO 流中的数据灌满整个字节数组,然后把 IO 流中的一个协议数据保存到 Java 集合中

while ((len = in.read(buf)) != -1) {

// 逐字读取 IO 中的数据

for (byte temp : buf) {

// 判断结束符

if (temp != ‘\n’) {

arr.add(temp);

} else {

break flag;

}

}

//本来可以直接用 JVM 的垃圾自动回收机制,不过 Socket 的 read 方法必须不为 null,所以就用 Arrays 的方法

Arrays.fill(buf, (byte) 0);

}

// 将 Java 集合中的数据保存到一个新的数组中

byte[] bs = new byte[arr.size()];

for (int j = 0; j < arr.size(); j++) {

bs[j] = arr.get(j);

}

arr.clear();

// 接下来可以将该字节数据转化为 String 字符串进行操作

String bsStr = new String(bs, StandardCharsets.UTF_8);

2.2 图像转换成字节数组发送时所需要进行的转换


图像通过按照如下方式加入到 Socket 的数据包协议中,然后发送并解析,正因为这一步,所以后面我们还需要导入一个 jar 包。

在这里插入图片描述

2.2.1 代码实现

// 通过 Java 的文件流得到的图像数组,Utils 为我自己写的方法

byte[] fileArr1818 = Utils.fileToByteArray(“C:\Users\Sharm\Desktop\boke\src\main\resources\实验室图像.jpg”);

assert fileArr1818 != null;

System.out.println(“数据正在编码中”);

// 将字节数组利用 Base64 解码为字符串,然后根据数据包协议进行拼接

imgStrs = Base64.encodeBase64String(fileArr1818);

sendStrs = head + insStr + imgStrs + end + terminator;

// 利用 UTF-8 编码为字节数组,用于 Socket 的传输

sendBytes = sendStrs.getBytes(StandardCharsets.UTF_8);

3 Maven

==========================================================================

因为代码中引入了一个新的 jar 包,即import org.apache.commons.codec.binary.Base64; ,所以利用 Maven 来构建该项目是极好的,如果不使用 Maven 的话,直接在该项目中导入这个 jar 包就行。

如果使用 Maven,只需在 pom.xml 中加入以下依赖,即可。

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<maven.compiler.source>11</maven.compiler.source>

<maven.compiler.target>11</maven.compiler.target>

org.apache.directory.studio

org.apache.commons.codec

1.8

不要以为不会 Maven 就放弃这个步骤,相信我,用过 Maven 之后,你就再也不想四处找 jar 包了!

Maven 不难,现阶段就把它当成是一个自动安装 jar 包的工具。有不了解的可以阅读一下这个博客。【传送门

4 项目流程图

==========================================================================

依据需求,通过画出对应的流程图来理清自己的思路,然后根据流程图写出相应代码。

在这里插入图片描述

5 代码实现

=========================================================================

5.1 客户端代码


import org.apache.commons.codec.binary.Base64;

import java.io.*;

import java.net.Socket;

import java.nio.charset.StandardCharsets;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Scanner;

public class Client {

public static void main(String[] args) throws IOException {

int countConnect = 0;

while (true) {

try {

//连接服务器

Socket s = new Socket(“127.0.0.1”, 5612);

System.out.println(“已连接到服务器5612端口,准备向服务器发送指令。”);

//获取输出流

OutputStream out = s.getOutputStream();

while (true) {

//服务端开始输入指令

System.out.println(“请输入指令:”);

System.out.println(“1818:请求获取实验室目前的图像”);

System.out.println(“1819:请求实验室人员的数量”);

System.out.println(“1820:请求实验室人员的平均补贴”);

Scanner scanner = new Scanner(System.in);

int instruction = scanner.nextInt();

//将int型指令转化为byte[]

byte[] instructionArr = intToByteArray(instruction);

// 利用自定义数据帧的形式发送

String instructionStrs = Base64.encodeBase64String(instructionArr);

String terminator = “\r\n”;

String sendStrs = “Req0R” + instructionStrs + “End” + terminator;

//一个字符为一个字节,如“Req”为三个字节

byte[] sendBytes = sendStrs.getBytes(StandardCharsets.UTF_8);

// 开始在输出流中发送数据

out.write(sendBytes, 0, sendBytes.length);

//获取开始时间

long startTime = System.currentTimeMillis();

上面是发送,下面是接收

// 为了解决粘包问题所设置的接收代码

InputStream in = s.getInputStream();

byte[] buf = new byte[1024];

int len = 0;

ArrayList arr = new ArrayList();

flag:

while ((len = in.read(buf)) != -1) {

for (byte temp : buf) {

if (temp != ‘\n’) {

arr.add(temp);

} else {

break flag;

}

}

//原来两台电脑间通讯遇到的问题可以通过清空

Arrays.fill(buf, (byte) 0);

}

byte[] bs = new byte[arr.size()];

for (int j = 0; j < arr.size(); j++) {

bs[j] = arr.get(j);

}

arr.clear();

// bsStr 就是得到的最终字符串数据

String bsStr = new String(bs, StandardCharsets.UTF_8);

//得到目标文件

//这个可以按步解析出对应的指令

String head = bsStr.substring(0, 4);

String insStr = bsStr.substring(4, 12);

String resqStr = bsStr.substring(12, bsStr.length() - 4);

String end = bsStr.substring(bsStr.length() - 4, bsStr.length() - 1);

byte[] receInsBytes = Base64.decodeBase64(insStr);

byte[] receBytes = Base64.decodeBase64(resqStr);

// 按照不同的指令,进行不同的操作

int receInsInt = byteArrayToInt(receInsBytes);

switch (receInsInt) {

case 1818:

//对于图像,原来缺少几个字节真的不会影响整体

File destFile = new File(“lab.jpg”);

//下面这段代码呢,就是先将字节数组的数据放到数组流中,然后再传给节点流,参考自己的节点流复制代码

//字节数组与程序之间的管道。把数组写到流中

InputStream bais = new ByteArrayInputStream(receBytes);

//程序与新文件之间的管道

OutputStream fos = new FileOutputStream(destFile);

//一样的字节数组缓冲操作

byte[] buf2 = new byte[1024];

int len2;

while ((len2 = bais.read(buf2)) != -1) {

fos.write(buf2, 0, len2);

}

System.out.println(“图片保存完毕。”);

//获取结束时间

long endTime = System.currentTimeMillis();

System.out.printf(“实验室图片传输过来花了%d ms时间:”, (endTime - startTime) / 2);

//流的关闭

bais.close();

fos.close();

break;

case 1819:

int speed = byteArrayToInt(receBytes);

System.out.printf(“此时,实验室人员的数量有 %d 人\n”, speed);

break;

case 1820:

int volume = byteArrayToInt(receBytes);

System.out.printf(“实验室人员的平均补贴为 %d 元\n”, volume);

break;

default:

break;

}

}

} catch (IOException ex) {

System.out.printf(“服务器断开,请求重连,正在进行第%d次连接。”, countConnect);

System.out.println();

countConnect += 1;

}

}

}

/**

  • int到byte[]

  • @param i 输入待转换的int

  • @return 返回的对应的byte[]

*/

public static byte[] intToByteArray(int i) {

byte[] result = new byte[4];

//由高位到低位

result[0] = (byte)((i >> 24) & 0xFF);

result[1] = (byte)((i >> 16) & 0xFF);

result[2] = (byte)((i >> 8) & 0xFF);

result[3] = (byte)(i & 0xFF);

return result;

}

/**

  • byte[]转int

  • @param bytes 指定的byte[]

  • @return int型的值

*/

public static int byteArrayToInt(byte[] bytes) {

return (bytes[3] & 0xFF) |

(bytes[2] & 0xFF) << 8 |

(bytes[1] & 0xFF) << 16 |

(bytes[0] & 0xFF) << 24;

}

}

5.2 服务器端代码


import org.apache.commons.codec.binary.Base64;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.net.ServerSocket;

import java.net.Socket;

import java.nio.charset.StandardCharsets;

import java.util.ArrayList;

import java.util.Arrays;

public class Server {

public static void main(String[] args) throws IOException {

//服务器开始监听5612端口

ServerSocket serverSocket = new ServerSocket(5612);

System.out.println(“服务端已启动,正在监听5612端口”);

//等待客户端连接

Socket s = serverSocket.accept();

System.out.println(“得到来自一个客户端的连接”);

//接收服务器的消息

InputStream in = s.getInputStream();

while (true) {

//一样的字节数组缓冲操作,将IO流中的数据放到buf数组中

byte[] buf = new byte[1024];

int len = 0;

ArrayList arr = new ArrayList();

flag:

while ((len = in.read(buf)) != -1) {

for (byte temp : buf) {

if (temp != ‘\n’) {

arr.add(temp);

} else {

break flag;

}

}

//本来可以直接用JVM的垃圾自动回收机制,不过socket的read方法必须不为null,所以就用Arrays的方法

Arrays.fill(buf, (byte) 0);

}

byte[] bs = new byte[arr.size()];

for (int j = 0; j < arr.size(); j++) {

bs[j] = arr.get(j);

}

arr.clear();

String bsStr = new String(bs, StandardCharsets.UTF_8);

//原来是要在String中截取出字串,而不能在下面这个字节数组中拿出子数组来得到instruction1818。

//截取服务器请求。协议:

String insStr = bsStr.substring(5, bsStr.length()-4);

String head = “Resp”;

String end = “End”;

byte[] insBytes = Base64.decodeBase64(insStr);

System.out.println(Arrays.toString(insBytes));

//得到的是服务器的指令

int ins = byteArrayToInt(insBytes);

System.out.println(“服务器发过来的指令是:” + ins);

String imgStrs = null;

String sendStrs = null;

byte[] sendBytes = null;

String terminator = “\r\n”;

switch (ins) {

case 1818:

System.out.println(“正在打包图像”);

// 下面的图像的路径我写的是该图像在我电脑中的绝对路径

byte[] fileArr1818 = Utils.fileToByteArray(“C:\Users\Sharm\Desktop\boke\src\main\resources\实验室图像.jpg”);

assert fileArr1818 != null;

System.out.println(“数据正在编码中”);

imgStrs = Base64.encodeBase64String(fileArr1818);

sendStrs = head + insStr + imgStrs + end + terminator;

sendBytes = sendStrs.getBytes(StandardCharsets.UTF_8);

break;

case 1819:

byte[] fileArr1819 = intToByteArray(18);

System.out.println(“数据正在编码中……”);

imgStrs = Base64.encodeBase64String(fileArr1819);

sendStrs = head + insStr + imgStrs + end + terminator;

sendBytes = sendStrs.getBytes(StandardCharsets.UTF_8);

break;

case 1820:

byte[] fileArr1820 = intToByteArray(400);

System.out.println(“数据正在编码中……”);

imgStrs = Base64.encodeBase64String(fileArr1820);

sendStrs = head + insStr + imgStrs + end + terminator;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

文章中涉及到的知识点我都已经整理成了资料,录制了视频供大家下载学习,诚意满满,希望可以帮助在这个行业发展的朋友,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我把这些资料,分享出来。相信对于已经工作和遇到技术瓶颈的朋友们,在这份资料中一定都有你需要的内容。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-cQ15TdZz-1713606319342)]

[外链图片转存中…(img-Y66lNIoz-1713606319343)]

[外链图片转存中…(img-YfVhowfn-1713606319344)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

文章中涉及到的知识点我都已经整理成了资料,录制了视频供大家下载学习,诚意满满,希望可以帮助在这个行业发展的朋友,在论坛博客等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我把这些资料,分享出来。相信对于已经工作和遇到技术瓶颈的朋友们,在这份资料中一定都有你需要的内容。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 19
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值