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

目录

一、bug描述

 二、问题分析

三、问题解决

🍑改过后的服务器代码

 🍑改过后的客户端代码:

四、进一步的改进


一、bug描述

 

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

下面是服务器的代码

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查看相关的线程执行情况 

 ​​​​​​​

 

客户端相关代码: 

 

 

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

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

 对应的服务器代码:

 

 

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

 

 对应的服务器代码: 

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

 

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

 

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

String request = scannerNet.nextLine();

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


 

三、问题解决

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

 

 对应的服务器代码:

🍑改过后的服务器代码

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();
    }
}

 

测试案例

四、进一步的改进

 实现多线程服务器

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();
    }
}

 

 

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

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();
    }
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小鱼儿哈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值