java中read粘包怎么解决_Java BIO 的拆包、粘包问题探究

前言

最近参加了"拉勾教育Java高薪训练营",学习到自定义RPC一节,老师讲了各种IO的特性和案例。我自己在写BIO代码中遇到一些问题,在此记录总结一下。

1 最简单的BIO代码

1.1 IOServer

public class IOServer {

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

//首先创建了一个serverSocket

ServerSocket serverSocket = new ServerSocket();

serverSocket.bind(new InetSocketAddress("127.0.0.1",8081));

while (true){

Socket socket = serverSocket.accept(); //同步阻塞

new Thread(()->{

try {

byte[] bytes = new byte[1024];

int len = socket.getInputStream().read(bytes); //同步阻塞

System.out.println(new String(bytes,0,len));

socket.getOutputStream().write(bytes,0,len);

socket.getOutputStream().flush();

} catch (IOException e) {

e.printStackTrace();

}

}).start();

}

}

}

1.2 IOClient

public class IOClient {

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

Socket socket = new Socket("127.0.0.1",8081);

socket.getOutputStream().write("hello".getBytes());

socket.getOutputStream().flush();

System.out.println("server send back data =====");

byte[] bytes = new byte[1024];

int len = socket.getInputStream().read(bytes);

System.out.println(new String(bytes,0,len));

socket.close();

}

}

第一波中使用如下这段代码来获取数据

byte[] bytes = new byte[1024];

int len = socket.getInputStream().read(bytes); //同步阻塞

System.out.println(new String(bytes,0,len)

程序可以正常工作,但是客户端传递的数据超出了1024字节呢?所以修改代码为循环获取数据

2 改为循环处理IO流后的问题

2.1 IOServer

public class IOServer {

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

//首先创建了一个serverSocket

ServerSocket serverSocket = new ServerSocket();

serverSocket.bind(new InetSocketAddress("127.0.0.1", 8081));

while (true) {

Socket socket = serverSocket.accept(); //同步阻塞

new Thread(() -> {

try {

BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

String body = "";

String s = null;

while ((s = reader.readLine()) != null) {

body += s;

}

System.out.println(body);

socket.getOutputStream().write("success".getBytes());

socket.getOutputStream().flush();

} catch (IOException e) {

e.printStackTrace();

}

}).start();

}

}

}

2.2 IOClient

public class IOClient {

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

Socket socket = new Socket("127.0.0.1",8081);

socket.getOutputStream().write("hello".getBytes());

socket.getOutputStream().flush();

System.out.println("server send back data =====");

BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

String body = "";

String s = null;

while ((s = reader.readLine()) != null) {

body += s;

}

socket.close();

}

}

修改后主要使用如下代码接收数据

BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

String body = "";

String s = null;

while ((s = reader.readLine()) != null) {

body += s;

}

测试时会发现程序执行不符合预期,完全没有数据!

87311dc9ec8d0faa5b6a1a6accf6ac5d.png

通过官方提供的jconsole工具分别分析service进程和client进程,接下来就是本文精华了^_^

2.3 服务端分析

服务端启动监听

Socket socket = serverSocket.accept(); //同步阻塞

当接收到客户端的连接时,会启动子线程来处理这个请求

while (true) {

Socket socket = serverSocket.accept(); //同步阻塞

new Thread(() -> {

try {

BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

通过jconsole可以看到main线程阻塞在了accept()方法上

0e3c48b295b6e9a98226b1f28c6ec21b.png

处理任务的子线程阻塞在read()方法上

36847951dc131f74380de6f3ee4913e6.png

说明循环读取数据这里出了问题,一直在这里读取数据,不知道什么时候是个头。我们知道IO流的特点是连续不断的,客户端也没有明确的说明数据传完了

2.4 客户端分析

客户端发送完数据,就会等待服务器返回数据

socket.getOutputStream().write("hello".getBytes());

socket.getOutputStream().flush();

System.out.println("server send back data =====");

BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

String body = "";

String s = null;

while ((s = reader.readLine()) != null) {

body += s;

}

同样的来看Jconsole

d9b5487cb273c37ffaace2e09c5e113c.png

可以看出,客户端阻塞在了read()方法上,因为服务端一直 没有返回,客户端也只能一直等着

问题的原因这时候就很清晰了,JDK提供给我们的流API是比较底层的,它的特点是连续不断的!当我们循环读取流的时候,只要连接不断开,就可以一直读取数据,就会阻塞在read()方法

所以我们必须明确指定数据什么时候接收完成!

在NIO中,我们通常会自定义协议来处理这个问题,这里也是一样,我们规定把数据分成两个部分

数据头:存储数据的长度(int型,4个byte)

数据体:存储数据(byte数组)

当然这个协议根据情况是可以变化的,如在增加一个消息的格式,指定消息是json还是xml。

下面我们就来实现吧

3 定义格式处理拆包、粘包问题

3.1 IOServer

public class IOServer {

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

//首先创建了一个serverSocket

ServerSocket serverSocket = new ServerSocket();

serverSocket.bind(new InetSocketAddress("127.0.0.1", 8081));

while (true) {

Socket socket = serverSocket.accept(); //同步阻塞

new Thread(() -> {

try {

// 解析数据头

byte[] lengthb = new byte[4];

InputStream in = socket.getInputStream();

in.read(lengthb);

Integer length = ConvertUtils.byteArrayToInt(lengthb);

// 解析数据体

byte[] b = new byte[1024];

String result = "";

while (length > 0) {

int readLength;

if (length > 1024) {

readLength = in.read(b);

} else {

readLength = in.read(b, 0, length);

}

length -= readLength;

result += new String(b, 0, readLength);

}

System.out.println("接收到客户端的数据:" + result);

String content = "success";

socket.getOutputStream().write(ConvertUtils.intToByteArray(content.getBytes().length));

socket.getOutputStream().write(content.getBytes());

socket.getOutputStream().flush();

} catch (IOException e) {

e.printStackTrace();

}

}).start();

}

}

}

3.2 IOClient

public class IOClient {

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

while (true){

try {

Thread.sleep(2000);

Socket socket = new Socket("127.0.0.1", 8081);

String content = "hello";

socket.getOutputStream().write(ConvertUtils.intToByteArray(content.getBytes().length));

socket.getOutputStream().write(content.getBytes());

socket.getOutputStream().flush();

// 解析数据头

byte[] lengthb = new byte[4];

InputStream in = socket.getInputStream();

in.read(lengthb);

Integer length = ConvertUtils.byteArrayToInt(lengthb);

// 解析数据体

byte[] b = new byte[1024];

String result = "";

while (length > 0) {

int readLength;

if (length > 1024) {

readLength = in.read(b);

} else {

readLength = in.read(b, 0, length);

}

length -= readLength;

result += new String(b, 0, readLength);

}

System.out.println("接收到客户端的数据:" + result);

socket.close();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

程序中用到了一个int和byte转换的工具类

3.3 ConvertUtils

public class ConvertUtils {

/**

* byte[]转int

* @param bytes

* @return

*/

public static int byteArrayToInt(byte[] bytes) {

int value = 0;

// 由高位到低位

for (int i = 0; i < 4; i++) {

int shift = (4 - 1 - i) * 8;

value += (bytes[i] & 0x000000FF) << shift;// 往高位游

}

return value;

}

/**

* int到byte[]

* @param i

* @return

*/

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;

}

public static void main(String[] args) {

byte[] b = intToByteArray("hello".getBytes().length);

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

int i = byteArrayToInt(b);

System.out.println(i);

}

}

大功告成

35f1a55f39abe4eb656dc2956f16c9a5.png

4 总结

此前从未考虑过BIO拆包、粘包的问题,在自定义RPC框架中,使用Netty进行网络通信时,是通过自定义协议对象来解决拆包、粘包问题的,类比到BIO中,同样的思路来处理,因为IO流的特点是连续不断的。

博客都是自己的一些思考,不能保证内容完全正确,欢迎探讨

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值