传统BIO网络编程知识点总结与Java NIO简介

原创 吴就业 112 0 2019-08-02

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/3eaeb703f86a4186be3849e8a6fbfe40

作者:吴就业
链接:https://wujiuye.com/article/3eaeb703f86a4186be3849e8a6fbfe40
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

传统BIO网络编程知识点总结与Java NIO简介

本篇作为《Netty高并发编程及性能调优实战经验分享》的补充。

本篇内容包括:

传统BIO编程知识点总结

下图为我用印象笔记所做的《Netty高并发编程与性能调优》思维导图。这是我对网络编程的总结吧,我算是很早就开始接触Socket编程的,在大学就学过Socket编程,虽然那时候学的是C#,但原理是一样的,不分语言。

我参加过中国软件杯,做的就是一个“同步手绘板”应用,使用Socket实现移动端和PC端同步绘制,虽然没得奖。专科实习的时候,做过一个监控摄像头设备的系统,比如远程控制拍照角度、自动拍照、获取电量等。从第一家公司辞职之后,也自己做了一个模仿微信的聊天系统。升本期间,了解到有NIO,Netty这些云云的存在,也跟风学了一把,后面毕设也用Netty实现一个恋爱APP的私密聊天功能的服务器。对BIO、NIO也算有点了解,最近为了项目的性能调优,也啃了好久的Netty的源码,虽然现在也只看懂了些皮毛,不过对本次性能调优还是非常有帮助的。

图片

我对传统BIO编程的一点总结,几个我认为最重要的知识点。

知识点一:Socket套接字复用池

不管任何一门高级语言,Socket编程都是服务端一个线程处理客户端的一个连接,因为读写都是阻塞的。为避免频繁的线程创建和消毁,都会使用线程池来实现线程的复用。对于BIO,一边会根据服务器硬件配置估算服务所能并发处理的最大连接数,据此设置线程复用池的大小。

图片

知识点二:内存缓存池

用于接收和发送字节数据的缓存区,也是用于避免频繁向系统申请和释放内存。对应的,Netty也有缓存池的概念,相对复杂些,分直接内存和堆内存两种,直接内存就是jvm堆外内存,不被jvm所管。

图片

知识点三:消息队列,解析数据包

有了内存缓存池,为啥还要有个消息队列。服务端读取到客户端发送过来的消息,可能不是一个完整的数据包,所以就需要对接收到的字节数据做解析,根据所使用的协议去解析字节数据,解析成一个个完整的数据包。

图片

知识点四:自定义通信协议

做为后端开发人员,我们最熟悉不过的就是HTTP协议了。使用Socket编写网络程序,我们可以自定义通信协议,这相当的好玩。自定义协议可以避免别人识别你的通信协议拦截数据包分析,也可对数据包进行加密传输。自定义协议的数据包体积小,可跟据业务需求修改。

图片

知识点五:心跳保活

TCP协议,需三次握手才建立连接,需四次挥手才释放连接。但避免不了的是客户端或是服务器意外断开连接,而对方并不知道。比如客户端蹭隔壁老王的Wifi被发现了…。如果客户端意外断开后,服务端还往客户端写消息,就会抛出异常。如果长时间没有读写数据,那线程一直会阻塞在那,占用线程。心跳包除了能检测连接是否可用外,还可实现断开自动重新连接。

在某些业务场景下,我们可以设置,当连接多久没有读写过数据时,服务端主动断开连接。这就是空闲检测。

图片

关于JAVA NIO

NIO与BIO的区别:

BIO: 同步阻塞式IO,服务器需要为每一个客户端创建一个线程处理连接。

NIO:同步非阻塞式IO,服务端可以使用一个或多个线程监听客户端的连接请求,并将连接注册到多路复用器Selector上,使用Selector轮询I/O就绪事件,当监听到有就绪事件时,才会为准备就绪的连接开启一个线程去处理。

NIO与传统的BIO模型相比,节省了为每个连接绑定一个线程的开销,支持同时与大量的客户端建立连接,而只受限于系统最大打开文件描述符的数量。

NIO

Channel:channel是一个通道,可以通过它读取和写入数据。对于网络编程而言,网络数据通过channel接受客户端发来的消息,也可以通过channel向客户端发送消息,channel是全双工的,对应的类分别为SocketChannel、ServerSocketChannel。

ServerSocketChannel: 用于监听TCP连接的通道,类似于ServerSocket。

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定服务端监听端口
serverSocketChannel.socket().bind(new InetSocketAddress(this.port), 1024);
// 监听客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();

SocketChannel:用于TCP网络连接的通道。实现与客户端数据传输。可以设置为非阻塞。类似于Socket。

Socket于Channel的区别:socket数据流是单向的,客户端与服务器实现双向通信需要一个Input流和一个Output流,一个用于接收数据,一个用于发送数据。而Channel是全双工的,即可以接受客户端发来的数据,也可以向客户端发送数据。

Selector:选择器,或多路复用器。用于轮询检查一个或多个NIO 通道(Channel)的状态。对于网络编程而言,Socket有四种状态,监听连接、连接准备就绪、读准备就绪、写准备就绪,对应的SelectorKey的取值如下。

SelectorKey:

OP_READ = 1 <<  0;     0000 0001
OP_WRITE = 1 << 2;     0000 0100
OP_CONNECT = 1 << 3;    0000 1000
OP_ACCEPT = 1 << 4;       0001 0000

I/O多路复用:I/O指的是网络I/O,多路指多个TCP连接(BIO: socket, NIO:channel),复用指的是只用一个或多个线程处理事件。总的来说,就是使用一个或多个线程处理多个TCP连接。不再像BIO的一个连接一个线程处理,NIO则可以只用一个线程处理所有连接,也可以使用n个线程处理所有连接。

Channel通过register方法与Selector多路复用器绑定,并指定自己感兴趣的事件,比如:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

绑定后生成一个SelectionKey,SelectionKey持有Channel。

通过轮询监听,通过Selector的select()方法可以选择已经准备就绪的通道,这些通道包含你注册的事件。比如你对读、写就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道和写事件已经就绪的那些通道。select方法是一个阻塞方法,至少有一个通道在你注册的事件准备就绪时,才会返回。返回结果为准备就绪的SelectionKey总数。

当监听到有事件准备就绪时,再通过Selector的selectedKeys()获取准备就绪的SelectionKey集合。通过遍历处理事件。处理完后需要移除,否则下次selectedKeys()还会重复拿到。


for (;;) {
    try {
        // 获取到之后返回总数,否则线程将处于阻塞状态
        int readyCount = this.selector.select();
        if (readyCount == 0){
            continue;
        }
        // 获取当前所有准备就绪的SelectionKey
        Set<SelectionKey> readyKeys = this.selector.selectedKeys();
        for (SelectionKey selectionKey : readyKeys) {
            // 需要注册新的感兴趣事件
            handleEvent(selectionKey);
            // 处理完要移除,否则下次selectedKeys还是能拿到
            readyKeys.remove(selectionKey);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

select方法底层通过调用native方法实现事件监听,在linux上,就是调用系统的epoll方法,网卡设备对应一个中断信号, 当网卡收到网络端的消息的时候会向CPU发起中断请求, 然后CPU处理该请求。通过驱动程序进而操作系统得到通知,系统再通知epoll,epoll通知用户代码。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

JVM垃圾回收大白话总结

一开始接触垃圾回收这个话题的时候,我最感兴趣的是,jvm是怎么判断一个对象是否被引用的?

Redis Cluster分布式集群搭建,及封装适配主从集群与Cluster集群的客户端组件

老项目一直在使用AWS的ElastiCache的Redis集群服务,为什么突然要自己部署集群呢。理由只有一个,贵了。对的,使用AWS的Redis集群服务,每个月要300$以上的费用,这成本是高了些,并且现在这个平台的并发量不高,缓存的数据量也只有1G多,确实贵了。

Netty高并发编程及性能调优实战经验分享

本篇内容介绍我们项目为何选择使用Netty,以及如何衡量一个服务的并发处理能力,再介绍如何做业务代码调优、针对不同业务场景的高并发性能调优。

RocketMQ集群搭建与监控后台部署

本篇介绍RocketMQ集群几种模式的搭建、配置,以及监控管理后台的部署。

公司项目中的代码为什么会烂得像一坨SHI

不要再抱怨你们公司项目的代码写得多烂,因为你不了解它的历史,你没有参与它的成长,你根本就不懂它是怎么长残的。

使用Sharding-JDBC实现分表,并让动态数据源支持Sharding-JDBC数据源

本篇介绍了笔者在一个业务场景下,通过各种优化手动都无法降低查询耗时的情况下,选择将表拆分多个,并使用Sharding-JDBC实现分表的查询,并介绍如何在已经实现了多数据源的项目中支持Sharding-JDBC数据源。