第一章 Netty——异步和事件驱动

第一章 Netty——异步和事件驱动

1.1 Java网络编程

阻塞I/O示例

package J2Learn;

import java.io.BufferedReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class App 
{
    public static void main( String[] args ) throws Exception{
        // 创建服务器端,用以监听指定端口上的连接请求
        ServerSocket serverSocket = new ServerSocket(portNumber);
        // 对accept方法的调用将被阻塞,直到一个连接被建立
        Socket clientSocket = serverSocket.accept();
        // 这些流对象派生于该套接字的流对象
        BufferedReader in = new BufferedReader(clientSocket.getInputStream());
        PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
        String request, response;
        // 处理循环开始
        while ((request = in.readLine()) != null){
            if ("Done".equalsIgnoreCase(request)){ // 如果接收了Done,退出
                break;
            }
            // 请求被传递给服务器的处理方法
            response = processRequest(request);
            // 服务器的响应被发送给了客户端
            out.println(response);
        }
    }
}
  1. ServerSocket上的accept()方法将会一直阻塞到一个连接建立,随后返回一个新的Socket用于客户端和服务器之间的通信。该Serversocket将继续监听传入的连接
  2. BufferedReader 和 PrintWRIter 都衍生自Socket的输入输出流。前者从一个字符输入流中读取文本,后者打印对象的格式化的表示到文本输出流
  3. readLine()方法将会阻塞,知道循环条件出现一个由换行符或者回车符结尾的字符串被读取
  4. 客户端的请求已经被处理

这段代码将只能同时处理一个连接,如果同时处理多个并发客户端,需要为每个客户端Socket建立一个新的Thread,将会使得大量线程处于休眠状态,等待其他任务的进行,浪费资源。线程内存分配默认为64KB到1MB。

1.1.1 Java NIO

上述的基础套接字都是阻塞式的,本地API库也提供了非阻塞调用,提高了网络的利用效率

  1. 可以使用setsokopt()方法配置套接字,以便读写调用在没有数据的时候立即返回,也就是说,如果是一个阻塞调用应该已经被阻塞了
  2. 可以使用操作系统的事件通知API注册一组非阻塞套接字,已确定它们中是否有任何的套接字已经有数据可以读写

NIO最开始是New Input/Ouput的缩写,但是该java api出现的时间已经够长了,如今被认为是Non-blocking IO,而阻塞I/O(BIO)是旧的输入输出,也可叫做plain I/O

1.1.2 选择器

class java.nio.channels.Selector 是java的NIO实现的关键,就是设置一个选择器,检查多个socket中有哪些已经就绪,可以进行读写操作,然后选择该socket。这样就实现了单一线程处理多个并发的连接

Socket	Socket	Socket
   |       |       |
  读写    读写    读写
   |	   |       |
   \			  /
     \     |     /
        Selector
           |
           Thread

1.2 Netty介绍

表 1-1 Netty的特性总结

分类 Netty的特性总结
设计 统一的API,支持多种传输类型,阻塞和非阻塞的;简单而强大的线程模型;真正的无连接数据报套接字支持;连接逻辑组件以支持复用
易用性 详实的JavaDoc和大量的示例;不需要超过JDK 1.6的依赖
性能 拥有比java核心api更高的吞吐量以及更低的延迟;得益于池化和复用,拥有更低的资源消耗
健壮性 不会因为慢速,快速或者超载的连接而导致OutOfMemoryError;消除在高速网络中NIO程序常见的不公平读写比率
安全性 完成的SSL/TLS以及StartTLS支持;可用于受限环境下,如Applet和OSGI
发布快速且频繁

1.2.1 谁在使用Netty

很多大公司的通信框架都是基于Netty的

1.2.2 异步和事件驱动

异步:不知道什么时候会收到消息,但是在收到后应该可以进行处理,等待的时候也可以做其他的事情

可伸缩性:当处理的工作不断增加时,可以通过某种可行的方式或者扩大它的处理能力来适应这种增长的能力

二者之间的联系?

  1. 非阻塞网络调用使得我们不用等待一个操作的完成,完全异步的IO正式基于此特性构建的,并且更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户
  2. 选择器使得我们能够通过较少的线程便可监视许多连接上的事件

这就是构建Netty系统的设计底蕴的关键

1.3 Netty的核心组件

  1. Channel
  2. 回调
  3. Future
  4. 事件和ChannelHandler

这些构建块代表了不同类型的构造:资源、逻辑以及通知。

1.3.1 Channel

达标一个到实体(如硬件设备,一个文件,一个网络套接字或者一个能够执行一个或多个不同IO操作的程序组件)的开放链接,如读操作和写操作

可以把Channel看作是传入或者传入数据的载体。因此其可以被打开或者被关闭,连接或者断开连接

1.3.2 回调

编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。

当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。

比如,想让宾馆叫醒客人,这就是库函数,谁都可以要求这个叫醒服务,但是这个函数中包含客人指定的叫醒方式,也就是回调函数,是客人自己指定的,并且传入给宾馆叫醒服务。

package even
def double(x):# 回调函数
    return 2*x

def quart(x):
    return 4*x

from even import *

def createOddNum(x, function): # 向库函数中传入自定义函数
    return 1+function(x)

def main():
    print(createOddNum(1, quart)) 

if __name__ == "__main__":
    main()

Netty在内部使用回调来处理事件,当一个回调被触发时,相关的事件可以被一个interface-ChannelHandler的实现处理。下面的代码展示:当一个新的连接已经被建立时,ChannelHannel的channelActivate()回调方法将会被调用,并将打印出一条信息。

package J2Learn;

public class ConnectHandler extends ChannelInboundHandleAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception{ // 当一个新的连接已经被建立时,channelActive(ChannelHandlerContext将会被调用)
        System.out.println("Client "+ ctx.channel().remoteAddress()+" connected");
    }
}

1.3.3 Future

Future提供了在操作完成时通知应用程序的方式。这个对象可以看做是一个异步操作的结果的占位符:其将在未来的某个时刻完成,并提供对结果的访问

JDK预置了interface java.util.concurrent.Future,允许手动检查对应操作是否已经完成,或者一直阻塞到它完成。Netty提供了更为简单的实现ChannelFuture,用于在执行异步操作的时候使用

ChannelFutureListener提供了几种额外的方法,这些方法使得我们能够注册一个或多个实例。监听器的回调方法operationComplete(),将会在对应的操作完成时调用。然后监听器可以判断该操作是完成了还是出错了。如果是后者,可以检索产生的Throwable。所以,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。

每个Netty的出战IO操作都会返回一个ChannelFuture;也就是,他们都不会阻塞

Channel channel = ...;
// 异步连接到远程节点
ChannelFuture futrue = channel.connect(new InetSocketAddress("192.168.0.1", 25));

如何利用ChannelFutureListener,首先,要连接到远程节点上,然后注册一个新的ChannelFutureListener到对connect()方法的调用所返回的ChannelFuture上。当该监听器被通知链接已经建立时,要检查对应的状态。如果操作成功,那么将数据写到该Channel。否则要从ChannelFuture中检索到对应的Trowable

    Channel channel = ...;
    // 异步连接到远程节点
    ChannelFuture futrue = channel.connect(new InetSocketAddress("192.168.0.1", 25));
    // 注册监听器,以便在操作完成后接到通知
    future.addListener(new ChannelFutureListener()){
        @Override
        public void opeartionComplete(ChannelFuture future) {
            if (future.isSuccess()){
                ByteBuf buffer = Unpooled.copiedBuffer( // 如果操作成功, 创建一个ByteBuf储存数据
                    "Hello", Charset.defaultCharset());
                ChannelFuture wf = future.channel().writeAndFlush(buffer) // 将数据异步地发送到远程节点。返回一个ChannelFuture
            } else {
                Trowable cause = future.cause();// 如果发生错误,返回错误信息
                cause.printStackTrace();
            }
        }
    }

1.3.4 事件和ChannelHandler

Netty使用不同的事件来通知我们状态的改变。这时的我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:记录日志、数据转换、流控制、应用程序逻辑;

Netty是一个网络编程框架,所以事件是按照他们与入栈或出站数据流的相关性进行分类的,可能由入站数据或者相关的状态更改而触发的事件包括:连接已被激活或者连接失活、数据读取、用户时间、错误时间。出站时间是未来将会触发的某个动作的操作结果,包括:打开或者关闭到远程节点的连接、将数据写到或者冲刷到套接字

每个事件都可被分给ChannelHandler类中的某个用户实现的方法

目前可以认为,每个Channel-Handler的实例都类似于一种为了响应特定事件而被执行的回调

1.3.5 把他们放在一起

你可能感兴趣的:(netty)