关于创建一个1对1的聊天的问题。

来源:5-2 UDP搜索IP与端口-2

PerryMore

2019-03-14

本来我的思路是创建一个类HandlerSocketHelper,它的作用就是开启读和写两条线程分别进行数据处理。但是以下代码服务器端调试了很久一直出现无限读取为null的现象,希望老师帮忙看一下哪里的问题。

import helper.HandlerSocketHelper;

import java.io.*;
import java.net.*;

public class Client {
    private static final int SERVER_PORT = 20000;
    private static final int LOCAL_PORT = 20001;
    private static final int NET_DATA_SIZE = 64 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Socket socket = createSocket();
        initSocket(socket);
        socket.connect(new InetSocketAddress(Inet4Address.getByName("192.168.199.117")
                , SERVER_PORT), 3000);

        System.out.println("已发起服务器连接,并进入后续流程~");
        System.out.println("客户端信息: " + socket.getLocalAddress() + " : " + socket.getLocalPort());
        System.out.println("服务端信息: " + socket.getInetAddress() + " : " + socket.getPort());

        HandlerSocketHelper mSocketHelper = new HandlerSocketHelper(socket);
        mSocketHelper.operateSocket();

    }

    private static Socket createSocket() throws IOException {
        Socket socket = new Socket();

        socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), LOCAL_PORT));

        return socket;
    }

    private static void initSocket(Socket socket) throws IOException {
        // 设置读取超时时间为2s
        socket.setSoTimeout(2000);

        // 1、是否复用未完全关闭的socket地址,对于指定bind操作后的套接字有效
        // 2、当TCP连接调用关闭操作时,此时网络层连接通道在一小段时间内还是会保持
        // 着超时连接状态
        // 3、需要在bind操作前进行设置。
        //
        // 翻译:对于我们使用一个众所周知的IP地址和端口而言,如果牵连到这个端口的
        // 连接正处于超时连接状态,这将可能导致我们无法绑定到这个所需的端口上。
        socket.setReuseAddress(true);

        // 是否开启nagle算法
        // 1、比较严谨的模式下:一份数据包发送完后,服务端将进行收到数据的回送消息。
        // 但这样回送包将充斥在网络通道中,有种方式就是将几个数据打包成一份数据,然
        // 后同时发送给服务器端,最后收到一份回送包数据。
        // 2、网络环境中,小片数据发送时,其首部的封装数据比实际有用数据量还要大,这
        // 显然会扩大网络数据传输的消耗。
        // true:无延迟,表示不使用Nagle算法。
        socket.setTcpNoDelay(false);

        // 是否需要在长时无数据响应时发送确认数据(类似心跳包),时间大约2小时。
        socket.setKeepAlive(true);

        // 对于close关闭操作的行为做何种处理,默认(false,0)
        socket.setSoLinger(false, 0);

        // 是否让紧急数据内敛,默认为false;通过 socket.sendUrgentData();进行发送
        // 所谓内敛,就是是否会和应用层的行为数据出现在一起。一般不希望出现这种情况。
        socket.setOOBInline(false);

        socket.setReceiveBufferSize(NET_DATA_SIZE);
        socket.setSendBufferSize(NET_DATA_SIZE);

        // 设置性能参数:短链接,延迟,宽带的相对权重
        socket.setPerformancePreferences(1, 1, 1);
    }


}

import helper.HandlerSocketHelper;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    private static final int SERVER_PORT = 20000;

    public static void main(String[] args) throws IOException {
        ServerSocket server = createServerSocket();

        System.out.println("服务器已开启,等待客户端连接~");
        System.out.println("服务端信息: " + server.getInetAddress() + " : " + server.getLocalPort());

        // 这里的无限循环是为了接受不同的客户端连接
        for (; ; ) {
            // 这个方法会阻塞,只有新的客户端进行连接时才会被调用
            Socket client = server.accept();
            // 一个客户端连接后,为了能够持续的接受到其发过来的消息,那么就需要一个无限循环来不断从已连接的
            // 流通道中读取数据。
            HandlerSocketHelper mSocketHelper = new HandlerSocketHelper(client);
            mSocketHelper.operateSocket();


        }

    }

    private static ServerSocket createServerSocket() throws IOException {
        ServerSocket server = new ServerSocket();
        // 设置超时时间,当2s内没有新的客户端连接时(也就是accept方法没有返回时),服务器端将会断开。
//        server.setSoTimeout(2000);

        server.bind(new InetSocketAddress(InetAddress.getLocalHost(),SERVER_PORT));

        return server;
    }

    
}

package helper;

import java.io.*;
import java.net.Socket;

public class HandlerSocketHelper {
    private Socket mSocket;
    private int mUserPort;
    private String mUserAddress;
    private String mUserFormat;

    public HandlerSocketHelper(Socket mSocket) {
        this.mSocket = mSocket;
        mUserFormat = "聊天用户 %s 说:";
    }

    public void operateSocket() {
        if (mSocket == null)
            return;

        mUserPort = mSocket.getPort();
        mUserAddress = mSocket.getInetAddress().getHostAddress();

        System.out.println("客户信息:IP:" + mUserAddress);
        System.out.println("Port:" + mUserPort);

        ReadThread readThread = new ReadThread();
        readThread.start();

        WriteThread writeThread = new WriteThread();
        writeThread.start();
    }

    public void exit() {
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            mSocket = null;
        }
    }

    class ReadThread extends Thread {
        private boolean isDone;

        @Override
        public void run() {
            super.run();

            BufferedReader readStream;
            try {
                readStream = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }

            while (!isDone) {
                try {
                    String line = readStream.readLine();

                    if ("bye".equalsIgnoreCase(line)) {
                        isDone = true;
                    } else {
                        System.out.println(String.format(mUserFormat, mUserAddress) + line);
                    }
                } catch (IOException e) {
                    HandlerSocketHelper.this.exit();
                    System.out.println("异常退出!");
                    break;
                }
            }

            try {
                readStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println(String.format(mUserFormat, mUserAddress) + "我退出了!");


        }
    }

    class WriteThread extends Thread {

        @Override
        public void run() {
            super.run();
            PrintStream printStream;
            try {
                printStream = new PrintStream(mSocket.getOutputStream());
            } catch (IOException e) {
                e.printStackTrace();
                HandlerSocketHelper.this.exit();
                return;
            }

            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

            while (true) {
                try {
                    String line = bufferedReader.readLine();
                    printStream.println(line);

                } catch (IOException e) {
                    HandlerSocketHelper.this.exit();
                    break;
                }

            }

        }
    }

}

写回答

2回答

Qiujuer

2019-03-16

这个问题,我调试了一下的确是出现循环读取null的情况。

一般来说读取操作是阻塞的,也就是会等待到数据到来才会返回,但是此刻直接返回了,原因无他:

  1. 客户端已异常退出,所以关闭了连接

  2. 服务器并为在客户端断开时进行退出处理,所以依然处于读取数据状态

但是读取数据的逻辑中我们可以调试跟踪进去看见:

//img.mukewang.com/szimg/5c8c774600010e2b13220630.jpg

他进入到了EOF判断块中,并且当前并为含有任何的读取数据;所以可以判定是已经读取到末尾了。

读取到末尾的原因也是因为客户端断开导致的问题。


所以本质来说没有任何的问题,只是客户端断开时服务器应该进行对应的处理即可。

一般来说:

String line = readStream.readLine();

只要返回了null其实我们就可以判定为出现了异常断开了,而不必继续去进行读取操作。

0
1
PerryMore
非常感谢!
2019-03-24
共1条回复

Qiujuer

2019-03-15

好的,我看看哈,一会儿给你答复。

0
0

Socket网络编程进阶与实战 系统掌握Socket核心技术

理论+实践,系统且深入掌握Socket核心技术,从容应对各种Socket应用场景的不二之选

2316 学习 · 476 问题

查看课程