Java Socket 长连接互发消息时发生的阻塞问题

想实现一个功能,客户端发送一个消息,服务器接收到消息后回复客户端。

写好后发现并没有像预期的那样运作,一运行就阻塞,经过一番排查发现了问题。

问题分析

原来的代码如下:

服务端:

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

 

Http协议Content-Length详解

Http协议中关于Content-Length的解读

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中,Socket服务端通常用于创建客户端与服务器之间的通信通道,而要实现长连接,我们需要关注如何有效地管理连接并保持数据交换。在长连接场景下,Socket会维持一个打开的连接直到主动关闭。以下是创建Java Socket服务器实现长连接的一般步骤: 1. **ServerSocket实例化**:创建一个ServerSocket监听特定的端口,通过`ServerSocket(int port)`构造函数指定。 ```java ServerSocket serverSocket = new ServerSocket(port); ``` 2. **接受连接**:服务器进入监听状态,当有客户端请求连接,调用`accept()`方法阻塞等待。 ```java Socket clientSocket = serverSocket.accept(); ``` 3. **处理连接**:对于每个新连接,你可以创建一个新的线程来处理其输入/输出流(InputStream和OutputStream),例如使用BufferedReader和PrintWriter。 ```java Thread clientHandler = new Thread(new Runnable() { @Override public void run() { try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { // 读取客户端消息并响应... } catch (IOException e) { e.printStackTrace(); } } }); clientHandler.start(); ``` 4. **维护连接**:在处理过程中,可以周期性地检查连接是否还活跃,如果需要,发送心跳包或检查客户端是否有数据传输。如果连接断开,可以根据需求选择关闭连接或尝试恢复。 5. **关闭连接**:当不再需要连接,记得关闭Socket资源以释放系统资源,如`clientSocket.close()`。 **相关问题--:** 1. 长连接如何避免性能瓶颈? 2. Java中如何处理长连接的异常情况? 3. 有没有现成的库或框架简化长连接的管理?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值