《bug记录》在利用TCP协议创建【服务器-客户端交互程序】中出现的一些问题

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 《bug记录》在利用TCP协议创建【服务器-客户端交互程序】中出现的一些问题

一、bug描述

88d1152fd0624da0b203a23eed474eb9.png今天在写上传交互程序中,当我把服务器和客户端成功启动后,在客户端输入请求指令的时候,出现了上述情况。

下面是服务器的代码

package network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
    ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
        Socket clientSocket = serverSocket.accept();
        processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
    }
    // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    public void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream() ) {
            Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                if (!scannerNet.hasNext()) {
                    // 说明此时连接中断,服务器无法继续从客户端读取到请求
                    // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 1、读取客户端的请求
                String request = scannerNet.nextLine();
                // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
                // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
                // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
                // 2、处理客户端发来的请求
                String response = process(request);
                // 3、把响应发送给客户端
                printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
                printWriter.flush(); // 刷新缓冲区
                // 打印日志
                System.out.printf("request: %s, response: %s\n", request, response);
            }
        }
        finally {
            clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
        }
    }
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        server.start();
    }
}

客户端的代码:

package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
    Socket socket = null;
    public TcpEchoClient() throws IOException {
        // 指定客户端要连接的服务器
        socket = new Socket("127.0.0.1", 8000);
        // 程序走到了这里,说明客户端和服务器就已经成功建立了连接
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("当前是客户端,请输入请求");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送
            // 对我们的读取和发送进行写包装,使得读取和包装更方便
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
            while (true) {
                System.out.print("> ");
                // 1、客户端从键盘输入请求
                String request = scanner.nextLine();
                // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了
                // 2、把请求发送给服务器
                printWriter.write(request);
                // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符
                // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞
                printWriter.flush(); // 刷新缓冲区
                // 3、从服务器读取响应内容
                String response = scannerNet.nextLine();
                // 4、打印日志
                System.out.printf("request: %s, response: %s\n", request, response);
            }
        }
        finally {
            socket.close();
        }
}
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient();
        client.start();
    }
}

二、问题分析

于是我打开java的工具包jconsole.exe查看相关的线程执行情况


087bf5640df9418ea8dfa41f2a03ddf0.png

7d31728b3c2b45c4a81e0ea8e6edcb42.png

客户端相关代码:


994905ed32d642f7863b7019c84bd1e7.png

那么客户端在这里出现异常正常吗?很正常因为我们的服务器在正常情况下,当把响应发送给客户端后,是要打印日志的。但从我们上述的运行结果来看,我们的服务器并没有打印响应(这说明服务器没有成功的把响应发送给客户端,而是陷入了阻塞)


所以客户端在这里的阻塞是正常的


对应的服务器代码:

b937fdf141ab4e18ab69619698338c29.png

那么我们在来看看服务器的代码究竟在哪里出现了问题:’


77eddf6f7a1d4837958d39087962b8d9.png

对应的服务器代码:

e40482a5c38f4c0087959dbee6213565.png


为什么会在scannerNet.nextLine()这里陷入阻塞呢?nextLine不是遇到回车就结束吗?我们在客户端输入请求后明明敲了一个回车呀!

 

通过下面这个栗子我们可以发现一些问题:


ee3895bff2ec41d9b016df59df97c2f1.png

 

那么如果这样的话, 我们服务器从客户端读取request的时候,接收到的数据也是没有换行符的——也就是说:

String request = scannerNet.nextLine();

这一行的读取一直无法正常的结束,线程自然就陷入了阻塞当中。


三、问题解决

清楚了这一点就好办了,我们在下面的客户端代码中:


8723afa8abb246d5b16c11abadb72ea1.png

对应的服务器代码:

01ae52bd920f45049a0c25fe59409335.png

🍑改过后的服务器代码

package network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
    ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
        Socket clientSocket = serverSocket.accept();
        processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
    }
    // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    public void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream() ) {
            Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                if (!scannerNet.hasNext()) {
                    // 说明此时连接中断,服务器无法继续从客户端读取到请求
                    // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 1、读取客户端的请求
                String request = scannerNet.nextLine();
                // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
                // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
                // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
                // 2、处理客户端发来的请求
                String response = process(request);
                // 3、把响应发送给客户端
                printWriter.println(response);
                // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
                printWriter.flush(); // 刷新缓冲区
                // 打印日志
                System.out.printf("request: %s, response: %s\n", request, response);
            }
        }
        finally {
            clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
        }
    }
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        server.start();
    }
}

🍑改过后的客户端代码:

package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
    Socket socket = null;
    public TcpEchoClient() throws IOException {
        // 指定客户端要连接的服务器
        socket = new Socket("127.0.0.1", 8000);
        // 程序走到了这里,说明客户端和服务器就已经成功建立了连接
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("当前是客户端,请输入请求");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送
            // 对我们的读取和发送进行写包装,使得读取和包装更方便
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
            while (true) {
                System.out.print("> ");
                // 1、客户端从键盘输入请求
                String request = scanner.nextLine();
                // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了
                // 2、把请求发送给服务器
                printWriter.println(request);
                // printWriter.write(request);
                // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符
                // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞
                printWriter.flush(); // 刷新缓冲区
                // 3、从服务器读取响应内容
                String response = scannerNet.nextLine();
                // 4、打印日志
                System.out.printf("request: %s, response: %s\n", request, response);
            }
        }
        finally {
            socket.close();
        }
}
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient();
        client.start();
    }
}

测试案例

29fe6eca115b48d4987f83e64dd018c0.png

四、进一步的改进

实现多线程服务器

package network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
    ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        while (true) {
            // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
            // 如果当前没有客户端来建立连接, 就会阻塞等待!!
            Socket clientSocket = serverSocket.accept();
            // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
            // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信
            // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的.
            Thread thread = new Thread(() -> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
    // 通过这个方法, 给当前连上的这个客户端, 提供服务!!
    // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    public void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream() ) {
            Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                if (!scannerNet.hasNext()) {
                    // 说明此时连接中断,服务器无法继续从客户端读取到请求
                    // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 falseSystem.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 1、读取客户端的请求
                String request = scannerNet.nextLine();
                // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
                // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
                // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
                // 2、处理客户端发来的请求
                String response = process(request);
                // 3、把响应发送给客户端
                printWriter.println(response);
                // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
                printWriter.flush(); // 刷新缓冲区
                // 打印日志
                System.out.printf("[%s:%d]  request: %s, response: %s\n",
                        clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        }
        finally {
            clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
        }
    }
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        server.start();
    }
}

668fe80a45fc480095bca50546adb09e.png

通过线程池实现多线程服务器

package network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
    ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        ExecutorService service = Executors.newCachedThreadPool();// 此处不太适合使用 "固定个数的"
        while (true) {
            // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
            // 如果当前没有客户端来建立连接, 就会阻塞等待!!
            Socket clientSocket = serverSocket.accept();
            // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
            // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信
            // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的.
//            Thread thread = new Thread(() -> {
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            thread.start();
            // [版本3] 使用线程池, 来解决频繁创建销毁线程的问题.
            // 此处不太适合使用 "固定个数的"
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    // 通过这个方法, 给当前连上的这个客户端, 提供服务!!
    // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    public void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream() ) {
            Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                if (!scannerNet.hasNext()) {
                    // 说明此时连接中断,服务器无法继续从客户端读取到请求
                    // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 1、读取客户端的请求
                String request = scannerNet.nextLine();
                // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
                // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
                // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
                // 2、处理客户端发来的请求
                String response = process(request);
                // 3、把响应发送给客户端
                printWriter.println(response);
                // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
                printWriter.flush(); // 刷新缓冲区
                // 打印日志
                System.out.printf("[%s:%d]  request: %s, response: %s\n",
                        clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        }
        finally {
            clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
        }
    }
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        server.start();
    }
}


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
存储 开发工具 git
[Git] 深入理解 Git 的客户端与服务器角色
Git 的核心设计理念是分布式,每个仓库既可以是客户端也可以是服务器。通过 GitHub 远程仓库和本地仓库的协作,Git 实现了高效的版本管理和代码协作。GitHub 作为远程裸仓库,存储项目的完整版本历史并支持多客户端协作;本地仓库则通过 `.git` 文件夹独立管理版本历史,可在离线状态下进行提交、回滚等操作,并通过 `git pull` 和 `git push` 与远程仓库同步。这种分布式特性使得 Git 在代码协作中具备强大的灵活性和可靠性。
70 18
[Git] 深入理解 Git 的客户端与服务器角色
|
3月前
|
存储 人工智能 自然语言处理
ChatMCP:基于 MCP 协议开发的 AI 聊天客户端,支持多语言和自动化安装 MCP 服务器
ChatMCP 是一款基于模型上下文协议(MCP)的 AI 聊天客户端,支持多语言和自动化安装。它能够与多种大型语言模型(LLM)如 OpenAI、Claude 和 OLLama 等进行交互,具备自动化安装 MCP 服务器、SSE 传输支持、自动选择服务器、聊天记录管理等功能。
461 15
ChatMCP:基于 MCP 协议开发的 AI 聊天客户端,支持多语言和自动化安装 MCP 服务器
|
3月前
|
缓存 网络协议 Java
【JavaEE】——TCP回显服务器(万字长文超详细)
ServerSocket类,Socket类,PrintWriter缓冲区问题,Socket文件释放问题,多线程问题
|
4月前
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
78 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
4月前
|
XML 前端开发 JavaScript
PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑
本文深入探讨了PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑;Ajax则通过异步请求实现页面无刷新更新。文中详细介绍了两者的工作原理、数据传输格式选择、具体实现方法及实际应用案例,如实时数据更新、表单验证与提交、动态加载内容等。同时,针对跨域问题、数据安全与性能优化提出了建议。总结指出,PHP与Ajax的结合能显著提升Web应用的效率和用户体验。
98 3
|
5月前
|
安全 区块链 数据库
|
5月前
|
弹性计算 安全 Windows
通过远程桌面连接Windows服务器提示“由于协议错误,会话将被中断,请重新连接到远程计算机”错误怎么办?
通过远程桌面连接Windows服务器提示“由于协议错误,会话将被中断,请重新连接到远程计算机”错误怎么办?
|
1天前
|
弹性计算 运维 监控
【阿里云】控制台使用指南:从创建ECS到系统诊断测评
本文介绍了如何通过阿里云获取ECS云服务器并进行操作系统配置与组件安装,以实现高效的资源管理和系统监控。阿里云凭借强大的基础设施和丰富的服务成为用户首选。文中详细描述了获取ECS、RAM授权、开通操作系统控制台及组件安装的步骤,并展示了如何利用控制台实时监控性能指标、诊断系统问题及优化性能。特别针对idle进程进行了深入分析,提出了优化建议。最后,建议定期进行系统健康检查,并希望阿里云能推出更友好的低成本套餐,满足学生等群体的需求。
49 17
【阿里云】控制台使用指南:从创建ECS到系统诊断测评
|
2天前
|
弹性计算 Linux 数据安全/隐私保护
阿里云幻兽帕鲁联机服务器搭建全攻略,速来抄作业!2025新版教程
阿里云提供2025年最新幻兽帕鲁服务器申请购买及一键开服教程。4核16G配置支持8人,70元/月;8核32G配置支持20人,160元/月。选择配置、地域、操作系统后,点击【一键购买及部署】,约3分钟完成创建。本地安装STEAM客户端并登录,进入游戏选择多人模式,输入服务器IP和端口(8211),即可开始游戏。详细教程及更多问题解答请参考阿里云幻兽帕鲁游戏专区。
43 20
|
1天前
|
存储 人工智能 运维
阿里云操作系统控制台——解决服务器磁盘I/O故障
阿里云操作系统控制台——解决服务器磁盘I/O故障
28 12

热门文章

最新文章