Java聊天器

本文最后更新于:5 个月前

项目地址

https://github.com/Chao-Yin-Github/javaChartDemo

项目介绍

  • 开发环境

    • Manjaro 18.1.4 Juhraya

    • JDK:1.8.0_212

    • intellij-idea 2019.3

  • 本项目基于Java NIO开发,使用到了以下技术:

    1. Java NIO

      使用ServerSocketChannel和SocketChannel,完成客户端和服务端socket通道的建立,来实现数据传输

    2. 使用slf4j日志框架进行日志管理和程序信息输出

    3. 使用lombok简化Java Bean的Setter和Getter方法、构造方法、toString等模板化方法的撰写

    4. 自定义数据传输协议(Transmission.java)**

  • 技术栈分析

    • NIO

      • 背景

        NIO是jdk1.4才引入的,有人称作Non-Blocking-IO(非阻塞IO),还有人喜欢把他成为New-IO。

        我认为各自都有理,首先Non-Blocking-IO就不用说了,因为NIO提供了非阻塞的IO模式,这和传统的BIO(Blocking-IO)有了明显区别。

        那为什么New-IO也是可以的呢,因为NIO是面向缓冲的(Buffer oriented),基于通道(Channel)(双工,既能读也能写)的IO操作方法,这样就和老式的BIO面向流(Stream oriented)(单方向,只能读或者只能写)的操作,有了本质的区别,所以把它称作New-IO也不无道理。

    • 使用自定义协议传输数据(Transmission.java)

      • 背景:

        当我在基本分别完成传输字符串和传输文件的功能时,遇到了一个问题,NIO的Server是单线程的,那么就不能通过一般的方式解决传输文件和文本字符串的问题。

        因为如果是多线程的服务端,可以在有一个新的连接到服务端时,服务端就开启一个线程专门处理这个客户端的连接,多个客户端之间不会混乱。

        这样,我这个用户1可以先发送一个标识符来标识一下,我接下来将要发送文本字符串还是文件。如果有客户2同时发送别的信息,服务端的这两个线程之间是不会受到任何影响的。

        但是通过NIO写的服务端是单线程处理连接的,这样,一个用户1发送一个标识符表示接下来会发送一个字符串,但是这时,又有另外一个用户把文件发送过来,服务端是没法分辨出到底是哪一个客户端发送的标识符,也无法分辨出是哪一个客户端发送的数据,这样服务端就会混乱掉。

      • 解决办法

        1. 方法一:

          最容易想到的是:

          既然服务端无法标识用户,那么客户端就可以规定一个长度一定的标识,然后后面接着数据一起发送给服务端,那么服务端就可以直接把标识信息和数据一起读出来。

          我传输文件用的是FileChannel,然后再使用transferFrom和transferTo方法来实现传到SocketChannel里面和从SocketChannel里面读取数据到FileChannel。

          这样转化其实是可行的,但是我觉得有些麻烦也不够优雅。

        2. 方法二:

          和方法一一样,都是把标识信息和数据一起传输,但是方式略有不同。

          对于发送方的客户端,我们可以把信息和数据全部封装成一个类,然后我们就可以把这个类序列化成字符串,再把字符串转化为字节数组用SocketChannel传输。

          而服务端则将接收的字节数组反序列化成对象,从中提取需要的信息,再序列化传输给另一个客户端。

          至于另一个接收方的客户端,则同理,将获得的字节数组反序列化成对象,就可以拿到数据进行处理获取相应的内容了。

      • 选择方法二的理由

        我认为方法二比方法一更好的理由是:如果自定义一个类传输数据,我们就可以把这个类称作为一种数据传输协议。

        因为像http传输协议就是一个基于tcp/ip的传输协议,它定义了请求/响应行(Request/Response Line),请求/响应头(Request/Response Header),空行和请求/响应数据(Request/Response Body),所有使用http协议都要有它所规定的这些结构。

        而这个Transmission类其实也是一样的道理,声明了数据的类型(TransmissionType),也保存了数据内容(content)本身,还有消息目的方的信息(destinationNumber)等等信息。

        由此,我可以大胆把这个Transmission类称作为我自己的一个聊天器协议!

      • Transmission.java

        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public class Transmission implements Serializable {
            /**
             * 数据内容
             */ private String content;
        
            /**
             * 文件类型
             */
            private TransmissionType transmissionType;
        
            /**
             * 发送目的客户编号
             */
            private int destinationNumber;
        
            /**
             * 原客户编号
             */
            private int sourceNumber;
        }
      • TransmissionType.java

        public enum TransmissionType implements Serializable {
            /**
             * 文件类型
             */
            FILE,
            
            /**
             * 文本字符串
             */
            STRING,
        
            /**
             * 信息
             */
            MESSAGE,
        
            /**
             * id标识
             */
            UUID
        }
      • 那这个Transmission类如何传输文本字符串和文件这两种数据呢?

        对于文本字符串自不必说,把要发送的字符串存到content字段里即可,接收方可以直接获取content字段内容。

        至于传输文件,我是先把文件转化成字节数组,再用Base64加密以防字符集乱码,再转化成字符串存到content里。

        接收数据时,则跟发送数据时的操作正好相反,先把读到的字节数组转化成Transmission类,再拿到content字段内容之后,先用Base64解密,再转化为文件保存下来。

  • 可能时由于网络速度的限制加上NIO的特性,这个在本地测试没问题,可是一放到服务器上就出现超过一定大小文件就发送失败的问题。可能和服务器的网速太慢还有NIO非阻塞的特性两者综合作用的结果。因为网速较慢,所以有时服务端接收到一半就认为没有数据了,而客户端的数据还没有发送上来,而服务端又是NIO非阻塞的特性,所以很可能数据读到一半就停止了,开始转化信息,但是数据不完整就会出错。这个由于时间问题还没有解决,以后有空的时候还是想找找其他方法解决一下。