Netty教程-循序渐进学Netty之Java IO 体系、线程模型大总结

该文章已在GitChat发布:
https://gitbook.cn/gitchat/activity/6059ac3ed1fed911f45498e5

【仅需9.9订阅专栏合集,作者所有付费文章都能看】

推荐【Kafka教程https://bigbird.blog.csdn.net/article/details/108770504
推荐【rabbitmq教程https://bigbird.blog.csdn.net/article/details/81436980
推荐【Flink教程https://blog.csdn.net/hellozpc/article/details/109413465
推荐【SpringBoot教程https://blog.csdn.net/hellozpc/article/details/107095951
推荐【SpringCloud教程https://blog.csdn.net/hellozpc/article/details/83692496
推荐【Mybatis教程https://blog.csdn.net/hellozpc/article/details/80878563
推荐【SnowFlake教程https://blog.csdn.net/hellozpc/article/details/108248227
推荐【并发限流教程https://blog.csdn.net/hellozpc/article/details/107582771
推荐【JVM面试与调优教程https://bigbird.blog.csdn.net/article/details/113888604

循序渐进学Netty教程

本教程的题目是《循序渐进学Netty》,就是说要从最基础的知识开始讲起,一步一步、由浅入深引导大家进行Netty的学习。在正式学习Netty之前,我们不妨先回顾一下Java中I/O相关的知识。所谓温故而知新,可以为师矣!

回顾Java IO体系

Java中的I/O按照其发展历程,可以划分为传统IO(阻塞式I/O)和新IO(非阻塞式I/O)。

传统I/O

传统IO也称为BIO(Blocking IO),是面向字节流或字符流编程的I/O方式。

一个典型的基于BIO的文件复制程序

字节流方式

public class FileCopy01 {
    public static void main(String[] args) {
        //使用jdk7引入的自动关闭资源的try语句(该资源类要实现AutoCloseable或Closeable接口)
        try (FileInputStream fis = new FileInputStream("D:\\file01.txt");
             FileOutputStream fos = new FileOutputStream("D:\\file01_copy.txt")) {
            byte[] buf = new byte[126];
            int hasRead = 0;
            while ((hasRead = fis.read(buf)) > 0) {
                //每次读取多少就写多少
                fos.write(buf, 0, hasRead);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

字符流方式

public class FileCopy02 {
    public static void main(String[] args) {
        //使用jdk7引入的自动关闭资源的try语句
        try (FileReader fr = new FileReader("D:\\file01.txt");
             FileWriter fw = new FileWriter("D:\\file01_copy2.txt")) {
            char[] buf = new char[2];
            int hasRead = 0;
            while ((hasRead = fr.read(buf)) > 0) {
                //每次读取多少就写多少
                fw.write(buf, 0, hasRead);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

字符缓冲,按行读取

public class FileCopy02_2 {
    public static void main(String[] args) {
        //使用普通的Reader不方便整行读取,可以使用BufferReader包装,资源变量要定义在try()中,否则不会自动关闭
        try (FileReader fr = new FileReader("D:\\file01.txt");
             FileWriter fw = new FileWriter("D:\\file01_copy2_2.txt");
             BufferedReader bufferedReader = new BufferedReader(fr);
             BufferedWriter bufferedWriter = new BufferedWriter(fw)) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                //每次读取一行、写入一行
                bufferedWriter.write(line);
                bufferedWriter.newLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

随机读写(RandomAccessFile)

public class FileCopy03 {
    public static void main(String[] args) {
        try (RandomAccessFile in = new RandomAccessFile("D:\\file01.txt","rw");
             RandomAccessFile out = new RandomAccessFile("D:\\file01_copy3.txt","rw")) {
            byte[] buf = new byte[2];
            int hasRead = 0;
            while ((hasRead = in.read(buf)) > 0) {
                //每次读取多少就写多少
                out.write(buf, 0, hasRead);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

通常进行输入输出的内容是文本内容,应该考虑使用字符流;如果是二进制内容,则应考虑使用字节流;RandomAccessFile支持自由访问文件的任意位置。如果需要访问文件的部分内容,而不是从头读到尾,可以优先考虑RandomAccessFile,比如文件断点续传。

Java NIO

NIO也称新IO或者非阻塞IO(Non-Blocking IO)。传统IO是面向输入/输出流编程的,而NIO是面向通道编程的。

NIO的3个核心概念:Channel、Buffer、Selector。我们先来谈谈其中的两个。

Channel(通道)

Channel是对IO输入/输出系统的抽象,是IO源与目标之间的连接通道,NIO的通道类似于传统IO中的各种“流”。与InputStream和OutputStream不同的是,Channel是双向的,既可以读,也可以写,且支持异步操作。这契合了操作系统的特性,比如linux底层通道就是双向的。此外Channel还提供了map()方法,通过该方法可以将"一块"数据直接映射到内存中。因此也有人说,NIO是面向块处理的,而传统I/O是面向流处理的。

程序不能直接访问Channel中的数据,必须通过Buffer(缓冲区)作为中介。Channel可以直接将文件的部分或者全部映射成Buffer。Channel是一个接口,有多种实现类,比较常用的是FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel,分别用于文件读写,TCP客户端、服务端网络通信、UDP通信。

Channel通常都不是通过构造器来创建的,而是通过传统的输入/输出流的getChannel()来返回。通过不同类型的Stream获得的Channel也不同。比如常见的几个Channel的获取方式如下:

FileChannel:由文件流FileInputStream、FileOutputStream的getChannel()方法返回。

ServerSocketChannel:由ServerSocketChannel的静态方法open()返回。

SocketChannel:由SocketChannel的静态方法open()返回。

Channel中读写数据对应的方法分别是read(ByteBuffer)和write(ByteBuffer)方法。一些Channel还提供了map()方法将Channel对应的部分或全部数据映射为ByteBuffer(实际的实现类为MappedByteBuffer)。如果Channel对应的数据过大,使用map()方法一次性映射到内存会引起性能下降,此时还得用"多次重复取水"的方式处理。

Buffer(缓冲)

Buffer本质上就是一个容器,其底层持有了一个具体类型的数组来存放具体数据。从Channel中取数据或者向Channel中写数据都需要通过Buffer。在Java中Buffer是一个抽象类,除boolean之外的基本数据类型都提供了对应的Buffer实现类。比较常用的是ByteBuffer和CharBuffer。

通常Buffer的实现类中都没有提供public的构造方法,而是提供了静态方法allocate(int capacity)用来创建自身对应的Buffer对象。使用get、put方法来读取、写入数据到Buffer中。ByteBuffer还支持直接缓冲区,即ByteBuffer还提供了allocateDirect(int capacity)方法来创建直接缓冲区(直接使用堆外内存),这能与当前操作系统更好地耦合,减少数据在JVM堆内存和操作系统内核缓冲区之间的数据拷贝,进一步提高I/O的性能。但是分配直接缓冲区的系统开销较大,只适合缓冲区较大且需要长期驻留的情况。

Buffer中还有两个经常调用的重要方法,即flip()和clear()。flip方法为从Buffer中取出数据做好准备,而clear方法为再次向Buffer中写入数据做好准备。

NIO案例

下面通过几个例子演示一下NIO的日常操作

文件复制

public class FileCopy04 {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("D:\\file01.txt");
             FileOutputStream fos = new FileOutputStream("D:\\file01_copy4.txt");
             FileChannel inc = fis.getChannel();
             FileChannel outc = fos.getChannel()
        ) {
            ByteBuffer buffer = ByteBuffer.allocate(4);
            //多次重复"取水"的方式
            while (inc.read(buffer) != -1) {
                buffer.flip();
                outc.write(buffer);
                buffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

文件复制-映射方式

public class FileCopy05 {
    public static void main(String[] args) {
        File f = new File("D:\\file01.txt");
        try (FileInputStream fis = new FileInputStream(f);
             FileOutputStream fos = new FileOutputStream("D:\\file01_copy5.txt");
             FileChannel inc = fis.getChannel();
             FileChannel outc = fos.getChannel()
        ) {
            //将FileChannel里的全部数据映射到ByteBuffer中
            MappedByteBuffer mappedByteBuffer = inc.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
            outc.write(mappedByteBuffer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

文件复制-零拷贝方式

transferFrom方式

public class FileCopy06 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("D:\\file01.txt");
        FileOutputStream fos = new FileOutputStream("D:\\file01_copy06.txt");
        FileChannel srcChannel = fis.getChannel();
        FileChannel destChannel = fos.getChannel();
        destChannel.transferFrom(srcChannel, 0, srcChannel.size());
        destChannel.close();
        srcChannel.close();
        fis.close();
        fos.close();
    }
}

transferTo方式

public class FileCopy07 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("D:\\file01.txt");
        FileOutputStream fos = new FileOutputStream("D:\\file01_copy07.txt");
        FileChannel srcChannel = fis.getChannel();
        FileChannel destChannel = fos.getChannel();
        long size = srcChannel.size();
        long position = 0;
        while (size > 0) {
            long count = srcChannel.transferTo(position, srcChannel.size(), destChannel);
            position += count;
            size -= count;
        }

        destChannel.close();
        srcChannel.close();
        fis.close();
        fos.close();
    }
}

JAVA NIO2.0

JDK7对原有的NIO进行了改进。第一个改进是提供了全面的文件I/O相关API。第二个改进是增加了异步的基于Channel的IO机制。

我们说说第一个,第二个也就是通常所说的AIO( Asynchronous IO),即异步IO。由于实际工作中不常见,我们就不做介绍了。

原来的I/O框架中只有一个File类来操作文件,新的NIO引入了Path接口,代表一个平台无关的路径。并且提供了Paths、Files两个强大的工具类来方便文件操作。

使用新的API来完成文件复制代码大大简化:

public class FileCopy06 {
    public static void main(String[] args) {
        try (OutputStream fos = new FileOutputStream("D:\\file01_copy6.txt")) {
            Files.copy(Paths.get("D:\\file01.txt"), fos);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Java网络编程

下文通过几个网络IO的例子,循序渐进地讲述Socket编程的流程。先讲解传统Socket编程(阻塞式),再讲解基于NIO的Socket编程。

传统Socket编程

需求描述:实现一个简单的C/S架构的客户端/服务端通信程序,分别包括客户端程序和服务端程序。

版本1:实现客户端/服务端一次性简单通信

这个例子只是简单地"一问一答"模式,极其简单地演示一下Socket编程的逻辑。

服务端

public class Server1 {
    public static void main(String[] args) throws IOException {
        //开启一个TCP服务端,占用一个本地端口
        ServerSocket serverSocket = new ServerSocket(6666);
        //服务端循环不断地接受客户端的连接
        while (true) {
            Socket socket = null;
            try {
                //与单个客户端通信的代码放在一个try代码块中,单个客户端发生异常(断开)时不影响服务端正常工作
                System.out.println("server start...");
                //下面这行代码会阻塞,直到有客户端连接
                socket = serverSocket.accept();
                System.out.println("客户端" + socket.getRemoteSocketAddress() + "上线了");
                //从Socket中获得输入输出流,接收和发送数据
                InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream();
                byte[] buf = new byte[1024];
                int len;
                while ((len = inputStream.read(buf)) != -1) {
                    String msg = new String(buf, 0, len);
                    System.out.println("来自客户端的消息:" + msg);
                    String serverResponseMsg = "服务端收到了来自您的消息【" + msg + "】,并且探测到您的IP是:" + socket.getRemoteSocketAddress();
                    //向客户端回写消息
                    outputStream.write(serverResponseMsg.getBytes(StandardCharsets.UTF_8));
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //当与一个客户端通信结束后,需要关闭对应的socket
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

客户端

public class Client1 {
    public static void main(String[] args) {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        try {
            socket.connect(address, 2000);
            OutputStream outputStream = socket.getOutputStream();
            String clientMsg = "服务端你好!我是客户端!你的IP是:" + socket.getRemoteSocketAddress();
            outputStream.write(clientMsg.getBytes(StandardCharsets.UTF_8));

            InputStream inputStream = socket.getInputStream();
            byte[] buf = new byte[1024];
            int len;
            while ((len = inputStream.read(buf)) != -1) {
                String msgFromServer = new String(buf, 0, len);
                System.out.println("来自服务端的消息:" + msgFromServer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

版本2:实现客户端可以不断接收用户输入

版本1演示了最简单的Socket编程,只能实现一次性通信。现在要求客户端能够不断地接收用户输入,多次与服务端通信。服务端代码不变,客户端改造如下:

public class Client2 {
    public static void main(String[] args) {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        try {
            socket.connect(address, 2000);
            OutputStream outputStream = socket.getOutputStream();
            InputStream inputStream = socket.getInputStream();

            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
            String clientMsg;
            System.out.println("请输入消息:");
            while ((clientMsg = bufferedReader.readLine()) != null) {
                outputStream.write(clientMsg.getBytes(StandardCharsets.UTF_8));
                byte[] buf = new byte[1024];
                int readLen = inputStream.read(buf);
                String msgFromServer = new String(buf, 0, readLen);
                System.out.println("来自服务端的消息:" + msgFromServer);
                System.out.println("请输入消息:");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

版本3:使用字符流包装

上面的版本是按字节方式读取数据的,缓冲字节数组大小无法权衡,太小了不足以存放一行数据时,将会读取到不完整的数据,产生乱码。我们使用字符流包装字节流,读取整行数据,改进如下。

服务端

public class Server3 {
    public static void main(String[] args) throws IOException {
        //开启一个TCP服务端,占用一个本地端口
        ServerSocket serverSocket = new ServerSocket(6666);
        //服务端循环不断地接受客户端的连接
        while (true) {
            Socket socket = null;
            try {
                //与单个客户端通信的代码放在一个try代码块中,单个客户端发生异常(断开)时不影响服务端正常工作
                System.out.println("server start...");
                //下面这行代码会阻塞,直到有客户端连接
                socket = serverSocket.accept();
                System.out.println("客户端" + socket.getRemoteSocketAddress() + "上线了");
                //从Socket中获得输入输出流,接收和发送数据
                PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
                BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String msg;
                while ((msg = socketBufferedReader.readLine()) != null) {
                    System.out.println("来自客户端的消息:" + msg);
                    String serverResponseMsg = "服务端收到了来自您的消息【" + msg + "】,并且探测到您的IP是:" + socket.getRemoteSocketAddress();
                    socketPrintWriter.println(serverResponseMsg);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //当与一个客户端通信结束后,需要关闭对应的socket
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

客户端

public class Client3 {
    public static void main(String[] args) {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        try {
            socket.connect(address, 2000);
            PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            BufferedReader bufferedInputReader = new BufferedReader(new InputStreamReader(System.in));
            String clientMsg;
            System.out.println("请输入消息:");
            while ((clientMsg = bufferedInputReader.readLine()) != null) {
                socketPrintWriter.println(clientMsg);
                String msgFromServer = socketBufferedReader.readLine();
                System.out.println("来自服务端的消息:" + msgFromServer);
                System.out.println("请输入消息:");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

版本4:实现多客户端与服务器通信

上面的例子中,只能实现一个客户端和服务端的通信。假如有多个客户端连接服务端,就只能等上一个客户端处理完毕,服务端重新通过accept()方法从队列中取出连接请求时才能处理。可以使用多线程的方式实现一个服务器同时响应多个客户端。

和上一个版本相比,客户端代码没有改动,服务端改进如下:

public class Server4 {
    public static void main(String[] args) throws IOException {
        //开启一个TCP服务端,占用一个本地端口
        ServerSocket serverSocket = new ServerSocket(6666);
        //服务端循环不断地接受客户端的连接
        System.out.println("server start...");
        while (true) {
            Socket socket;
            try {
                socket = serverSocket.accept();
                System.out.println("客户端" + socket.getRemoteSocketAddress() + "上线了");
                //为每一个客户端分配一个线程
                Thread workThread = new Thread(new Handler(socket));
                workThread.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

class Handler implements Runnable {
    private Socket socket;

    public Handler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //从Socket中获得输入输出流,接收和发送数据
            PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String msg;
            while ((msg = socketBufferedReader.readLine()) != null) {
                System.out.println("来自客户端" + socket.getRemoteSocketAddress() + "的消息:" + msg);
                String serverResponseMsg = "服务端收到了来自您的消息【" + msg + "】,并且探测到您的IP是:" + socket.getRemoteSocketAddress();
                socketPrintWriter.println(serverResponseMsg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //当与一个客户端通信结束后,需要关闭对应的socket
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

版本5:实现一个简单的网络聊天室

一个服务端支持多个客户端同时连接,每个客户端都能不断读取用户键入的消息,发送给服务器并由服务器广播到所有连到服务器的客户端,实现群聊的功能。

客户端

public class Client5 {
    public static void main(String[] args) {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        try {
            socket.connect(address, 2000);
            new Thread(new ClientHandler(socket)).start();
            PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader bufferedInputReader = new BufferedReader(new InputStreamReader(System.in));
            String clientMsg;
            System.out.println("请输入消息:");
            while ((clientMsg = bufferedInputReader.readLine()) != null) {
                socketPrintWriter.println(clientMsg);
                System.out.println("请输入消息:");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String msgFromServer;
            while ((msgFromServer = socketBufferedReader.readLine()) != null) {
                System.out.println("收到来自服务端的消息:" + msgFromServer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述客户端单独开启了一个线程来读取服务器响应的数据。主线程只负责接收客户端用户输入的数据,并发送给服务器。

服务端

public class Server5 {
    public static List<Socket> socketList = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        //开启一个TCP服务端,占用一个本地端口
        ServerSocket serverSocket = new ServerSocket(6666);
        //服务端循环不断地接受客户端的连接
        System.out.println("server start...");
        while (true) {
            Socket socket;
            try {
                socket = serverSocket.accept();
                socketList.add(socket);
                System.out.println("客户端" + socket.getRemoteSocketAddress() + "上线了");
                //为每一个客户端分配一个线程
                Thread workThread = new Thread(new ServerHandler(socket));
                workThread.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

class ServerHandler implements Runnable {
    private Socket socket;
    private BufferedReader socketBufferedReader;

    public ServerHandler(Socket socket) throws IOException {
        this.socket = socket;
        this.socketBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    }

    @Override
    public void run() {
        try {
            //从Socket中获得输入输出流,接收和发送数据
            String msg;
            while ((msg = readMsgFromClient()) != null) {
                System.out.println("收到来自客户端" + socket.getRemoteSocketAddress() + "的消息:" + msg);
                String massMsg = "客户端【" + socket.getRemoteSocketAddress() + "】说:" + msg;
                for (Socket socket : Server5.socketList) {
                    PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
                    socketPrintWriter.println(massMsg);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //当与一个客户端通信结束后,需要关闭对应的socket
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private String readMsgFromClient() {
        try {
            return socketBufferedReader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
            //如果捕获到异常,则将该客户端对应的socket删除
            Server5.socketList.remove(socket);
        }
        return null;
    }
}

上面的代码粗略地实现了一个网络聊天室的功能。使用传统的IO编程,比如BufferedReader的readLine()方法读取数据时,方法成功返回之前线程会被阻塞,因此要能同时处理多个客户端请求的话,服务端需要为每个客户端的socket连接启动一个线程单独处理与单个客户端的通信。同样的,客户端在读取服务端数据时同样会被阻塞,因此需要单独启动一个线程从流中去读取服务端的数据。

版本6:实现一对一聊天

上一个版本中,聊天室的客户端信息都是群发的,包括发送者也会收到服务器广播的消息。这里再次改进,发送者自己无需收到自己发出去的消息;并且发送者可以指定接受者的名称,实现一对一私聊。实现上述功能的关键就是在Server端记录每个客户端的信息。

消息格式约定

1)客户端发送的消息用冒号分割消息体。比如 消息类型**:消息接收人(用户名)😗*消息内容

2)消息类型有两种,login、chat ,分别表示登录消息和普通聊天消息;消息接收人可以是all或者具体的用户名,分别表示群聊消息和私聊对象

客户端

客户端连上服务端后先发送登录消息,再发送聊天消息,控制台输入示例如下:

请输入消息:
login:zhou
收到来自服务端的消息:用户【zhou】登录成功!
请输入消息:
chat:all:大家好哈
请输入消息:
chat:laowang:老王你好哈

代码:

public class Client6 {
    public static void main(String[] args) {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        try {
            socket.connect(address, 2000);
            new Thread(new ClientHandler6(socket)).start();
            PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader bufferedInputReader = new BufferedReader(new InputStreamReader(System.in));
            String clientMsg;
            System.out.println("请输入消息:");
            while ((clientMsg = bufferedInputReader.readLine()) != null) {
                socketPrintWriter.println(clientMsg);
                System.out.println("请输入消息:");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class ClientHandler6 implements Runnable {
    private Socket socket;

    public ClientHandler6(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String msgFromServer;
            while ((msgFromServer = socketBufferedReader.readLine()) != null) {
                System.out.println("收到来自服务端的消息:" + msgFromServer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端

public class Server6 {
    public static Map<String, Socket> userConnectionInfo = new HashMap<>();

    public static void main(String[] args) throws IOException {
        //开启一个TCP服务端,占用一个本地端口
        ServerSocket serverSocket = new ServerSocket(6666);
        //服务端循环不断地接受客户端的连接
        System.out.println("server start...");
        while (true) {
            Socket socket;
            try {
                socket = serverSocket.accept();
                System.out.println("客户端" + socket.getRemoteSocketAddress() + "上线了");
                //为每一个客户端分配一个线程
                Thread workThread = new Thread(new ServerHandler6(socket));
                workThread.start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

class ServerHandler6 implements Runnable {
    private Socket socket;
    private BufferedReader socketBufferedReader;

    public ServerHandler6(Socket socket) throws IOException {
        this.socket = socket;
        this.socketBufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    }

    @Override
    public void run() {
        try {
            //从Socket中获得输入输出流,接收和发送数据
            String msg;
            while ((msg = readMsgFromClient()) != null) {
                String[] split = msg.split(":");
                if (("login".equals(split[0]) && split.length != 2) || (!"login".equals(split[0])) && (split.length != 3)) {
                    response("消息格式错误,请用冒号分割,形如:消息类型:消息接收人(用户名):消息内容 ,消息类型有两种:login、chat;消息接收人可以是all或者具体的用户名");
                    continue;
                }

                String msgType = split[0];
                String userName = split[1];
                if ("login".equals(msgType)) {
                    if (Server6.userConnectionInfo.get(userName) == null) {
                        Server6.userConnectionInfo.put(userName, socket);
                        response("用户【" + userName + "】登录成功!");
                    } else {
                        response("用户【" + userName + "】已登录,无需重复登录");
                    }
                } else if ("chat".equals(msgType)) {
                    if ("all".equals(userName)) {
                        String senderName = getUname();
                        //群发消息
                        for (Map.Entry<String, Socket> entry : Server6.userConnectionInfo.entrySet()) {
                            Socket userSocket = entry.getValue();
                            if (userSocket == socket) {
                                continue;
                            }
                            PrintWriter socketPrintWriter = new PrintWriter(userSocket.getOutputStream(), true);
                            String sendMsg = "【" + senderName + "】对大家说:" + split[2];
                            socketPrintWriter.println(sendMsg);
                        }
                    } else {
                        if (Server6.userConnectionInfo.get(userName) == null) {
                            response("用户【" + userName + "】不在线");
                        } else {
                            Socket userSocket = Server6.userConnectionInfo.get(userName);
                            PrintWriter socketPrintWriter = new PrintWriter(userSocket.getOutputStream(), true);
                            String sendMsg = "【" + getUname() + "】对你说:" + split[2];
                            socketPrintWriter.println(sendMsg);
                        }
                    }
                } else {
                    response("消息类型错误,只支持login/chat");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //当与一个客户端通信结束后,需要关闭对应的socket
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private String getUname() {
        String uname = "";
        //找出该socket对应的用户名
        for (Map.Entry<String, Socket> entry : Server6.userConnectionInfo.entrySet()) {
            String userNameInfo = entry.getKey();
            Socket userSocket = entry.getValue();
            if (userSocket == socket) {
                uname = userNameInfo;
                break;
            }
        }
        return uname;
    }

    private void response(String msg) throws IOException {
        PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
        socketPrintWriter.println(msg);
    }

    private String readMsgFromClient() {
        try {
            return socketBufferedReader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
            //如果捕获到异常,则将该客户端对应的socket删除
            System.out.println("客户端" + socket.getRemoteSocketAddress() + "下线了");
            Server6.userConnectionInfo.remove(getUname());
        }
        return null;
    }
}

基于NIO的网络编程

讲完了传统的Java I/O编程,下面我们讲解如何使用Java NIO的方式实现非阻塞的网络通信。

之前的网络通信程序中,我们都是用的传统I/O方式,即阻塞式IO,顾名思义,在程序运行过程中常常会阻塞。比如在前文的例子中,当一个线程执行到ServerSocket的accept()方法时,该线程会一直阻塞,直到有了客户端连接才从accept()方法返回。再比如,当某个线程执行Socket的read()方法时,如果输入流中没有数据,则该线程会一直阻塞到读入了足够的数据才从read()方法返回。

Java NIO(New I/O)提供了非阻塞的实现方式。NIO也可以理解为non-blocking I/O的简称。所谓非阻塞I/O,就是当线程执行这些I/O方法时,如果某个操作还没有准备好,就立即返回,而不会因为某个操作还没就绪就进入线程阻塞状态,一直在那等。比如,当服务端的线程接收客户端连接时,如果没有客户端连接,就立即返回。再比如,当某个线程从输入流中读取数据时,如果流中还没有数据,就立即返回。或者如果输入流中没有足够的数据就直接读取现有的数据并返回。很明显,这种非阻塞的方式效率会更高。

有人说,那我用多线程方式处理阻塞式通信不香么?

是的,确实不香!前面我们的服务端代码演示了如何使用多线程同时处理多个客户端的连接。通常是主线程负责接收客户端的连接,每当主线程接收到一个客户端连接后,就把具体的数据交互任务交给一个单独的线程去完成,主线程继续接收下一个客户端的连接。

尽管使用多线程能够满足同时相应多个客户端的要求,但是这种方式有下列局限性:

1)如果服务端对于每个客户端的连接请求都单独开启一个线程来处理,那么在客户端数量庞大时,势必导致服务端开启的线程数过多。即使是使用线程池,也得设置池中放多少个线程,放多放少都是个问题。我们知道,JVM会为每个线程分配一个Java虚拟机栈,线程越多,系统开销就越大,线程的调度负担就越重,甚至会由于线程同步的复杂性导致线程死锁。

2)负责读写数据的工作线程很多时间浪费在I/O阻塞中,因为要等流中的数据准备好。这就会导致JVM频繁转让CPU的使用权,让阻塞状态的线程放弃CPU,让可运行状态的线程获得CPU使用权。

实践经验告诉我们,工作线程并不是越多越好。保持适当的线程数,可以提高服务器的并发性能。但是当线程数到达某个阈值,超出系统负荷,反而会导致并发性能降低,增大响应时间。Java NIO可以做到用一个线程来处理多个I/O操作,再也不要来一个客户端分配一个线程了,比如来10000个并发连接,可以只分配1个、50个或者100个线程来处理。

Java NIO 提供了支持阻塞/非阻塞I/O通信的类。下面介绍几个核心的类。

1)ServerSocketChannel

​ 可以看成是ServerSocket的替代类,既支持非阻塞通信,也支持阻塞式通信,同时也有负责接收客户端连接的accept()方法。每一个ServerSocketChannel对象都和一个ServerSocket对象关联。前面提到了,ServerSocketChannel没有public的构造器,只能通过它自身的静态方法open()来创建ServerSocketChannel对象。ServerSocketChannel是SelectableChannel的派生类。

2)SocketChannel

​ 可以看成是Socket的替代类,既支持非阻塞通信,又支持阻塞式通信。SocketChannel具有读数据的read(ByteBuffer dst)方法和写数据的write(ByteBuffer src)方法。SocketChannel也没有public类型的构造器,也是通过静态方法open()来创建自身的对象。每一个SocketChannel对象都和一个Socket对象关联。SocketChannel也是SelectableChannel的派生类。

​ SocketChannel提供了发送和接收数据的方法。

​ read(ByteBuffer dst):接收数据,并把接收到的数据存到指定的ByteBuffer中。假设ByteBuffer的剩余容量为n,在阻塞模式下,read()方法会争取读入n个字节,如果通道中不足n个字节,就会阻塞,直到读入了n个字节或者读到了输入流的末尾,或者出现了I/O异常。在非阻塞模式下,read()方法奉行能读多少就读多少的原则。不会等待数据,而是读取之后立即返回。可能读取了不足n个字节的数据,也可能就是0。如果返回-1则表示读到了流的末尾。

​ write(ByteBuffer src):发送数据,即把指定的ByteBuffer中的数据发送出去。假设ByteBuffer的剩余容量为n,在阻塞模式下,write()方法会争取输出n个字节,如果底层的网络输出缓冲区不能容纳n个字节,就会进入阻塞状态,直到输出n个字节,或者出现I/O异常才返回。在非阻塞模式下,write()方法奉行能输出多少就输出多少的原则,有可能不足n个字节,有可能是0个字节,总之立即返回。

3)Selector(选择器)

用一个线程就能处理多个的客户端连接的关键就在于Selector。Selector是SelectableChannel对象的多路复用器,用于判断channle上是否发生IO事件,所有希望使用非阻塞方式通信的Channel都需要注册到Selector上。Selector可以同时监控多个SelectableChannel的IO状态,即只要ServerSocketChannel或者SocketChannel向Selector注册了特定的事件,Selector就会监控这些事件是否发生。Selector为ServerSocketChannel监听连接就绪的事件,为SocketChannel监控连接就绪、读就绪、写就绪事件。Selector实例对象的创建通常是通过调用其静态的open()方法。

Selector有如下几种方法来返回I/O相关事件已经发生的SelectionKey的数目。

selectNow():该方法使用非阻塞的方式返回相关事件已经发生的SelectionKey的数目,如果没有任何事件发生,立即返回0。

select()和select(long timeout):该方法使用阻塞的方式。如果没有一个事件发生,就进入阻塞状态。直到有事件发生或者超出timeout设置的等待时间,才会正常返回。

使用Selector能够保证只在真正有读写事件发生时,才会进行读写,若通道中没有数据可用,该线程可以执行其它任务,不必阻塞。比如一个通道没有准备好数据时,可以将空闲时间用于其它通道执行IO操作。由于单个线程可以管理多个Channel的输入输出,避免了频繁的线程切换和阻塞,提升了I/O效率。实际上Netty的I/O线程NioEventLoop就是聚合了Selector(多路复用器),因此能够处理成千上万的客户端连接。

4)SelectionKey

​ ServerSocketChannel或者SocketChannel通过register()方法向Selector注册事件时,会返回一个SelectionKey对象,用来跟踪注册事件。Selector会一直监控与SelectionKey相关的事件。当一个SelectionKey对象被放到Selector对象的selected-keys集合中时,就表示与这个SelectionKey相关的事件发生了。

ServerSocketChannel及SocketChannel都继承自SelectableChannel类,该类及其子类可以委托Selector来监控它们可能发生的一些事件,这种委托过程就是事件注册。比如下列代码展示了ServerSocketChannel向Selector注册接收连接事件。

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT)

ServerSocketChannel只会发生一种事件,即SelectionKey.OP_ACCEPT,接受连接就绪事件。该事件的发生表明至少有一个客户端连接了,服务端可以通过accept()去接受这个连接了。

SocketChannel可以发生下列3种事件。

1)SelectionKey.OP_CONNECT,连接就绪事件,表示客户端和服务端已经成功建立连接。

2)SelectionKey.OP_READ,读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了。

3)SelectionKey.OP_WRITE,写就绪事件,表示可以向通道中写数据了。

默认情况下,所有的Channel都是阻塞模式的,要想使用非阻塞模式,可以通过下列方式设置

serverSocketChannel.configureBlocking(false);

此外,前面已经介绍了NIO的Buffer 、Channel相关概念,此处不再赘述。

下面我们就使用NIO的方式来编写网络通信程序的案例。

需求描述:实现客户端服务端的网络通信,客户端每发送一条消息,服务端就原样回复,并加一句前缀以示区分。

版本1:使用NIO的阻塞模式,并配以线程池方式

服务端:

public class NIOServer1 {
    private int port = 6666;
    private ServerSocketChannel serverSocketChannel;
    private ExecutorService executorService;

    public NIOServer1() throws IOException {
        executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4);
        serverSocketChannel = ServerSocketChannel.open();
        //允许地址重用,即关闭了服务端程序之后,哪怕立即再启动该程序时可以顺利绑定相同的端口
        serverSocketChannel.socket().setReuseAddress(true);
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        System.out.println("server started...");
    }

    public static void main(String[] args) throws IOException {
        new NIOServer1().service();
    }

    private void service() {
        while (true) {
            SocketChannel socketChannel;
            try {
                socketChannel = serverSocketChannel.accept();//阻塞
                executorService.execute(new NioHandler1(socketChannel));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

class NioHandler1 implements Runnable {
    private SocketChannel socketChannel;

    public NioHandler1(SocketChannel socketChannel) {
        this.socketChannel = socketChannel;
    }

    @Override
    public void run() {
        Socket socket = socketChannel.socket();
        System.out.println("接受到客户端的连接,来自" + socket.getRemoteSocketAddress());
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            String msg;
            while ((msg = reader.readLine()) != null) {
                System.out.println("客户端【" + socket.getInetAddress() + ":" + socket.getPort() + "】说:" + msg);
                writer.println(genResponse(msg));
                if ("bye".equals(msg)) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String genResponse(String msg) {
        return "服务器收到了您的消息:" + msg;
    }
}

客户端:

public class NIOClient1 {
    private SocketChannel socketChannel;

    public NIOClient1() throws IOException {
        socketChannel = SocketChannel.open();
        InetAddress localHost = InetAddress.getLocalHost();
        InetSocketAddress socketAddress = new InetSocketAddress(localHost, 6666);
        //采用阻塞模式连接服务器
        socketChannel.connect(socketAddress);
        System.out.println("与服务端连接成功!");
    }

    public static void main(String[] args) throws IOException {
        new NIOClient1().chat();
    }

    public void chat() {
        Socket socket = socketChannel.socket();
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader inputReader = new BufferedReader(new InputStreamReader(System.in));
            String msg;
            while ((msg = inputReader.readLine()) != null) {
                writer.println(msg);
                System.out.println("【服务器】说:" + reader.readLine());
                //如果输入bye,则终止聊天
                if ("bye".equals(msg)) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述案例中,我们使用的是ServerSocketChannel和SocketChannel的默认模式,即阻塞模式。为了能同时响应多个客户端,服务端依然是使用多线程的方式,只不过这次使用的是线程池。

版本2:使用NIO非阻塞模式

在非阻塞模式下,服务端只需启动一个主线程,就能同时完成3件事

1)接受客户端的连接

2)接收客户端发送的数据

3)向客户端发送响应数据

服务端会委托Selector来监听接收连接就绪事件、读就绪事件、写就绪事件,如有特定的事件发生,就处理该事件。

服务端:

public class NIOServer2 {
   private int port = 6666;
   private ServerSocketChannel serverSocketChannel;
   private Selector selector;
   private Charset charset = Charset.forName("UTF-8");

   public NIOServer2() throws IOException {
       selector = Selector.open();
       serverSocketChannel = ServerSocketChannel.open();
       serverSocketChannel.socket().setReuseAddress(true);
       //设置为非阻塞模式
       serverSocketChannel.configureBlocking(false);
       serverSocketChannel.socket().bind(new InetSocketAddress(port));
       System.out.println("server started...");
   }

   public static void main(String[] args) throws IOException {
       new NIOServer2().service();
   }

   private void service() throws IOException {
       serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
       while (selector.select() > 0) {
           Set<SelectionKey> selectionKeys = selector.selectedKeys();
           Iterator<SelectionKey> iterator = selectionKeys.iterator();
           while (iterator.hasNext()) {
               SelectionKey key = null;
               //处理每个SelectionKey的代码放在一个try/catch块中,如果出现异常,就使其失效并关闭对应的Channel
               try {
                   key = iterator.next();
                   if (key.isAcceptable()) {
                       doAccept(key);
                   }
                   if (key.isWritable()) {
                       sendMsg(key);
                   }

                   if (key.isReadable()) {
                       receiveMsg(key);
                   }
                   //从Selector的selected-keys集合中删除处理过的SelectionKey
                   iterator.remove();
               } catch (Exception e) {
                   e.printStackTrace();
                   try {
                       //发生异常时,使这个SelectionKey失效,Selector不再监控这个SelectionKey感兴趣的事件
                       if (key != null) {
                           key.cancel();
                           //关闭这个SelectionKey关联的SocketChannel
                           key.channel().close();
                       }
                   } catch (Exception ex) {
                       ex.printStackTrace();
                   }
               }
           }
       }
   }

   private void receiveMsg(SelectionKey key) throws IOException {
       ByteBuffer buffer = (ByteBuffer) key.attachment();
       SocketChannel socketChannel = (SocketChannel) key.channel();
       //创建一个ByteBuffer存放读取到的数据
       ByteBuffer readBuffer = ByteBuffer.allocate(64);
       socketChannel.read(readBuffer);
       readBuffer.flip();
       buffer.limit(buffer.capacity());
       //把readBuffer中的数据拷贝到buffer中,假设buffer的容量足够大,不会出现溢出的情况
       //在非阻塞模式下,socketChannel.read(readBuffer)方法一次读入多少字节的数据是不确定的,无法保证一次读入的是一整行字符串数据
       //因此需要将其每次读取的数据放到buffer中,当凑到一行数据时再回复客户端
       buffer.put(readBuffer);
   }

   private void sendMsg(SelectionKey key) throws IOException {
       ByteBuffer buffer = (ByteBuffer) key.attachment();
       SocketChannel socketChannel = (SocketChannel) key.channel();
       buffer.flip();
       String data = decode(buffer);
       //当凑满一行数据时再回复客户端
       if (data.indexOf("\r\n") == -1) {
           return;
       }
       //读取一行数据
       String recvData = data.substring(0, data.indexOf("\n") + 1);
       System.out.print("客户端【" + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort() + "】说:" + recvData);
       ByteBuffer outputBuffer = encode(genResponse(recvData));
       while (outputBuffer.hasRemaining()) {
           socketChannel.write(outputBuffer);
       }

       ByteBuffer temp = encode(recvData);
       buffer.position(temp.limit());
       //删除buffer中已经处理过的数据
       buffer.compact();

       if ("bye\r\n".equals(recvData)) {
           key.cancel();
           key.channel().close();
           System.out.println("关闭与客户端" + socketChannel.socket().getRemoteSocketAddress() + "的连接");
       }
   }

   private ByteBuffer encode(String msg) {
       return charset.encode(msg);//转为字节
   }

   private String decode(ByteBuffer buffer) {
       CharBuffer charBuffer = charset.decode(buffer);//转为字符
       return charBuffer.toString();
   }

   private void doAccept(SelectionKey key) throws IOException {
       ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
       SocketChannel socketChannel = ssc.accept();
       System.out.println("接受到客户端的连接,来自" + socketChannel.socket().getRemoteSocketAddress());
       //设置为非阻塞模式
       socketChannel.configureBlocking(false);
       //创建一个用于接收客户端数据的缓冲区
       ByteBuffer buffer = ByteBuffer.allocate(1024);
       //向Selector注册读、写就绪事件,并关联一个buffer附件
       socketChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, buffer);
   }

   private String genResponse(String msg) {
       return "服务器收到了您的消息:" + msg;
   }
}

上述例子中,服务端使用一个线程就完成了连接接收、数据接收、数据发送的功能。假设有许多的客户端连接,并且每此与客户端的数据交互都很多,势必会影响服务器的响应效率。如果把接收客户端连接的操作单独由一个线程处理,把接收数据和发送数据的操作交给另外的线程完成,就可以提高服务器的并发性能。读者可以尝试自己来实现一个主从线程模式的服务端程序,欢迎在在评论区留言哦!

下面再来看看客户端的实现。客户端和服务端的通信按照它们接收数据和发送数据的协调程度来区分可以分为同步通信异步通信。比如前面我们演示的传统阻塞式IO案例版本2就是同步通信,即每次客户端发送一行消息后,必须等到收到了服务端的响应数据后才能再发送下一行数据。而异步通信指的是数据的发送操作和接收操作互不影响,各自独立进行。异步通信使用非阻塞方式更容易实现。

比如下面这个NIOClient2类就是采用非阻塞方式来实现异步通信。在NIOClient2中定义了两个ByteBuffer:recvBuf和sendBuf。NIOClient2把用户从控制台输入的数据存放到sendBuf中,并将sendBuf中的数据发给服务器。把从服务器接收到的数据放在recvBuf中,并打印到控制台。由于接收用户控制台输入的线程和发送数据给服务器的线程都会使用sendBuf,因此加了synchronized进行同步。

客户端:

public class NIOClient2 {
    private ByteBuffer recvBuf = ByteBuffer.allocate(1024);
    private ByteBuffer sendBuf = ByteBuffer.allocate(1024);
    private Charset charset = Charset.forName("UTF-8");
    private SocketChannel socketChannel;
    private Selector selector;

    public NIOClient2() throws IOException {
        socketChannel = SocketChannel.open();
        InetAddress localHost = InetAddress.getLocalHost();
        InetSocketAddress socketAddress = new InetSocketAddress(localHost, 6666);
        //采用阻塞模式连接服务器
        socketChannel.connect(socketAddress);
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);
        System.out.println("与服务端连接成功!");
        selector = Selector.open();
    }

    public static void main(String[] args) throws IOException {
        NIOClient2 nioClient2 = new NIOClient2();
        Thread inputThread = new Thread() {
            @Override
            public void run() {
                nioClient2.receiveInput();
            }
        };

        inputThread.start();
        nioClient2.chat();
    }

    private void chat() throws IOException {
        //接收和发送数据
        socketChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
        while (selector.select() > 0) {
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = null;
                try {
                    key = iterator.next();
                    iterator.remove();
                    if (key.isWritable()) {
                        sendMsg(key);
                    }

                    if (key.isReadable()) {
                        receiveMsg(key);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    try {
                        //发生异常时,使这个SelectionKey失效,Selector不再监控这个SelectionKey感兴趣的事件
                        if (key != null) {
                            key.cancel();
                            //关闭这个SelectionKey关联的SocketChannel
                            key.channel().close();
                        }
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    private void receiveMsg(SelectionKey key) throws IOException {
        //接收服务端发来的数据,放到recvBuf中,如满一行数据,就输出,然后从recvBuf中删除
        SocketChannel channel = (SocketChannel) key.channel();
        channel.read(recvBuf);
        recvBuf.flip();
        String recvMsg = decode(recvBuf);
        if (recvMsg.indexOf("\n") == -1) {
            return;
        }
        String recvMsgLine = recvMsg.substring(0, recvMsg.indexOf("\n") + 1);
        System.out.print("【服务器】说:" + recvMsgLine);
        if (recvMsgLine.contains("bye")) {
            key.cancel();
            socketChannel.close();
            System.out.println("与服务器断开连接");
            selector.close();
            System.exit(0);
        }

        ByteBuffer temp = encode(recvMsgLine);
        recvBuf.position(temp.limit());
        //删除已经输出的数据
        recvBuf.compact();
    }

    private void sendMsg(SelectionKey key) throws IOException {
        //发送sendBuf中的数据
        SocketChannel channel = (SocketChannel) key.channel();
        synchronized (sendBuf) {
            //为取出数据做好准备
            sendBuf.flip();
            //将sendBuf中的数据写入到Channel中去
            channel.write(sendBuf);
            //删除已经发送的数据(通过压缩的方式)
            sendBuf.compact();
        }
    }

    private void receiveInput() {
        try {
            BufferedReader inputReader = new BufferedReader(new InputStreamReader(System.in));
            String msg;
            while ((msg = inputReader.readLine()) != null) {
                synchronized (sendBuf) {
                    sendBuf.put(encode(msg + "\r\n"));
                }
                //如果输入bye,则终止聊天
                if ("bye".equals(msg)) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ByteBuffer encode(String msg) {
        return charset.encode(msg);//转为字节
    }

    private String decode(ByteBuffer buffer) {
        CharBuffer charBuffer = charset.decode(buffer);//转为字符
        return charBuffer.toString();
    }
}

版本3:基于NIO重写网络聊天室的案例

我们对照传统IO方式的实现的简单网络聊天室,使用NIO来实现同样的功能。传统方式请参照前文的Server5/Client5。

服务端

public class NIOServer3 {
    private int port = 6666;
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private Charset charset = Charset.forName("UTF-8");

    public NIOServer3() throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().setReuseAddress(true);
        //设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        System.out.println("server started...");
    }

    public static void main(String[] args) throws IOException {
        new NIOServer3().service();
    }

    private void service() throws IOException {
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (selector.select() > 0) {
            for (SelectionKey key : selector.selectedKeys()) {
                selector.selectedKeys().remove(key);
                if (key.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = ssc.accept();
                    System.out.println("接受到客户端的连接,来自" + socketChannel.socket().getRemoteSocketAddress());
                    //设置为非阻塞模式
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }

                if (key.isReadable()) {
                    SocketChannel sc = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    String msg = "";
                    try {
                        while (sc.read(buffer) > 0) {
                            buffer.flip();
                            msg += charset.decode(buffer);
                        }
                        System.out.println("客户端【" + sc.getRemoteAddress() + "】说:" + msg);
                    } catch (IOException e) {
                        e.printStackTrace();
                        try {
                            //对某个Client对应的Channel读写发生异常时,使这个SelectionKey失效,Selector不再监控这个SelectionKey感兴趣的事件
                            if (key != null) {
                                key.cancel();
                                //关闭这个SelectionKey关联的SocketChannel
                                System.out.println("客户端【" + ((SocketChannel) key.channel()).socket().getRemoteSocketAddress() + "】下线了");
                                key.channel().close();
                            }
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }

                    if (msg.length() > 0) {
                        for (SelectionKey selectedKey : selector.keys()) {
                            Channel channel = selectedKey.channel();
                            //遍历Selector中的所有注册的Channel,如果是客户端的SocketChannel,则群发消息,并排除自己
                            if (channel instanceof SocketChannel && channel != sc) {
                                SocketChannel socketChannel = (SocketChannel) channel;
                                socketChannel.write(charset.encode("用户【" + sc.getRemoteAddress() + "】说:" + msg));
                            }
                        }
                    }
                }
            }
        }
    }
}

客户端

public class NIOClient3 {
    private ByteBuffer recvBuf = ByteBuffer.allocate(1024);
    private ByteBuffer sendBuf = ByteBuffer.allocate(1024);
    private Charset charset = Charset.forName("UTF-8");
    private SocketChannel socketChannel;
    private Selector selector;

    public NIOClient3() throws IOException {
        socketChannel = SocketChannel.open();
        InetAddress localHost = InetAddress.getLocalHost();
        InetSocketAddress socketAddress = new InetSocketAddress(localHost, 6666);
        //采用阻塞模式连接服务器
        socketChannel.connect(socketAddress);
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);
        System.out.println("与服务端连接成功!");
        selector = Selector.open();
    }

    public static void main(String[] args) throws IOException {
        NIOClient3 nioClient3 = new NIOClient3();
        Thread inputThread = new Thread() {
            @Override
            public void run() {
                nioClient3.sendInputMsg();
            }
        };

        inputThread.start();
        nioClient3.receiveMsg();
    }

    private void receiveMsg() throws IOException {
        socketChannel.register(selector, SelectionKey.OP_READ);
        while (selector.select() > 0) {
            for (SelectionKey key : selector.selectedKeys()) {
                try {
                    selector.selectedKeys().remove(key);
                    if (key.isReadable()) {
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        String msg = "";
                        while (sc.read(buffer) > 0) {
                            buffer.flip();
                            msg += charset.decode(buffer);
                        }
                        System.out.println(msg);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    try {
                        //发生异常时,使这个SelectionKey失效,Selector不再监控这个SelectionKey感兴趣的事件
                        if (key != null) {
                            key.cancel();
                            //关闭这个SelectionKey关联的SocketChannel
                            key.channel().close();
                        }
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

    private void sendInputMsg() {
        //接收键盘输入的消息并发送数据到服务器
        try {
            BufferedReader inputReader = new BufferedReader(new InputStreamReader(System.in));
            String msg;
            while ((msg = inputReader.readLine()) != null) {
                socketChannel.write(charset.encode(msg));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

相比传统的I/O,基于NIO的socket编程复杂度提高了很多,这也是我们学习Netty的原因之一:简化网络编程。

上面我们以网络通信的例子展示了传统的阻塞式IO和新的非阻塞式IO的区别,相信通过多个实际的代码例子,能让大家有个直观的感受,有效复习了一下Java的IO体系。在介绍Netty这款封装了Java NIO的框架之前,我们稍安勿躁,先补充一下NIO相关的理论知识

参见 https://bigbird.blog.csdn.net/article/details/101109105

  • 0
    点赞
  • 2
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页

打赏

程猿薇茑

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值