高性能网络编程socket之BIO与NIO区别

对于SSL/TLS协议,如果要每个开发者都自己去实现显然会带来不必要的麻烦,正是为了解决这个问题为广大开发者提供了Java安全套接字扩展——JSSE,它包含了实现Internet安全通信的一系列包的集合,是SSL和TLS的纯Java实现,同时它是一个开放的标准,每个公司都可以自己实现JSSE,通过它可以透明地提供数据、服务器认证、信息完整性等功能,就像使用普通的套接字一样使用安全套接字,大大减轻了开发者的负担,使开发者可以很轻松将SSL协议整合到程序中,并且JSSE能将安全隐患降到了最低点。

在用JSSE实现SSL通信过程中主要会遇到以下类和接口,由于过程中涉及到加解密、密钥生成等运算的框架和实现,所以也会间接用到JCE包的一些类。如图为JSSE接口的主要类图:

通信核心类——SSLSocket和SSLServerSocket。对于使用过socket进行通信开发的朋友比较好理解,它们对应的就是Socket与ServerSocket,只是表示实现了SSL协议的Socket和ServerSocket,同时它们也是Socket与ServerSocket的子类。SSLSocket负责的事情包括设置加密套件、管理SSL会话、处理握手结束时间、设置客户端模式或服务器模式。

③ SSL会话——SSLSession。安全通信握手过程需要一个会话,为了提高通信的效率,SSL协议允许多个SSLSocket共享同一个SSL会话,在同一个会话中,只有第一个打开的SSLSocket需要进行SSL握手,负责生成密钥及交换密钥,其余SSLSocket都共享密钥信息。

④ SSL上下文——SSLContext。它是对整个SSL/TLS协议的封装,表示了安全套接字协议的实现。主要负责设置安全通信过程中的各种信息,例如跟证书相关的信息。并且负责构建SSLSocketFactory、SSLServerSocketFactory和SSLEngine等工厂类。

⑤ SSL非阻塞引擎——SSLEngine。假如你要进行NIO通信,那么将使用这个类,它让通过过程支持非阻塞的安全通信。

⑥ 密钥管理器——KeyManager。此接口负责选择用于证实自己身份的安全证书,发给通信另一方。KeyManager对象由KeyManagerFactory工厂类生成。

⑧ 密钥证书存储设施——KeyStore。这个对象用于存放安全证书,安全证书一般以文件形式存放,KeyStore负责将证书加载到内存。

通过上面这些类就可以完成SSL协议的安全通信了,在利用SSL/TLS进行安全通信时,客户端跟服务器端都必须要支持SSL/TLS协议,不然将无法进行通信。而且客户端和服务器端都可能要设置用于证实自己身份的安全证书,并且还要设置信任对方的哪些安全证书。

关于身份认证方面有个名词叫客户端模式,一般情况客户端要对服务器端的身份进行验证,但是无需向服务器证实自己的身份,这样不用向对方证实自己身份的通信端我们就说它处于客户模式,否则成它处于服务器模式。SSLSocket的setUseClientMode(Boolean mode)方法可以设置客户端模式或服务器模式。

使用BIO模式实现SSL通信除了对一些证书密钥生成外,只需使用JDK自带的SSLServerSocket和SSLSocket等相关类的API即可实现,简洁直观。

一般而言作为服务器端必须要有证书以证明这个服务器的身份,并且证书应该描述此服务器所有者的一些基本信息,例如公司名称、联系人名等。证书由所有人以密码形式签名,基本不可伪造,证书获取的途径有两个:一是从权威机构购买证书,权威机构担保它发出的证书的真实性,而且这个权威机构被大家所信任,进而你可以相信这个证书的有效性;另外一个是自己用JDK提供的工具keytool创建一个自我签名的证书,这种情况下一般是我只想要保证数据的安全性与完整性,避免数据在传送的过程中被窃听或篡改,此时身份的认证已不重要,重点已经在端与端传输的秘密性上,证书的作用只体现在加解密签名。

另外,关于证书的一些概念在这里陈述,一个证书是一个实体的数字签名,这个实体可以是一个人、一个组织、一个程序、一个公司、一个银行,同时证书还包含这个实体的公共钥匙,此公共钥匙是这个实体的数字关联,让所有想同这个实体发生信任关系的其他实体用来检验签名。而这个实体的数字签名是实体信息用实体的私钥加密后的数据,这条数据可以用这个实体的公共钥匙解密,进而鉴别实体的身份。这里用到的核心算法是非对称加密算法。

SSL协议通信涉及密钥储存的文件格式比较多,很容易搞混,例如.ssl.truststore指定了truststore文件,那么信任管理器将去jre路径下的lib/security目录寻找这个文件作为信任存储器;②如果没设置①中的系统属性,则去寻找一个%java_home%/lib/security/jssecacerts文件作为信任存储器;③如果jssecacerts不存在而cacerts存在,则cacerts作为信任存储器。

至此,一个利用JSSE实现BIO模式的SSL协议通信的例子已完成。

在jdk1.5之前,由于互联网还没快速发展起来,对于常见的应用使用BIO模式即可满足需求,而这时jdk的JSSE接口也仅仅只是提供了基于流的安全套接字,但随着网络的发展,BIO模型明显已经不足以满足一些高并发多连接接入的场景,体现在机器上就是要不同的线程模型以至于能最大程度地压榨计算器的运算,于是此时引入了NIO模式,原来基于流的阻塞模式IO只需使用SSLServerSocket和SSLSocket即可完成SSL通信,而JDK中对于NIO模式并没有提供与之对应的“SSLServerSocketChannel”和“SSLSocketChannel”,这是由NIO模式决定的,很难设计一个“SSLServerSocketChannel”类与Selector交互,强行地引入将带来更多的问题,这更像解决一个问题引入了三个问题,并且还会导致API更加复杂,另外Nio细节也不适合屏蔽,它应该由应用开发层去控制。所有的这些都决定了jdk不会也不能有NIO安全套接字。

jdk1.5为了支持NIO模式的SSL通信,引入了SSLEngine引擎,它负责了底层ssl协议的握手、加密、解密、关闭会话等等操作,根据前面SSL\TLS协议章节我们知道SSL协议在握手阶段会有十三个步骤,在握手过程中不会有应用层的数据传输,只有在握手认证完成后双方才会进行应用层数据交换。大致把握手分为四阶段,①客户端发送hello消息;②服务端响应hello消息且发送附带的认证消息;③客户端向客户端发送证书和其他认证消息;④完成握手。

SSLEngine在握手过程中定义了五种HandshakeStatus状态,【NEED_WRAP、NEED_UNWRAP、NEED_TASK、FINISHED、NOT_HANDSHAKING】,通过他们实现协议通信过程中状态管理,按照四个阶段其中的状态是这样转换的,刚开始它的状态为NEED_UNWRAP,表示等待解包,读取客户端数据并解包后,把状态置为NEED_WRAP,表示等待打包,打包完向客户端响应数据后状态又重置为NEED_UNWRAP,如此切换直至握手完成时状态被置为FINISHED,表示握手已经完成,此后状态置为NOT_HANDSHAKING,表示已经不在握手阶段了。另外还有一个NEED_TASK状态表示SSLEngine有额外的任务需要执行,而且这些任务都是比较耗时或者可能阻塞的,例如访问密钥文件、连接远程证书认证服务、密钥管理器使用何种认证方式作为客户端认证等等操作。为了保证NIO特性,这些操作不能直接由当前线程操作,当前线程只会把状态改为NEED_TASK,后面处理线程会交由其他线程处理。

看看程序是如何使用nio模式进行ssl通信的,主要看服务端如何实现。

根据程序大致说明程序过程,①创建用于非阻塞通信的主要对象ServerSocketChannel和Selector、绑定端口、注册接收事件;②创建SSL上下文,此过程主要是根据前面创建好的密钥存储器tomcat.jks和client.jks去创建密钥管理器和信任管理器,并通过密钥管理器和信任管理器去初始化SSL上下文;③创建SSL引擎,主要通过SSL上下文创建SSL引擎,并将它设为不验证客户端身份;④创建缓冲区,使用SSL协议通信的过程中涉及到四个缓冲区,如下图,netInData表示实际从网络接收到的字节流,它是包含了SSL协议和应用数据的字节流,通过SSLEngine引擎进行认证解密等处理后的应用可直接使用的数据则用appInData表示,同样地,应用层要传递的数据为appOutData,而经过SSLEngine引擎认证加密处理后放到网络中传输的字节流则为netOutData;⑤接下去开始监听处理客户端的连接请求,一旦有可接受的连接则会先进行SSL协议握手,完成握手后才能进行传输,即对通道的读写操作。

握手操作是一个比较复杂的过程,必须要保证握手完成后才能进行应用层数据交换,所以这里使用一个while循环不断做握手操作直到完成。前面已经介绍了握手阶段会有五种状态,【NEED_WRAP、NEED_UNWRAP、NEED_TASK、FINISHED、NOT_HANDSHAKING】,由于SSL协议握手的报文都由SSLEngine引擎自动生成,所以我们只需对不同状态做不同操作即可,例如,NEED_UNWRAP状态则调用unwrap方法,NEED_WRAP则调用wrap方法,NEED_TASK则使用其他线程处理委托任务,握手报文自动由这些方法完成,当握手完成后状态则被置为FINISHED,接着状态变为NOT_HANDSHAKING,表示已经不在握手阶段了,已经可以进行应用层通信了,此时整个SSL握手结束。

应用层安全通信过程其实也是靠SSLEngine引擎的unwrap和wrap方法对数据进行加解密并且对通信双方进行认证,例如应用层读操作是将netInData和appInData传入unwrap方法,处理后的appInData即为应用需要的数据,而写操作则是将appOutData和netOutData传入wrap方法,处理后的netOutData即为传输给对方的数据。

至此,通过在网络与应用直接增加一个SSLEngine引擎层,则实现了安全通信,并且使用了NIO模式让服务端拥有更加出色的处理性能。

版权声明:本文为博主原创文章,未经博主允许不得转载。 /wy0123/article/details/

NIO比BIO效率高,主要原因是什么呢?
网上大多给出了两者的区别,可是具体效率高在哪里呢。

首先我们看一下各自的特点

  1. 取得socket后,将这个socket分给一个线程去处理。此时socket需要等待有效的请求数据到来后,才可以真正开始处理请求。
  2. socket交给线程后,这时socketServer才可以接收下一个连接请求。
  3. 获得连接的顺序是和客户端请求到达服务器的先后顺序相关。
  1. 基于事件驱动,当有连接请求,会将此连接注册到多路复用器上(selector)。
  2. 在多路复用器上可以注册监听事件,比如监听accept、read
  3. 通过监听,当真正有请求数据时,才来处理数据。
  4. 不会阻塞,会不停的轮询是否有就绪的事件,所以处理顺序和连接请求先后顺序无关,与请求数据到来的先后顺序有关

  • BIO一个连接,一个线程,非http请求,有可能只连接不发请求数据,此时线程是无用浪费的。
  • BIO处理依赖于连接建立;NIO处理依赖于请求数据的到来。导致执行顺序不同。

      BIO:连接请求来,建立socket,等待请求数据到来(t1),处理时间(t2)
      NIO:连接请求来,注册到selector,设置读监听,等待请求数据(t1),处理时间(t2)
      此时,两者用时皆为t1+t2,没有区别
  • 第一个请求,等待请求数据(10),处理时间(1)
    第二个请求,等待请求数据(1),处理时间(2)
    BIO:用时 10+1+1+2=14,第1个执行完用时10+1,等待第一个执行完处理第2个,用时1+2
    NIO:用时 1+2+7+1=11, 第二个数据先到,时间 1+2,此时第一个需要等时为10秒,还没到,还需等待7秒,时间为7+1 第一个请求,等待请求数据(10),处理时间(1)
    第二个请求,等待请求数据(1),处理时间(2)
    BIO:用时 10+1+2=13,等待第1个请求10,交给工作线程一处理,此时同时接受第2个,等待1秒,处理时间2秒,此间线程一处理时间为一秒,在线程二结束之前就已经结束
    NIO:用时 1+2+7+1=11,第二个数据先到,时间 1+2,此时第一个还没到,还需等待7秒,时间为7+1
    如果两个请求顺序相反,则bio和nio一样,都是11秒
    由此可见由于阻塞等待机制的不同,导致效率不同,主要优化点为,不必排队等待,先到先处理,就有可能效率高一点。
  • BIO如果想要处理并发请求,则必须使用多线程,一般后端会用线程池来支持
    NIO可以使用单线程,可以减少线程切换上下文的消耗。
    但是虽然单线程减少了线程切换的消耗,但是处理也变为线性的,也就是处理完一个请求,才能处理第二个。
    这时,有这么两个场景:

    1. 后端是密集型的计算,没有大量的IO操作,比如读些文件、数据库等
    2. 后端是有大量的IO操作。

    NIO单线程则比较有优势, 理由是虽然是单线程,但是由于线程的计算是并发计算,不是并行计算,说到底,计算压力还是在CPU上,一个线程计算,没有线程的多余消耗,显然比NIO多线程要高效。BIO则必为多线程,否则将阻塞到天荒地老,但多线程是并发,不是并行,主要还是依靠CPU的线性计算,另外还有处理大量的线程上下文。
    如果为第二种场景,多线程将有一定优势,多个线程把等待IO的时间能平均开。此时两者区别主要取决于以上分析的处理顺序了,显然NIO要更胜一筹。

NIO在接收请求方式上,无疑是要高效于BIO,原因并非是不阻塞,我认为NIO一样是阻塞的,只是方式不同,先来的有效请求先处理,先阻塞时间短的。此时间可用于等待等待时间长的。
在处理请求上,NIO和BIO并没有什么不同,主要看线程池规划是否和理。NIO相对BIO在密集型计算的模型下,可以用更少的线程,甚至单线程。

        经典的网络服务的设计如下图,在每个线程中完成对数据的处理:


        但这种模式在用户负载增加时,性能将下降非常的快。我们需要重新寻找一个新的方案,保持数据处理的流畅,很显然,事件触发机制是最好的解决办法,当有事件发生时,会触动handler,然后开始数据的处理。

        //来一个事件 第一次触发一个accepter线程     //将SelectionKey绑定为本Handler 下一步有事件触发时,将调用本类的run方法。     //同时将SelectionKey标记为可读,以便读取。     //激活线程池 处理这些request

        将数据读出后,可以将这些数据处理线程做成一个线程池,这样,数据读出后,立即扔到线程池中,这样加速处理速度:

        一个高性能的Java网络服务机制就要形成,激动人心的集群并行计算即将实现。

我要回帖

更多关于 高性能网络编程 的文章

 

随机推荐