最近在整理Java IO相关内容,会遇到一些以前没有注意的问题,特此记录,以供自查和交流。
需求:
基于Java的BIO API,实现简单的客户端和服务端通信模型,客户端使用BufferedReader的readLine方法读取System.in上的用户输入,然后通过字节输出流发送给服务端,服务端使用BufferedReader的readLine方法读取客户端的数据,进行打印;
问题:
服务端没有打印出客户端发送的数据,且卡在BufferedReader的readLine方法处
上代码:
客户端:
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 基于BIO的TCP网络通信的客户端,接收控制台输入的数据,然后通过字节流发送给服务端
*
*/
class ChatClient {
public static void main(String[] args) throws IOException {
// 连接server
Socket serverSocket = new Socket("localhost", 9999);
System.out.println("client connected to server");
// 读取用户在控制台上的输入,并发送给服务器
InputStream in = System.in;
// sendDataToServerByByteStream(in, serverSocket.getOutputStream()); //服务端可以正常接收
sendDataToServerByCharStream(in, serverSocket.getOutputStream()); //服务端无法正常接收
}
/**
* 通过字节流发送数据给服务端
*/
private static void sendDataToServerByByteStream(InputStream in, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[1024];
int len;
// read操作阻塞,直到有数据可读
while ((len = in.read(buffer)) != -1) {
System.out.println("client receive data from console" + in + " : " + new String(buffer, 0, len));
// 发送数据给服务器端
outputStream.write(new String(buffer, 0, len).getBytes()); // 此时buffer中是有换行符的
}
}
/**
* 通过字符流,使用readLine发送数据给服务端
*/
private static void sendDataToServerByCharStream(InputStream in, OutputStream outputStream) {
String content = null;
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
content = bufferedReader.readLine();
while (content != "exit") {
System.out.println("client send data: " + content);
outputStream.write(content.getBytes()); // 字节流,没有添加换行符
content = bufferedReader.readLine();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
服务端代码:
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 基于BIO的TCP网络通信的服务端,可以接收多个客户端连接,通过字节流接收客户端发送的消息;
* 一个客户端需要使用一个线程
* todo:线程资源复用
*
* @author freddy
*/
class ChatServer {
public static void main(String[] args) throws IOException {
// 开启server 监听端口
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
Socket client = serverSocket.accept(); // 阻塞操作,需要新的线程处理客户端
// 接收Client数据,并转发
new Thread(new ServerThread(client)).start();
}
}
}
/**
* 服务端的线程,一个客户端对应一个
*/
class ServerThread implements Runnable {
Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
System.out.println("server had a client" + socket);
// 获取输入流程,读取用户输入
// 持续接收Client数据,并打印
readDataFromClientByCharStream(); // 无法正常读取客户端发送过来的数据
}
/**
* 使用字符流读取客户端的数据,主要使用readLine
*/
private void readDataFromClientByCharStream() {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
String content = bufferedReader.readLine();
while (content != "exit") {
System.out.println("serer receive data from " + socket + " : " + content);
content = bufferedReader.readLine();
}
} catch (IOException e) {
System.out.println("Client disconnect ");
}
}
}
先运行服务端,在启动客户端,然后在客户端的控制台发送数据:
可以看到,客户端和服务端之间已经建立了连接,但是服务端并没有打印日志,说明服务端的程序卡在了代码1这个地方。
为什么呢?
那我们需要去看java.io.BufferedReader#readLine()这个方法的源码:
基于debug方式,我们可以看到java.io.BufferedReader#readLine()这个方法
先调用java.io.BufferedReader#fill方法读取输入流的内容
可以看到,这里读取到的内容是hello 5个字符,没有换行符;
fill方法调用完后,回到readLine方法的charLoop中:
可以看到,for循环中有个条件,当读取到的字节中包含'\n' 或者 '\r'的时候,会设置eol = true,后面会根据该eol标志,return读取到的字符串,结束readLine方法;
当读取到的字节中没有'\n' 或者 '\r'的时候,eol = false,readLine方法就会回到
bufferLoop循环中的fill方法继续读取输入流程中的内容:
如果输入流中有内容,会读取后继续判断是否有换行符:'\n' 或者 '\r'
如果输入流中没有内容,那么fill方法会阻塞在java.io.Reader#read(char[], int, int)方法:
这就是服务端的代码阻塞在java.io.BufferedReader#readLine()的原因;
解决问题:
找到问题后,那么我们就好解决问题了:
解决思路如下:
1.服务端仍然使用java.io.BufferedReader#readLine()读取客户端的数据的话,那么客户端发送数据时,就必须代换行符
1.1 客户端在发送完用户数据后,继续Socket.getOutputStream().write("\r\n".getBytes());发送换行符;
1.2 调用增强的输出流的api,直接发送数据的同时发送换行符:
比如:PrintWriter pw = new PrintWriter(outputStream, true);
pw.println(content); // 添加换行符
1.3 调整客户端获取用户输入数据的方式,把用户的换行符直接读取过来后,用原来的方式发送
private static void sendDataToServerByByteStream(InputStream in, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[1024];
int len;
// read操作阻塞,直到有数据可读
while ((len = in.read(buffer)) != -1) {
System.out.println("client receive data from console" + in + " : " + new String(buffer, 0, len));
// 发送数据给服务器端
outputStream.write(new String(buffer, 0, len).getBytes()); // 此时buffer中是有换行符的
}
}
2.服务端调整数据读取方式
客户端使用java.io.DataOutputStream#writeUTF(java.lang.String)发送给数据
服务端使用java.io.DataInputStream#readUTF()方法接收数据
这种方式,是相当于客户端在发送数据的时候,给数据规定了格式,服务端可以根据约定的格式,来正确读取数据;类似于java.io.DataOutputStream#writeShort方法
关于这种思想,用的地方很多
常用来解决RPC发送数据的粘包问题
在常用的RPC框架,如Netty中就有使用;在大数据框架如MapReduce中也有writeShort类似方式序列号和反序列话;
完整的客户端和服务端验证代码如下:
客户端:
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 基于BIO的TCP网络通信的客户端,接收控制台输入的数据,然后通过字节流发送给服务端
*
*/
class ChatClient {
public static void main(String[] args) throws IOException {
// 连接server
Socket serverSocket = new Socket("localhost", 9999);
System.out.println("client connected to server");
// 读取用户在控制台上的输入,并发送给服务器
InputStream in = System.in;
// sendDataToServerByByteStream(in, serverSocket.getOutputStream()); //服务端可以正常接收
sendDataToServerByCharStream(in, serverSocket.getOutputStream()); // 服务端无法正常接收
}
/**
* 通过字节流发送数据给服务端
*/
private static void sendDataToServerByByteStream(InputStream in, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[1024];
int len;
// read操作阻塞,直到有数据可读
while ((len = in.read(buffer)) != -1) {
System.out.println("client receive data from console" + in + " : " + new String(buffer, 0, len));
// 发送数据给服务器端
outputStream.write(new String(buffer, 0, len).getBytes()); // 此时buffer中是有换行符的
}
}
/**
* 通过字符流,使用readLine发送数据给服务端
*/
private static void sendDataToServerByCharStream(InputStream in, OutputStream outputStream) {
String content = null;
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
content = bufferedReader.readLine();
while (content != "exit") {
System.out.println("client send data: " + content);
outputStream.write(content.getBytes()); // 字节流,没有添加换行符
content = bufferedReader.readLine();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 通过字符流,使用readLine发送数据给服务端
*/
private static void sendDataToServerByCharStream2(InputStream in, OutputStream outputStream) {
String content = null;
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
content = bufferedReader.readLine();
while (content != "exit") {
System.out.println("client send data: " + content);
outputStream.write(content.getBytes()); // 字节流,没有添加换行符
outputStream.write("\r\n".getBytes());
content = bufferedReader.readLine();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 通过字符流,使用readLine发送数据给服务端
*/
private static void sendDataToServerByCharStream3(InputStream in, OutputStream outputStream) {
String content = null;
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
PrintWriter pw = new PrintWriter(outputStream, true);) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
content = bufferedReader.readLine();
while (content != "exit") {
System.out.println("client send data: " + content);
pw.println(content); // 添加换行符
content = bufferedReader.readLine();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void sendDataToServerByCharStream4(InputStream in, OutputStream outputStream) {
String content = null;
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
DataOutputStream pw = new DataOutputStream(outputStream);) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
content = bufferedReader.readLine();
while (content != "exit") {
System.out.println("client send data: " + content);
pw.writeUTF(content);
pw.flush();
content = bufferedReader.readLine();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
服务端:
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 基于BIO的TCP网络通信的服务端,可以接收多个客户端连接,通过字节流接收客户端发送的消息;
* 一个客户端需要使用一个线程
* todo:线程资源复用
*
* @author freddy
*/
class ChatServer {
public static void main(String[] args) throws IOException {
// 开启server 监听端口
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
Socket client = serverSocket.accept(); // 阻塞操作,需要新的线程处理客户端
// 接收Client数据,并转发
new Thread(new ServerThread(client)).start();
}
}
}
/**
* 服务端的线程,一个客户端对应一个
*/
class ServerThread implements Runnable {
Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
System.out.println("server had a client" + socket);
// 获取输入流程,读取用户输入
// 持续接收Client数据,并打印
readDataFromClientByCharStream(); // 无法正常读取客户端发送过来的数据
}
/**
* 使用字节流读取客户端的数据
*/
private void readDataFromClientByByteStream() {
try (InputStream inputStream = socket.getInputStream()) {
byte[] buffer = new byte[1024];
int len;
// read操作阻塞,直到有数据可读
while ((len = inputStream.read(buffer)) != -1) {
System.out.println("serer receive data from " + socket + " : " + new String(buffer, 0, len));
}
} catch (IOException e) {
System.out.println(socket + " disconnect ");
}
}
/**
* 使用字符流读取客户端的数据,主要使用readLine
*/
private void readDataFromClientByCharStream() {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
String content = bufferedReader.readLine();
while (content != "exit") {
System.out.println("serer receive data from " + socket + " : " + content);
content = bufferedReader.readLine();
}
} catch (IOException e) {
System.out.println("Client disconnect ");
}
}
/**
* 使用字符流读取客户端的数据,主要使用readLine
*/
private void readDataFromClientByCharStream2() {
try (DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());) {
// 此时content中已经没有换行符了,
// 并且如果原始字节流中没有换行符,该方法内部会循环等待换行符,相当于阻塞在这里
String content = dataInputStream.readUTF();
while (content != "exit") {
System.out.println("serer receive data from " + socket + " : " + content);
content = dataInputStream.readUTF();
}
} catch (IOException e) {
System.out.println("Client disconnect ");
}
}
}
参考:java网络编程 BufferedReader的readLine方法读不到数据的原因_java后台服务端bufferedreader不能读全数据 前台出现超时提示-CSDN博客