前言
最近参加了"拉勾教育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;
}
测试时会发现程序执行不符合预期,完全没有数据!
通过官方提供的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()方法上
处理任务的子线程阻塞在read()方法上
说明循环读取数据这里出了问题,一直在这里读取数据,不知道什么时候是个头。我们知道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
可以看出,客户端阻塞在了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);
}
}
大功告成
4 总结
此前从未考虑过BIO拆包、粘包的问题,在自定义RPC框架中,使用Netty进行网络通信时,是通过自定义协议对象来解决拆包、粘包问题的,类比到BIO中,同样的思路来处理,因为IO流的特点是连续不断的。
博客都是自己的一些思考,不能保证内容完全正确,欢迎探讨