首页 > 编程语言 >Java NIO Socket学习

Java NIO Socket学习

时间:2023-03-19 19:12:23浏览次数:40  
标签:Java NIO socketChannel Socket socket log public 服务端 客户端

前言

这周学习尼恩编著的《Netty、Redis、ZooKeeper高并发实战》, 这本书写的很不错,通过十几个例子带领大家去体会高并发如何实现, 这周我看了最基础的JavaNOI部分,读书的时候好像明白了作者写的内容,但是又体会不深,非得自己动手写一些书上得例子,有时候还要改动下例子,才能体会深刻,得出自己得结论。下面我们进入例子演示。

Blocking IO

首先我们会来一个Blocking IO的例子,也就是同步阻塞方式,在Java中,默认创建的socket都是阻塞的。
先来看Server端代码, 写一个继承ServerSocket的类

@Slf4j
public class BlockReceiveServer  extends ServerSocket {
    public BlockReceiveServer() throws Exception
    {
        super(SERVER_PORT);
    }

    public void startServer() throws Exception
    {
        while (true)
        {
            // server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的

            log.debug("server listen at:" + SERVER_PORT);
            Socket socket = this.accept();
            /**
             * 我们的服务端处理客户端的连接请求是同步进行的, 每次接收到来自客户端的连接请求后,
             * 都要先跟当前的客户端通信完之后才能再处理下一个连接请求。 这在并发比较多的情况下会严重影响程序的性能,
             * 为此,我们可以把它改为如下这种异步处理与客户端通信的方式
             */
            // 每接收到一个Socket就建立一个新的线程来处理它
            new Thread(new Task(socket)).start();
        }
    }

this.accept()是阻塞的,也就是说没有请求来的时候,它是停在这里的,有请求来了后,我们起一个新的线程来出来它。 这里定义了一个Task来出来客户端请求。

 class Task implements Runnable
    {

        private Socket socket;

        private DataInputStream dis;

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

        @Override
        public void run()
        {
            try
            {
                dis = new DataInputStream(socket.getInputStream());
                log.debug("start receive");
                for (int i = 0;i < 1000 ;i++)
                {
                    dis.readUTF();
                }

                log.debug("finish receive" );
            } catch (Exception e)
            {
                e.printStackTrace();
            } finally
            {

                IOUtil.closeQuietly(dis);
                IOUtil.closeQuietly(socket);

            }
        }
    }

书中的例子是传送文件,我这里把它改为接收客户端传过来的文本,就是客户端写一千次,服务端读一千次,这个也是写死的,因为阻塞,没有读完,你是不能进入下一个环节的,不然会出错。

客户端代码

@Slf4j
public class BlockSendClient extends Socket
{
    private Socket client;
    private DataOutputStream outputStream;

    /**
     * 构造函数<br/>
     * 与服务器建立连接
     *
     * @throws Exception
     */
    public BlockSendClient() throws Exception
    {
        super(SOCKET_SERVER_IP
                , SERVER_PORT);
        this.client = this;
    }

    /**
     * 向服务端传输文件
     *
     * @throws Exception
     */
    public void sendFile() throws Exception
    {
        try
        {
            outputStream = new DataOutputStream(client.getOutputStream());

            for (int i = 0;i < 1000 ;i++)
            {
                outputStream.writeUTF(String.valueOf(i));
                outputStream.flush();
                Thread.sleep(1000);
            }
            log.debug("======== file transfer success ========");
        } catch (Exception e)
        {
            e.printStackTrace();
        } finally
        {
            IOUtil.closeQuietly(outputStream);
            IOUtil.closeQuietly(client);

        }
    }

客户端就是继承了socket,然后往socket里面发送1000次数字。
为了体现效果,我把客户端也让它多线程的去和服务端连接。 这里启动50个线程去和服务端连接,如果启动100个,会报“java.net.ConnectException: Connection refused: connect”, 但是你多启动几个客户端去连接,是不会有问题的。


 public static void main(String[] args)
    {
        for(int i =0;i<50;i++){
            new Thread(new Task()).start();
        }
    }

class Task  implements Runnable
{

    @Override
    public void run() {
        try
        {
            BlockSendClient client = new BlockSendClient(); // 启动客户端连接
            client.sendFile();
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

我们可以看到服务端的输出
image

我们打开多少客户端,那么服务端就必须要打开50倍的线程数来连接。 而线程能开的数量是有限制的,一个Java线程大约占1M的空间。 对于高并发,这种方式肯定是不可取的。

Java NIO

我们来看下如何用Java NOI 来写一个和上面一样的功能。先来看下服务端代码

@Slf4j
public class NioDiscardServer {

    public static void startServer() throws IOException {

        // 1、创建一个 Selector选择器
        Selector selector = Selector.open();

        // 2、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4、绑定监听端口
        serverSocket.bind(new InetSocketAddress(SERVER_PORT));
        log.info("server start success");

        // 5、将通道注册到选择器上,并注册的IO事件为:“接收新连接”
        SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //  sk.interestOps(SelectionKey.OP_ACCEPT) ;

        // 6、轮询感兴趣的I/O就绪事件(选择键集合)
        while (selector.select() > 0) {
            // 7、获取选择键集合
            Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();

            while (selectedKeys.hasNext()) {
                // 8、获取单个的选择键,并处理
                SelectionKey selectedKey = selectedKeys.next();

                // 9、判断key是具体的什么事件
                if (selectedKey.isAcceptable()) {
                    log.info("new connection coming" + selectedKey.channel());
                    ServerSocketChannel server = (ServerSocketChannel) selectedKey.channel();
                    SocketChannel socketChannel = server.accept();
                    // 10、若选择键的IO事件是“连接就绪”事件,就获取客户端连接

                    // 11、切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);

                    // 12、将该通道注册到selector选择器上
                    SelectionKey channleSk=        socketChannel.register(selector,
                            SelectionKey.OP_READ | SelectionKey.OP_WRITE  | SelectionKey.OP_CONNECT);


                }
                if (selectedKey.isWritable()) {
                    //log.info("write ready:" + selectedKey.channel());
                }
                if (selectedKey.isConnectable()) {
                    log.info("client connect success:" + selectedKey.channel());

                }
                if (selectedKey.isReadable()) {
                    log.info("read ready:" + selectedKey.channel());

                    // 13、若选择键的IO事件是“可读”事件,读取数据
                    SocketChannel socketChannel = (SocketChannel) selectedKey.channel();

                    // 14、读取数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int length = 0;
                    while ((length = socketChannel.read(byteBuffer)) > 0) {

                        byteBuffer.flip();

                        log.info(new String(byteBuffer.array(), 0, length));

                        byteBuffer.clear();

                    }

                    if(length == -1)
                    {
                        socketChannel.close();
                    }
                }

                // 15、移除选择键
                selectedKeys.remove();
            }
        }
        // 7、关闭连接
        serverSocketChannel.close();
    }

    public static void main(String[] args) throws IOException {
        startServer();
    }
}

客户端代码如下


@Slf4j
public class NioDiscardClient {
    public  void startClient(int clientNum) throws IOException {
        InetSocketAddress address =
                new InetSocketAddress(SOCKET_SERVER_IP,
                        SERVER_PORT);

        // 1、获取通道(channel)
        SocketChannel socketChannel = SocketChannel.open(address);
        // 2、切换成非阻塞模式
        socketChannel.configureBlocking(false);
        //不断的自旋、等待连接完成,或者做一些其他的事情
        while (!socketChannel.finishConnect()) {

        }

        log.info("client connect success");
        for(int i = 0;i<1000;i++) {
            // 3、分配指定大小的缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put((clientNum + "hello" + i).getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);

            ThreadUtil.sleepSeconds(1);
        }
        log.info("client write success");

        socketChannel.shutdownOutput();
        socketChannel.close();
    }

    public static void main(String[] args) throws IOException {
        for(int i =0;i<50;i++){
            new Thread(new NoiTask(i)).start();
        }
    }

}

class NoiTask  implements Runnable
{
    private int clientNum;
    public NoiTask(int clientNum)
    {
        this.clientNum = clientNum;
    }

    @Override
    public void run() {
        try
        {
            NioDiscardClient client = new NioDiscardClient(); // 启动客户端连接
            client.startClient(clientNum); // 传输文件
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

代码这里不解释了。 这里服务端和客户端不需要约定多少内容要发送,
服务端有段代码需要注意
while ((length = socketChannel.read(byteBuffer)) > 0),这段很容易就退出循环,因为read是异步的,很容易就返回0, 让循环中断, 开始的时候socketChannel.close()没有加上条件,服务端打印出几次后,就close了,客户端就再写不进去了。报“java.io.IOException: An established connection was aborted by the software in your host machine”,因为服务端关闭了。
服务端的输出我们可以看出,它只启动了一个main线程,不论多少个客户端连接过来。
image

我再用CMD启动多个进行客户端进行测试的时候,发现它会启用300多M的内存,后面多加几个客户端,它也不会增长,保持再这个数字上面。 前面的例子中,内存使用的要小,但是增加客户端后,内存使用量会增加,但是也不是很明显。 这里面为什么会用这么多内存,我不是太明白,也许代码有什么缺陷在这里,暂时不细究了。

总结

我在使用NIO的时候,发现它在单机测试性能的时候不比传统的阻塞式的优秀,甚至还慢一些。它的应用场景是在量大的时候,高并发的时候。具体NIO的原理我也解释不清楚,只能从应用的层面来用例子实践一下,后面看能不能再深入下它的原理,书中讲述的很好,但是我只能说大致理解,谈不上深入。

标签:Java,NIO,socketChannel,Socket,socket,log,public,服务端,客户端
From: https://www.cnblogs.com/dk168/p/17233739.html

相关文章

  • Java中的恍然大悟
    Java中的恍然大悟数组与集合最大的区别数组里既可以装8钟基本数据类型,还可以装包装类型。集合里只能装包装类型集合工具类给你封装了排序方法java.util.Collections......
  • java文本获取
     使用正则方式提取文本中间内容获取文本中间(单次)参数1:文本参数2:文本前参数3:文本后返回一个StringpublicstaticStringgetSubString(Stringtext,Stringleft......
  • 【JavaScript】50_终篇_编程进阶与BOM编程概览(3k字+)
    12、节点的复制使用cloneNode()方法对节点进行复制时,它会复制节点的所有特点包括各种属性这个方法默认只会复制当前节点,而不会复制节点的子节点可以传递一个true作为参数,......
  • Java多线程开发CompletableFuture的应用
    ​做Java编程,难免会遇到多线程的开发,但是JDK8这个CompletableFuture类很多开发者目前还没听说过,但是这个类实在是太好用了,了解它的一些用法后相信你会对它爱不释手(呸渣男,......
  • java的数据类型
    2023-03-19java是强类型语言要求变量的使用严格符合规定,所有的变量需要先定义、后才能使用java的数据类型分为两大类1、基本类型 2、引用类型引用数据类型包括类......
  • 关于java.lang.ThreadDeath线程发生场景及模拟代码测试
    当调用stop()方法时会发生两件事:1.即刻停止run()方法中剩余的全部工作,包括在catch或finally语句中,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导......
  • java——Zookeeper学习——zk实现分布式锁了解
                   ......
  • Java基础字符串练习
    ​定义一个方法,把int数组中的数据按照指定的格式拼接成一个字符串返回,调用该方法,并在控制台输出结果。要求:1、如果传递的参数为空,返回null2、如果传递的数组元素个数为0......
  • 【Java】Allatori代码加密
    一般来说我们在写Java程序时都会使用Maven(或Gradle)做依赖集成。这过程中Maven(或Gradle)作为编译黑盒,输入源码而输出字节码。但我们也知道Java程序是可以通过反编译工具看到源......
  • java——Zookeeper学习——入门学习
    学习之前看了2个B站教程:   1、千峰:https://www.bilibili.com/video/BV1Ph411n7Ep/?vd_source=79bbd5b76bfd74c2ef1501653cee29d6   2、黑马:https://www.bili......