Java NIO 实现服务端和客户端的通信示例

温馨提示:阅读本示例前首先需要对 Java NIO 的三大核心有一定了解

  • channel (通道
  • buffer (缓冲区
  • selector(选择器
    可以先看看 Java NIO Tutorial

服务端

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

public class NIOServer {
    static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);

    public static void main(String[] args) throws IOException {
        int port = 9090;
        try {
            //1. 获取通道
            ServerSocketChannel ssChannel = ServerSocketChannel.open();
            //设置为非阻塞才能注册到选择器。FileChannel不能切换到非阻塞模式
            ssChannel.configureBlocking(false);

            //2. 绑定端口
            ssChannel.bind(new InetSocketAddress(port));

            //4.  将通道注册到选择器上
            Selector selector = Selector.open();
            ssChannel.register(selector, SelectionKey.OP_ACCEPT);

            LOGGER.info("NIO服务已启动,绑定端口号为:" + port);
            //5. 通过轮询的方式,获取准备就绪的事件
            while (selector.select() > 0) {   //该方法会一直阻塞,直到至少有一个 SelectionKey 准备就绪

                //6. 获取当前选择器中所有注册的 SelectionKey
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();

                    if (key.isAcceptable()) {
                        handleAccept(key);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }

                    //当SelectionKey 任务完成后需要移除,否则会一直执行这个key。
                    iterator.remove();
                }
            }
            ssChannel.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static void handleAccept(SelectionKey key) {
        try {
            LOGGER.info("接收就绪!");

            //7. 获取接受状态准备就绪的 selectionKey
            //调用accept 方法获取通道
            SocketChannel sChannel = ((ServerSocketChannel) key.channel()).accept();

            Selector selector = key.selector();
            //8. 将 sChannel 设置为非阻塞的
            sChannel.configureBlocking(false);
            //9. 将该通道注册到选择器上,让选择器能够监听这个通道
            sChannel.register(selector, SelectionKey.OP_READ);
            LOGGER.info("当前接收的连接: " + sChannel);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static void handleRead(SelectionKey key) throws IOException {
        LOGGER.info("读取就绪!");
        //10. 获取 读状态 准备就绪的 selectionKey
        SocketChannel sChannel = (SocketChannel) key.channel();
        //创建缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1032);
        byte[] bytes = new byte[1024];

        int read = 0;
        StringBuilder receiveMessage = new StringBuilder();
        int temp;
        boolean statusSent = false;

        //从通道中循环读取数据写入缓冲区,直到达到流的末尾,也就是客户端主动关闭输出流(SocketChannel.shutdownOutput()),或者关闭通道。但由于稍后客户端还需要从服务端接收数据,因此客户端还不能关闭通道。
        while ((read = sChannel.read(buf)) != -1) {
            //该方法会让缓冲区切换到读取状态,即①ByteBuff.limit = ByteBuff.position; ②ByteBuff.position = 0;
            buf.flip();
            receiveMessage.append(StandardCharsets.UTF_8.newDecoder().decode(buf));
            //该方法让缓冲区清空,并准备下一次写入,即①ByteBuff.limit = ByteBuff.capacity; ②ByteBuff.position = 0;
            buf.clear();
        }
        //执行至此时,地址接收完毕
        String tpUrl = receiveMessage.toString();
        LOGGER.info("长度:[{}],完整url: [{}]", tpUrl.length(), tpUrl);

        URL url = new URL(tpUrl);
        InputStream inputStream = null;
        int responseCode = -1;
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setConnectTimeout(10000);
        try {
            inputStream = con.getInputStream();
            responseCode = con.getResponseCode();
        } catch (IOException e) {
            String format = String.format("图片资源不存在或网络不通!图片地址:%s,异常信息:%s,状态码:%d", tpUrl, e.getMessage(), responseCode);
            e.printStackTrace();
            LOGGER.error(format);
            //返回异常信息。我在这里自定义了服务器返回的第一个Int类型数据标识着当前请求成功与否1:成功,0:失败。
            buf.putInt(0);
            buf.put(format.getBytes(StandardCharsets.UTF_8));
            buf.flip();
            sChannel.write(buf);
            buf.clear();
            if (inputStream != null) {
                inputStream.close();
            }
            //关闭通道,告知客户端写入已达到末尾,让它不再需要等待服务端。
            sChannel.close();
            return;
        }

        while ((temp = inputStream.read(bytes)) != -1) {
            if (!statusSent) {
                //发送成功标识
                buf.putInt(1);
                statusSent = true;
            }
            //写入图片数据到缓冲区
            buf.put(bytes, 0, temp);
            //反转一下,准备读取
            buf.flip();
            sChannel.write(buf);
            //清空缓冲区,准备下一次写入
            buf.clear();
        }

        LOGGER.info("返回结束.");
        if (inputStream != null) {
            inputStream.close();
        }
        //关闭通道
        sChannel.close();
        LOGGER.info("通道已关闭");
        return;
    }
}

客户端

import java.io.*;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public class NIOClient {
    public static void main(String[] args) throws FileNotFoundException {
        nioClient();
    }

    public static void nioClient() {
        try {

            String[] sends = {
                    "https://pic.cnblogs.com/avatar/1345194/20180308181944.png",
                    "https://pic.cnblogs.com/avatar/1345194/20180308181944.png"
            };

            for (int i = 0; i < sends.length; i++) {
                File file = new File("D:\\test\\IOnNio\\" + i + ".png");
                FileOutputStream outputStream = new FileOutputStream(file);
                output(outputStream, sends[i]);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static void output(OutputStream out, String url) throws IOException {
        //1. 获取通道
        SocketChannel sChannel = SocketChannel.open();
        //设置 10s 超时时间
        sChannel.socket().connect(new InetSocketAddress("127.0.0.1", 9090), 10000);

        //1.2 将阻塞的套接字 变为 非阻塞 的
        sChannel.configureBlocking(false);

        //2. 创建指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("发送:" + url);
        sChannel.write(ByteBuffer.wrap(url.getBytes()));
        // 主动关闭输出但不关闭通道,通知服务端当前发送请求已经完毕,等待服务端响应
        sChannel.shutdownOutput();

        int len = 0;
        int count = 0;
        int anInt = -1;
        boolean statusRead = false;
        //循环读取服务端返回到通道的数据,直到达到末尾
        while ((len = sChannel.read(buf)) != -1) {
            buf.flip();
            if (!statusRead && buf.limit() > 0) {
                //读取第一位标识成功或失败的 Int 值
                anInt = buf.getInt();
                statusRead = true;
            }
            if (anInt == 1) {
                if (len > 0) {
                    count += buf.limit() - buf.position();
                }
                //输出到本地磁盘
                out.write(buf.array(), buf.position(), buf.limit() - buf.position());

            } else if (anInt == 0) {
                //失败
                System.out.println("服务端请求图片资源失败!");
                System.out.println(new String(buf.array(), buf.position(), buf.limit() - buf.position(), StandardCharsets.UTF_8));
            }
            buf.clear();
        }
        out.close();
        System.out.println("返回最终大小:" + count);
        //关闭通道
        sChannel.close();
    }
}

日志依赖与配置

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.21</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.21</version>
        </dependency>
#log4j.properties
log4j.rootLogger=DEBUG, f, console
log4j.appender.f=org.apache.log4j.DailyRollingFileAppender
log4j.appender.f.File =logs/pic-download.log
log4j.appender.f.Append = true
log4j.appender.f.DatePattern='.'yyyy-MM-dd
log4j.appender.f.layout=org.apache.log4j.PatternLayout
log4j.appender.f.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p [%c{1}]: %m%n

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p [%c{1}]: %m%n

posted on 2021-11-23 13:56  Deemoo  阅读(295)  评论(0编辑  收藏  举报

导航