Java实现Socket网络编程(五)

在看到本文之前,如果读者没看过笔者的前文 Java实现Socket网络编程(四),请先翻阅。

接下来,笔者对几个核心点进行剖析:
1、如果读者是初次进行Socket网络编程开发,起初可能会因为端口的使用不当,而导致Socket无法连接。所以读者可以在编程前进行测试,这里笔者提供了一个方法:

    /**
     * 用于检测能连接到的端口号
     */
    public static void scan(String host) {
        Socket socket = null;

        for (int port = 1024; port < 10055; port++) {
            try {
                socket = new Socket(host, port);
                System.out.println(socket);
            } catch (IOException e) {
                continue;
            } finally {
                try {
                    if (socket != null) {
                        socket.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

2、当发现程序只能在本机运行,而在其他机器上不能运行时,是因为读者所使用的ip地址是本机ip地址,如果采用 loop back 地址 "127.0.0.1",则可以在任意机器上运行。(本案例实际上是本机服务器与本机客户端的Socket通信)

3、笔者在测试过程中,发现了这样一个现象:服务器检测客户端断开,零延时;而客户端检测服务器断开,有明显延时(延时长短和运行机器的速度有关)。

这是什么原因导致的呢?经测试发现,服务器检测客户端断开,心跳包异常的捕获速度快于读写错误的捕获,从而零延时。而客户端检测服务器断开,读写错误的捕获速度快于心跳包异常的捕获速度,导致有延时。

这种现象的产生,是由于Java底层对”服务器监听客户端断开“及”客户端监听服务器断开“的实现机制不一样,导致了两者之间的速度差异。

既然是实现机制的原因,那我们是否就没有解决的方法呢?显然不屈不饶的程序员不会就此善罢甘休。

服务器检测客户端断开,零延时,我们不需要再过多干预;我们要干预客户端检测服务器断开,以使得比捕获读写异常的速度更快地发现服务器断开。

笔者经过多次尝试:
①首先是想到自定义一个结束符,例如"bye",希望通过服务器传递”bye“给客户端,客户端就”意识到“服务器关闭。这可以做到,但是存在一个漏动,如果”bye“是由使用者在服务器对话框发送过去的呢?那样客户端就会“以为”服务器断开。

既然这样,笔者就想着改进,把结束符“bye”改成转义字符,如"\\u0025",让用户不容易输入,经过数次测试发现,用户毫无一例会成功输入"\\u0025"结束符,看起来毫无Bug,但这显然还是一个漏动,哪怕只有万分之一的可能?

②为了进一步改进,笔者想着自定义一种数据协议格式:
【服务器是否启动标志位】【要接收的数据】

这看起来不错,但前面已经提到,客服端读取数据要在for循环里,如果用String类的startsWith("启动标志位")方法判断,则客户端显示数据由于无法判断服务器一次性发送内容的结束,会导致如下输出结果:

s
sa
say
say h
say he
say hel
say hell
say hello
say hello!

③既然如此,也就是要为数据提供结束符,以判断数据长度,笔者再一次修改数据协议格式:【要接收的数据】【服务器是否启动标志位】

通过String类的endsWith("启动标志位")进行检测

然而,这又产生了一个新问题,当用户直接输入”服务器断开标志位“,客户端又再一次”认为“服务器已断开。

④笔者进一步考虑,如果加上长度判断,当输入内容s.length()>"标志为长度"且endsWith("启动标志位")进行检测。

心想理应成功,没料到又出现这样的一种情况:当用户输入任意长度字符+”服务器断开标志位“时,客户端又再一次”认为“服务器已断开。

⑤历经多次挫折,笔者最后对比使用C#的Socket网络编程实现,研究了其 Send()方法和 Receive()方法(Send方法用于发送数据,Receive方法用于接收数据,C#封装了底层输入输出流的实现,而Java没有,所以需要进行输入输出流的操作)

总结出一种方法:模拟C#的Receive函数(该函数具有检测服务器是否断开,且能返回接收数据长度)

笔者定义了以下的数据协议格式,彻底解决了”客户端延时发现服务器断开“、”发送数据无边界“的问题:【服务器是否启动标志位】【接收数据的长度】【要接收的数据】

在发送数据前进行包装

   String message = Common.OK; // 代表服务器正常连接
   String t = "server " + Common.IP + ":" + Common.PORT + " "
                        + jtaSendMessage.getText();
   /**
    *  封装发送数据的长度
    */
   String c = "" + t.length();
   if (c.length() < 2) {
       c = "000" + c;
   } else if (c.length() < 3) {
       c = "00" + c;
   } else if (c.length() < 4) {
       c = "0" + c;
   }

   message += c + t;

在收数据时进行解封

   // 使用"GBK"编码读取中文
   brIn = new BufferedReader(new InputStreamReader(
                        mSocket.getInputStream(), "GBK"));

   String s = "";// 记录每次读取的内容
   int count = -10;// 记录每次读取内容的长度
   // 接收内容并把内容添加到信息接收区
   for (int c = brIn.read(); c != -1; c = brIn.read()) {
        s += (char) c + "";
        count++;// 读取的长度
        // 如果服务器连接且一次数据接收完成
        if (s.startsWith(Common.OK) && s.length() > 10
                && count == Integer.parseInt((s.substring(6, 10)))) {
                        ClientMain.jtaReceivedMessage.append(s.substring(10)
                                + "\n");
                        count = -10;
                        s = "";
        }// 服务器断开且一次数据接收完成
        else if (s.startsWith(Common.ERROR) && s.length() > 10
                && count == Integer.parseInt((s.substring(6, 10)))) {
                        ClientMain.jtaReceivedMessage.append(s.substring(10)
                                + "\n");
                        count = -10;
                        s = "";
                        ClientMain.jlConnect.setText("Out Of Connect.");
        }
        // 滚动到底端
        ClientMain.jtaReceivedMessage
                            .setCaretPosition(ClientMain.jtaReceivedMessage
                                    .getText().length());
    }

以上为本次案例的全部内容,最后,笔者在github上给出了这个案例的完整源码和可运行文件Socket网络编程,供读者学习思考。

谢谢支持!

你可能感兴趣的:(Java实现Socket网络编程(五))