想实现一个功能,客户端发送一个消息,服务器接收到消息后回复客户端。
写好后发现并没有像预期的那样运作,一运行就阻塞,经过一番排查发现了问题。
问题分析
原来的代码如下:
服务端:
public class ServerProDemo {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8081);
Socket socket = serverSocket.accept();
handleSocket(socket);
}
private static void handleSocket(Socket socket) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
while (true) {
byte[] buf = new byte[2048];
inputStream.read(buf);
String res = new String(buf, StandardCharsets.UTF_8);
System.out.println(res);
if ("exit".equals(res)) {
break;
}
//回写信息
res = "服务器收到你发来的:" + res;
outputStream.write(res.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
NetUtils.closeConnection(outputStream, inputStream);
socket.close();
}
}
客户端:
public class ClientDemo {
public static void main(String[] args) throws IOException {
Socket socket = new Socket(InetAddress.getLocalHost(), 8081);
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
while (true) {
String message = geInputMessage();
//给服务器发送消息
outputStream.write(message.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
if("exit".equals(message)) {
break;
}
//接收服务器回写的信息
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, len);
}
System.out.println(new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8));
}
NetUtils.closeConnection(outputStream, inputStream);
socket.close();
}
private static String geInputMessage() {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入要发送的消息: ");
return scanner.nextLine();
}
}
工具类
public class NetUtils {
public static void closeConnection(OutputStream outputStream, InputStream inputStream) throws IOException {
if(outputStream != null) {
outputStream.close();
}
if(inputStream != null) {
inputStream.close();
}
}
}
传统的bio有两个地方会阻塞:
1、服务端等待连接
Socket socket = serverSocket.accept();
2、输入流读取数据
inputStream.read(buf)
在建立连接成功的前提下阻塞了,那问题一定处在了read(),debug后也证实了我的想法。
问题在于我用本地读取文件的思路,来处理socket中的输入流:定义一个byte容器,不断接收输入流里面的字节,并用buf数组作为缓冲,直到read返回-1才跳出循环。
然而,socket通信时,是一个长连接,输入流没有明确的结束符,所以inputStream.read() 一般情况下不会返回-1。
除非连接关闭(socket.close()),或者有一方主动关闭了输入流(socket.shutdownInput()),才会返回-1。
但是为了保持连接,持续对话,这两种方式都不能使用。
解决方案
方案一:
定义一个比较大的buf数组,将输入流里的数据全部读进这个数组里。
服务端
public class ServerProDemo {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8081);
Socket socket = serverSocket.accept();
handleSocket(socket);
}
private static void handleSocket(Socket socket) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
while (true) {
byte[] buf = new byte[2048];
inputStream.read(buf);
String res = new String(buf, StandardCharsets.UTF_8);
System.out.println(res);
if ("exit".equals(res)) {
break;
}
//回写信息
res = "服务器收到你发来的:" + res;
outputStream.write(res.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
NetUtils.closeConnection(outputStream, inputStream);
socket.close();
}
}
客户端
public class ClientProDemo {
public static void main(String[] args) throws IOException {
Socket socket = new Socket(InetAddress.getLocalHost(), 8081);
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
while (true) {
String message = geInputMessage();
//给服务器发送消息
outputStream.write(message.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
if("exit".equals(message)) {
break;
}
//接收服务器回写的信息
byte[] buf = new byte[2048];
inputStream.read(buf);
System.out.println(new String(buf, StandardCharsets.UTF_8));
}
NetUtils.closeConnection(outputStream, inputStream);
socket.close();
}
private static String geInputMessage() {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入要发送的消息: ");
return scanner.nextLine().trim();
}
}
分析:
这种方法的缺点还是比较明显的。
如果一次发送的消息过多,超过了buf的大小,多出来的部分将会残留在输入流中,下次读取的时候才能读到,导致消息错位。
把buf数组大小调成1024以下,发送一个比较大的字符串,就会出现我描述的方法。
方案二:
问了公司的大佬后,得到的回复是:
一般情况下都会提前定义通讯的格式,比如规定起始符、数据域的长度、数据域、结束符等。
我想起来了之前一个项目,设备厂商给的通信协议。虽然也是tcp通信,但是提前约定好的数据格式,按照顺序依次是:
帧头(1字节)、业务相关的标识(1字节),控制码(1字节,标识功能),数据长度(2字节,标识要发送的数据域的长度),数据域(N字节),校验(1字节),序号(1字节),帧尾(1字节)。
如果是公司内部的设备通信,数据量有限的的情况下,也可以用一个长度大点的数组接收(如方案一,有同事就是这么做的,甚至定了一长度过万的数组~~)。
但总的来说不是很好,比较规范的方式还是提前约定一个比较通用的数据格式。
拓展
总结一下,难点就在于tcp连接的双方发送消息后,怎么才能告诉对方,我已经发送完了,你别再等我了,该干嘛干嘛去。
最常见的http协议也是基于tcp传输的,我们可以了解一下http是怎么解决这个问题的。
http/1.0,使用Content-Length来表示要发送的数据有多大(粗浅的理解为byte数组的长度)。
http/1.1,引入chunk的概念,不会把整体的长度计算给你,而是分块传输。但是每一个分块也会拿出两个字节,来表示这个块数据域的大小。最后一个分块的数据域长度来标志传输的结束。
所以最后的解决方案还是 :起始标识符 + 数据长度 + 数据域 + 结束符。
关于http trunk 以及 Content-Length 相关的内容,我描述的可能不是很准确,可以看看下面的几篇文章:
HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP