• w3cschoolNetty 实战精髓篇3


    https://www.w3cschool.cn/essential_netty_in_action/essential_netty_in_action-wd1j28dq.html

    Netty WebSocket 程序示例

    2021-02-07 15:40 更新

    实时功能到底有什么吸引人的特点呢?下面,我们就用 WebSocket 协议来实现一个基于浏览器的实时聊天程序来说明它的特点。虽然它可能与在 Facebook 中的聊天操作有所类似,但是我们的目的是要让使用它的用户能够同时互相交谈,这样才能体现出它的特别之处不是?

    程序逻辑如图 11.1 所示

    微信截图_20210207154015

    #1客户端/用户连接到服务器,并且是聊天的一部分

    #2聊天消息通过 WebSocket 进行交换

    #3消息双向发送

    #4服务器处理所有的客户端/用户

    逻辑很简单:

    • 1.客户端发送一个消息。
    • 2.消息被广播到所有其他连接的客户端。

    这正如你所想的聊天室的工作方式:每个人都可以跟其他人聊天。此例子将仅提供服务器端,浏览器充当客户端,通过访问网页来聊天。正如您接下来要看到的,WebSocket 让这一切变得简单。

    Netty添加 WebSocket 支持

    2018-08-08 10:56 更新

    一种被称作“Upgrade handshake(升级握手)”的机制能够将标准的HTTP或者HTTPS协议转成 WebSocket。所以,应用程序如果使用了 WebSocket ,那么它都是以 HTTP/S 开始,之后再进行升级,升级会发生在什么时候是不确定的,要根据具体的应用来决定:可能是在应用启动的时候,也可能是当一个特定的 URL 被请求的时候。

    在我们的应用中,要想升级协议为 WebSocket,只有当 URL 请求以“/ws”结束时才可以,如果没有达到该要求,服务器仍将使用基本的 HTTP/S,一旦连接升级,之后的数据传输都将使用 WebSocket 。

    下面看下服务器的逻辑图

    Figure 11.2 Server logic

    Figure%2011

    #1客户端/用户连接到服务器并加入聊天

    #2 HTTP 请求页面或 WebSocket 升级握手

    #3服务器处理所有客户端/用户

    #4响应 URI “/”的请求,转到 index.html

    #5如果访问的是 URI“/ws” ,处理 WebSocket 升级握手

    #6升级握手完成后 ,通过 WebSocket 发送聊天消息

    处理 HTTP 请求

    本节我们将实现此应用中用于处理 HTTP 请求的组件,这个组件托管着可供客户端访问的聊天室页面,并且显示客户端发送的消息。

    下面就是这个 HttpRequestHandler 的代码,它是一个用来处理 FullHttpRequest 消息的 ChannelInboundHandler 的实现类。注意看它是怎么实现忽略符合 "/ws" 格式的 URI 请求的。

    Listing 11.1 HTTPRequestHandler

    public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {    //1
        private final String wsUri;
        private static final File INDEX;
    
        static {
            URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
            try {
                String path = location.toURI() + "index.html";
                path = !path.contains("file:") ? path : path.substring(5);
                INDEX = new File(path);
            } catch (URISyntaxException e) {
                throw new IllegalStateException("Unable to locate index.html", e);
            }
        }
    
        public HttpRequestHandler(String wsUri) {
            this.wsUri = wsUri;
        }
    
        @Override
        public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
            if (wsUri.equalsIgnoreCase(request.getUri())) {
                ctx.fireChannelRead(request.retain());                    //2
            } else {
                if (HttpHeaders.is100ContinueExpected(request)) {
                    send100Continue(ctx);                                //3
                }
    
                RandomAccessFile file = new RandomAccessFile(INDEX, "r");//4
    
                HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK);
                response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8");
    
                boolean keepAlive = HttpHeaders.isKeepAlive(request);
    
                if (keepAlive) {                                        //5
                    response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, file.length());
                    response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
                }
                ctx.write(response);                    //6
    
                if (ctx.pipeline().get(SslHandler.class) == null) {        //7
                    ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
                } else {
                    ctx.write(new ChunkedNioFile(file.getChannel()));
                }
                ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);            //8
                if (!keepAlive) {
                    future.addListener(ChannelFutureListener.CLOSE);        //9
                }
            }
        }
    
        private static void send100Continue(ChannelHandlerContext ctx) {
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
            ctx.writeAndFlush(response);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
                throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
    

    1.扩展 SimpleChannelInboundHandler 用于处理 FullHttpRequest信息

    2.如果请求是一次升级了的 WebSocket 请求,则递增引用计数器(retain)并且将它传递给在 ChannelPipeline 中的下个 ChannelInboundHandler

    3.处理符合 HTTP 1.1的 "100 Continue" 请求

    4.读取 index.html

    5.判断 keepalive 是否在请求头里面

    6.写 HttpResponse 到客户端

    7.写 index.html 到客户端,根据 ChannelPipeline 中是否有 SslHandler 来决定使用 DefaultFileRegion 还是 ChunkedNioFile

    8.写并刷新 LastHttpContent 到客户端,标记响应完成

    9.如果 请求头中不包含 keepalive,当写完成时,关闭 Channel

    HttpRequestHandler 做了下面几件事,

    • 如果该 HTTP 请求被发送到URI “/ws”,则调用 FullHttpRequest 上的 retain(),并通过调用 fireChannelRead(msg) 转发到下一个 ChannelInboundHandler。retain() 的调用是必要的,因为 channelRead() 完成后,它会调用 FullHttpRequest 上的 release() 来释放其资源。 (请参考我们先前在第6章中关于 SimpleChannelInboundHandler 的讨论)
    • 如果客户端发送的 HTTP 1.1 头是“Expect: 100-continue” ,则发送“100 Continue”的响应。
    • 在 头被设置后,写一个 HttpResponse 返回给客户端。注意,这不是 FullHttpResponse,这只是响应的第一部分。另外,这里我们也不使用 writeAndFlush(), 这个是在留在最后完成。
    • 如果传输过程既没有要求加密也没有要求压缩,那么把 index.html 的内容存储在一个 DefaultFileRegion 里就可以达到最好的效率。这将利用零拷贝来执行传输。出于这个原因,我们要检查 ChannelPipeline 中是否有一个 SslHandler。如果是的话,我们就使用 ChunkedNioFile。
    • 写 LastHttpContent 来标记响应的结束,并终止它
    • 如果不要求 keepalive ,添加 ChannelFutureListener 到 ChannelFuture 对象的最后写入,并关闭连接。注意,这里我们调用 writeAndFlush() 来刷新所有以前写的信息。

    这里展示了应用程序的第一部分,用来处理纯的 HTTP 请求和响应。接下来我们将处理 WebSocket 的 frame(帧),用来发送聊天消息。

    WebSocket frame

    WebSockets 在“帧”里面来发送数据,其中每一个都代表了一个消息的一部分。一个完整的消息可以利用了多个帧。

    处理 WebSocket frame

    WebSocket "Request for Comments" (RFC) 定义了六种不同的 frame; Netty 给他们每个都提供了一个 POJO 实现 ,见下表:

    Table 11.1 WebSocketFrame types

    名称描述
    BinaryWebSocketFrame contains binary data
    TextWebSocketFrame contains text data
    ContinuationWebSocketFrame contains text or binary data that belongs to a previous BinaryWebSocketFrame or TextWebSocketFrame
    CloseWebSocketFrame represents a CLOSE request and contains close status code and a phrase
    PingWebSocketFrame requests the transmission of a PongWebSocketFrame
    PongWebSocketFrame sent as a response to a PingWebSocketFrame

    我们的程序只需要使用下面4个帧类型:

    • CloseWebSocketFrame
    • PingWebSocketFrame
    • PongWebSocketFrame
    • TextWebSocketFrame

    在这里我们只需要处理 TextWebSocketFrame,其他的会由 WebSocketServerProtocolHandler 自动处理。

    下面代码展示了 ChannelInboundHandler 处理 TextWebSocketFrame,同时也将跟踪在 ChannelGroup 中所有活动的 WebSocket 连接

    Listing 11.2 Handles Text frames

    public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { //1
        private final ChannelGroup group;
    
        public TextWebSocketFrameHandler(ChannelGroup group) {
            this.group = group;
        }
    
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {    //2
            if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
    
                ctx.pipeline().remove(HttpRequestHandler.class);    //3
    
                group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));//4
    
                group.add(ctx.channel());    //5
            } else {
                super.userEventTriggered(ctx, evt);
            }
        }
    
        @Override
        public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            group.writeAndFlush(msg.retain());    //6
        }
    }
    

    1.扩展 SimpleChannelInboundHandler 用于处理 TextWebSocketFrame 信息

    2.覆写userEventTriggered() 方法来处理自定义事件

    3.如果接收的事件表明握手成功,就从 ChannelPipeline 中删除HttpRequestHandler ,因为接下来不会接受 HTTP 消息了

    4.写一条消息给所有的已连接 WebSocket 客户端,通知它们建立了一个新的 Channel 连接

    5.添加新连接的 WebSocket Channel 到 ChannelGroup 中,这样它就能收到所有的信息

    6.保留收到的消息,并通过 writeAndFlush() 传递给所有连接的客户端。

    上面显示了 TextWebSocketFrameHandler 仅作了几件事:

    • 当WebSocket 与新客户端已成功握手完成,通过写入信息到 ChannelGroup 中的 Channel 来通知所有连接的客户端,然后添加新 Channel 到 ChannelGroup
    • 如果接收到 TextWebSocketFrame,调用 retain() ,并将其写、刷新到 ChannelGroup,使所有连接的 WebSocket Channel 都能接收到它。和以前一样,retain() 是必需的,因为当 channelRead0()返回时,TextWebSocketFrame 的引用计数将递减。由于所有操作都是异步的,writeAndFlush() 可能会在以后完成,我们不希望它访问无效的引用。

    由于 Netty 在其内部处理了其余大部分功能,唯一剩下的需要我们去做的就是为每一个新创建的 Channel 初始化 ChannelPipeline 。要完成这个,我们需要一个ChannelInitializer

    初始化 ChannelPipeline

    接下来,我们需要安装我们上面实现的两个 ChannelHandler 到 ChannelPipeline。为此,我们需要继承 ChannelInitializer 并且实现 initChannel()。看下面 ChatServerInitializer 的代码实现

    Listing 11.3 Init the ChannelPipeline

    public class ChatServerInitializer extends ChannelInitializer<Channel> {    //1
        private final ChannelGroup group;
    
        public ChatServerInitializer(ChannelGroup group) {
            this.group = group;
        }
    
        @Override
        protected void initChannel(Channel ch) throws Exception {            //2
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new HttpServerCodec());
            pipeline.addLast(new HttpObjectAggregator(64 * 1024));
            pipeline.addLast(new ChunkedWriteHandler());
            pipeline.addLast(new HttpRequestHandler("/ws"));
            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
            pipeline.addLast(new TextWebSocketFrameHandler(group));
        }
    }
    

    1.扩展 ChannelInitializer

    2.添加 ChannelHandler 到 ChannelPipeline

    initChannel() 方法用于设置所有新注册的 Channel 的ChannelPipeline,安装所有需要的 ChannelHandler。总结如下:

    Table 11.2 ChannelHandlers for the WebSockets Chat server

    ChannelHandler  职责
    HttpServerCodec Decode bytes to HttpRequest, HttpContent, LastHttpContent.Encode HttpRequest, HttpContent, LastHttpContent to bytes.
    ChunkedWriteHandler Write the contents of a file.
    HttpObjectAggregator This ChannelHandler aggregates an HttpMessage and its following HttpContents into a single FullHttpRequest or FullHttpResponse (depending on whether it is being used to handle requests or responses).With this installed the next ChannelHandler in the pipeline will receive only full HTTP requests.
    HttpRequestHandler Handle FullHttpRequests (those not sent to "/ws" URI).
    WebSocketServerProtocolHandler As required by the WebSockets specification, handle the WebSocket Upgrade handshake, PingWebSocketFrames,PongWebSocketFrames and CloseWebSocketFrames.
    TextWebSocketFrameHandler Handles TextWebSocketFrames and handshake completion events

    该 WebSocketServerProtocolHandler 处理所有规定的 WebSocket 帧类型和升级握手本身。如果握手成功所需的 ChannelHandler 被添加到管道,而那些不再需要的则被去除。管道升级之前的状态如下图。这代表了 ChannelPipeline 刚刚经过 ChatServerInitializer 初始化。

    Figure 11.3 ChannelPipeline before WebSockets Upgrade

    Figure%2011

    握手升级成功后 WebSocketServerProtocolHandler 替换HttpRequestDecoder 为 WebSocketFrameDecoder,HttpResponseEncoder 为WebSocketFrameEncoder。 为了最大化性能,WebSocket 连接不需要的 ChannelHandler 将会被移除。其中就包括了 HttpObjectAggregator 和 HttpRequestHandler

    下图,展示了 ChannelPipeline 经过这个操作完成后的情况。注意 Netty 目前支持四个版本 WebSocket 协议,每个通过其自身的方式实现类。选择正确的版本WebSocketFrameDecoder 和 WebSocketFrameEncoder 是自动进行的,这取决于在客户端(在这里指浏览器)的支持(在这个例子中,我们假设使用版本是 13 的 WebSocket 协议,从而图中显示的是 WebSocketFrameDecoder13 和 WebSocketFrameEncoder13)。

    Figure 11.4 ChannelPipeline after WebSockets Upgrade

    Figure%2011

    引导

    最后一步是 引导服务器,设置 ChannelInitializer

    public class ChatServer {
    
        private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);//1
        private final EventLoopGroup group = new NioEventLoopGroup();
        private Channel channel;
    
        public ChannelFuture start(InetSocketAddress address) {
            ServerBootstrap bootstrap  = new ServerBootstrap(); //2
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(createInitializer(channelGroup));
            ChannelFuture future = bootstrap.bind(address);
            future.syncUninterruptibly();
            channel = future.channel();
            return future;
        }
    
        protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {        //3
           return new ChatServerInitializer(group);
        }
    
        public void destroy() {        //4
            if (channel != null) {
                channel.close();
            }
            channelGroup.close();
            group.shutdownGracefully();
        }
    
        public static void main(String[] args) throws Exception{
            if (args.length != 1) {
                System.err.println("Please give port as argument");
                System.exit(1);
            }
            int port = Integer.parseInt(args[0]);
    
            final ChatServer endpoint = new ChatServer();
            ChannelFuture future = endpoint.start(new InetSocketAddress(port));
    
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    endpoint.destroy();
                }
            });
            future.channel().closeFuture().syncUninterruptibly();
        }
    }
    

    1.创建 DefaultChannelGroup 用来 保存所有连接的的 WebSocket channel

    2.引导 服务器

    3.创建 ChannelInitializer

    4.处理服务器关闭,包括释放所有资源

    Netty测试程序

    2018-08-08 10:57 更新

    使用下面命令启动服务器:

    mvn -PChatServer clean package exec:exec
    

    其中项目中的 pom.xml 是配置了 9999 端口。你也可以通过下面的方法修改属性

    mvn -PChatServer -Dport=1111 clean package exec:exec
    

    下面是控制台的主要输出(删除了部分行)

    Listing 11.5 Compile and start the ChatServer

    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building ChatServer 1.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------
    ...
    [INFO]
    [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
    [INFO] Building jar: D:/netty-in-action/chapter11/target/chat-server-1.0-SNAPSHOT.jar
    [INFO]
    [INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ chat-server ---
    Starting ChatServer on port 9999
    

    可以在浏览器中通过 http://localhost:9999 地址访问程序。图11.5展示了此程序在Chrome浏览器下的用户界面。

    Figure 11.5 WebSockets ChatServer demonstration

    Figure%2011

    图中显示了两个已经连接了的客户端。第一个客户端是通过上面的图形界面连接的,第二个是通过Chrome浏览器底部的命令行连接的。 你可以注意到,这两个客户端都在发送消息,每条消息都会显示在两个客户端上。

    如何加密?

    在实际场景中,加密是必不可少的。在Netty中实现加密并不麻烦,你只需要向 ChannelPipeline 中添加 SslHandler ,然后配置一下即可。如下:

    Listing 11.6 Add encryption to the ChannelPipeline

    public class SecureChatServerIntializer extends ChatServerInitializer {    //1
        private final SslContext context;
    
        public SecureChatServerIntializer(ChannelGroup group, SslContext context) {
            super(group);
            this.context = context;
        }
    
        @Override
        protected void initChannel(Channel ch) throws Exception {
            super.initChannel(ch);
            SSLEngine engine = context.newEngine(ch.alloc());
            engine.setUseClientMode(false);
            ch.pipeline().addFirst(new SslHandler(engine)); //2
        }
    }
    

    1.扩展 ChatServerInitializer 来实现加密

    2.向 ChannelPipeline 中添加SslHandler

    最后修改 ChatServer,使用 SecureChatServerInitializer 并传入 SSLContext

    Listing 11.7 Add encryption to the ChatServer

    public class SecureChatServer extends ChatServer {//1
    
        private final SslContext context;
    
        public SecureChatServer(SslContext context) {
            this.context = context;
        }
    
        @Override
        protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
            return new SecureChatServerIntializer(group, context);    //2
        }
    
        public static void main(String[] args) throws Exception{
            if (args.length != 1) {
                System.err.println("Please give port as argument");
                System.exit(1);
            }
            int port = Integer.parseInt(args[0]);
            SelfSignedCertificate cert = new SelfSignedCertificate();
            SslContext context = SslContext.newServerContext(cert.certificate(), cert.privateKey());
            final SecureChatServer endpoint = new SecureChatServer(context);
            ChannelFuture future = endpoint.start(new InetSocketAddress(port));
    
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    endpoint.destroy();
                }
            });
            future.channel().closeFuture().syncUninterruptibly();
        }
    }
    

    1.扩展 ChatServer

    2.返回先前创建的 SecureChatServerInitializer 来启用加密

    这样,就在所有的通信中使用了 SSL/TLS 加密。和前面一样,你可以使用Maven拉取应用需要的所有依赖,并启动它,如下所示。

    Listing 11.8 Start the SecureChatServer

    $ mvn -PSecureChatServer clean package exec:exec
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building ChatServer 1.0-SNAPSHOT
    [INFO] ------------------------------------------------------------------------
    ...
    [INFO]
    [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
    [INFO] Building jar: D:/netty-in-action/chapter11/target/chat-server-1.0-SNAPSHOT.jar
    [INFO]
    [INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ chat-server ---
    Starting SecureChatServer on port 9999
    

    现在你可以通过 HTTPS 地址: https://localhost:9999 来访问SecureChatServer 了。

    Netty SPDY示例程序

    2018-08-08 10:58 更新

    编写一个简单的服务器应用程序,看看如何将 SPDY 集成到你的下一个应用程序中。客户机会接收到它提供的一些静态内容,而这些内容将由你所使用的协议是 HTTPS 还是 SPDY 决定。如果服务器 SPDY 能够被客户端的浏览器所支持的话,就会自动切换到 SPDY。图12.1显示了应用程序的流程

    Figure%2012

    对于这个应用程序只编写一个服务器组件处理 HTTPS 和 SPDY。为了演示其功能使用两个不同的 web 浏览器,一个支持 SPDY,另外一个不支持。

    Netty中的SPDY实现

    2020-11-06 16:16 更新

    SPDY 使用 TLS 的扩展称为 Next Protocol Negotiation (NPN)。在Java 中,我们有两种不同的方式选择的基于 NPN 的协议:

    • 使用 ssl_npn,NPN 的开源 SSL 提供者。
    • 使用通过 Jetty 的 NPN 扩展库。

    在这个例子中使用 Jetty 库。如果你想使用 ssl_npn,请参阅https://github.com/benmmurphy/ssl_npn项目文档

    Jetty NPN 库

    Jetty NPN 库是一个外部的库,而不是 Netty 的本身的一部分。它用于处理 Next Protocol Negotiation, 这是用于检测客户端是否支持 SPDY。

    集成 Next Protocol Negotiation

    Jetty 库提供了一个接口称为 ServerProvider,确定所使用的协议和选择哪个钩子。这个的实现可能取决于不同版本的 HTTP 和 SPDY 版本的支持。下面的清单显示了将用于我们的示例应用程序的实现。

    Listing 12.1 Implementation of ServerProvider

    public class DefaultServerProvider implements NextProtoNego.ServerProvider {
        private static final List<String> PROTOCOLS =
                Collections.unmodifiableList(Arrays.asList("spdy/2", "spdy/3", "http/1.1"));  //1
    
        private String protocol;
    
        @Override
        public void unsupported() {
            protocol = "http/1.1";   //2
        }
    
        @Override
        public List<String> protocols() {
            return PROTOCOLS;   //3
        }
    
        @Override
        public void protocolSelected(String protocol) {
            this.protocol = protocol;  //4
        }
    
        public String getSelectedProtocol() {
            return protocol;  //5
        }
    }
    
    1. 定义所有的 ServerProvider 实现的协议
    2. 设置如果 SPDY 协议失败了就转到 http/1.1
    3. 返回支持的协议的列表
    4. 设置选择的协议
    5. 返回选择的协议

    在 ServerProvider 的实现,我们支持下面的3种协议:

    • SPDY 2
    • SPDY 3
    • HTTP 1.1

    如果客户端不支持 SPDY ,则默认使用 HTTP 1.1

    实现各种 ChannelHandler

    第一个 ChannelInboundHandler 是用于不支持 SPDY 的情况下处理客户端 HTTP 请求,如果不支持 SPDY 就回滚使用默认的 HTTP 协议。

    清单12.2显示了HTTP流量的处理程序。

    Listing 12.2 Implementation that handles HTTP

    @ChannelHandler.Sharable
    public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { //1
            if (HttpHeaders.is100ContinueExpected(request)) {
                send100Continue(ctx); //2
            }
    
            FullHttpResponse response = new DefaultFullHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK); //3
            response.content().writeBytes(getContent().getBytes(CharsetUtil.UTF_8));  //4
            response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");  //5
    
            boolean keepAlive = HttpHeaders.isKeepAlive(request);
    
            if (keepAlive) {  //6
                response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
                response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            }
            ChannelFuture future = ctx.writeAndFlush(response);  //7
    
            if (!keepAlive) {
                future.addListener (ChannelFutureListener.CLOSE); //8
            }
        }
    
        protected String getContent() {  //9
            return "This content is transmitted via HTTP\r\n";
        }
    
        private static void send100Continue(ChannelHandlerContext ctx) {  //10
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
            ctx.writeAndFlush(response);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
                throws Exception {  //11
            cause.printStackTrace();
            ctx.close();
        }
    }
    
    1. 重写 channelRead0() ,可以被所有的接收到的 FullHttpRequest 调用
    2. 检查如果接下来的响应是预期的,就写入
    3. 新建 FullHttpResponse,用于对请求的响应
    4. 生成响应的内容,将它写入 payload
    5. 设置头文件,这样客户端就能知道如何与 响应的 payload 交互
    6. 检查请求设置是否启用了 keepalive;如果是这样,将标题设置为符合HTTP RFC
    7. 写响应给客户端,并获取到 Future 的引用,用于写完成时,获取到通知
    8. 如果响应不是 keepalive,在写完成时关闭连接
    9. 返回内容作为响应的 payload
    10. Helper 方法生成了100 持续的响应,并写回给客户端
    11. 若执行阶段抛出异常,则关闭管道

    这就是 Netty 处理标准的 HTTP 。你可能需要分别处理特定 URI ,应对不同的状态代码,这取决于资源存在与否,但基本的概念将是相同的。

    我们的下一个任务将会提供一个组件来支持 SPDY 作为首选协议。 Netty 提供了简单的处理 SPDY 方法。这些将使您能够重用FullHttpRequest 和 FullHttpResponse 消息,通过 SPDY 透明地接收和发送他们。

    HttpRequestHandler 虽然是我们可以重用代码,我们将改变我们的内容写回客户端只是强调协议变化;通常您会返回相同的内容。下面的清单展示了实现,它扩展了先前的 HttpRequestHandler。

    Listing 12.3 Implementation that handles SPDY

    @ChannelHandler.Sharable
    public class SpdyRequestHandler extends HttpRequestHandler {   //1
        @Override
        protected String getContent() {
            return "This content is transmitted via SPDY\r\n";  //2
        }
    }
    
    1. 继承 HttpRequestHandler 这样就能共享相同的逻辑
    2. 生产内容写到 payload。这个重写了 HttpRequestHandler 的 getContent() 的实现

    SpdyRequestHandler 继承自 HttpRequestHandler,但区别是:写入的内容的 payload 状态的响应是在 SPDY 写的。

    我们可以实现两个处理程序逻辑,将选择一个相匹配的协议。然而添加以前写过的处理程序到 ChannelPipeline 是不够的;正确的编解码器还需要补充。它的责任是检测传输字节数,然后使用 FullHttpResponse 和 FullHttpRequest 的抽象进行工作。

    Netty 的附带一个基类,完全能做这个。所有您需要做的是实现逻辑选择协议和选择适当的处理程序。

    清单12.4显示了实现,它使用 Netty 的提供的抽象基类。

    public class DefaultSpdyOrHttpChooser extends SpdyOrHttpChooser {
    
        public DefaultSpdyOrHttpChooser(int maxSpdyContentLength, int maxHttpContentLength) {
            super(maxSpdyContentLength, maxHttpContentLength);
        }
    
        @Override
        protected SelectedProtocol getProtocol(SSLEngine engine) {
            DefaultServerProvider provider = (DefaultServerProvider) NextProtoNego.get(engine);  //1
            String protocol = provider.getSelectedProtocol();
            if (protocol == null) {
                return SelectedProtocol.UNKNOWN; //2
            }
            switch (protocol) {
                case "spdy/2":
                    return SelectedProtocol.SPDY_2; //3
                case "spdy/3.1":
                    return SelectedProtocol.SPDY_3_1; //4
                case "http/1.1":
                    return SelectedProtocol.HTTP_1_1; //5
                default:
                    return SelectedProtocol.UNKNOWN; //6
            }
        }
    
        @Override
        protected ChannelInboundHandler createHttpRequestHandlerForHttp() {
            return new HttpRequestHandler(); //7
        }
    
        @Override
        protected ChannelInboundHandler createHttpRequestHandlerForSpdy() {
            return new SpdyRequestHandler();  //8
        }
    }
    
    1. 使用 NextProtoNego 用于获取 DefaultServerProvider 的引用, 用于 SSLEngine
    2. 协议不能被检测到。一旦字节已经准备好读,检测过程将重新开始。
    3. SPDY 2 被检测到
    4. SPDY 3 被检测到
    5. HTTP 1.1 被检测到
    6. 未知协议被检测到
    7. 将会被调用给 FullHttpRequest 消息添加处理器。该方法只会在不支持 SPDY 时调用,那么将会使用 HTTPS
    8. 将会被调用给 FullHttpRequest 消息添加处理器。该方法在支持 SPDY 时调用

    该实现要注意检测正确的协议并设置 ChannelPipeline 。它可以处理SPDY 版本 2、3 和 HTTP 1.1,但可以很容易地修改 SPDY 支持额外的版本。

    设置 ChannelPipeline

    通过实现 ChannelInitializer 将所有的处理器连接到一起。正如你所了解的那样,这将设置 ChannelPipeline 并添加所有需要的ChannelHandler 的。

    SPDY 需要两个 ChannelHandler:

    • SslHandler,用于检测 SPDY 是否通过 TLS 扩展
    • DefaultSpdyOrHttpChooser,用于当协议被检测到时,添加正确的 ChannelHandler 到 ChannelPipeline

    除了添加 ChannelHandler 到 ChannelPipeline, ChannelInitializer 还有另一个责任;即,分配之前创建的 DefaultServerProvider 通过 SslHandler 到 SslEngine 。这将通过Jetty NPN 类库的 NextProtoNego helper 类实现

    Listing 12.5 Implementation that handles SPDY

    public class SpdyChannelInitializer extends ChannelInitializer<SocketChannel> {  //1
        private final SslContext context;
    
        public SpdyChannelInitializer(SslContext context) //2 {
            this.context = context;
        }
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            SSLEngine engine = context.newEngine(ch.alloc());  //3
            engine.setUseClientMode(false);  //4
    
            NextProtoNego.put(engine, new DefaultServerProvider());  //5
            NextProtoNego.debug = true;
    
            pipeline.addLast("sslHandler", new SslHandler(engine));  //6
            pipeline.addLast("chooser", new DefaultSpdyOrHttpChooser(1024 * 1024, 1024 * 1024));
        }
    }
    
    1. 继承 ChannelInitializer 是一个简单的开始
    2. 传递 SSLContext 用于创建 SSLEngine
    3. 新建 SSLEngine,用于新的管道和连接
    4. 配置 SSLEngine 用于非客户端使用
    5. 通过 NextProtoNego helper 类绑定 DefaultServerProvider 到 SSLEngine
    6. 添加 SslHandler 到 ChannelPipeline 这将会在协议检测到时保存在 ChannelPipeline
    7. 添加 DefaultSpyOrHttpChooser 到 ChannelPipeline 。这个实现将会监测协议。添加正确的 ChannelHandler 到 ChannelPipeline,并且移除自身

    实际的 ChannelPipeline 设置将会在 DefaultSpdyOrHttpChooser 实现之后完成,因为在这一点上它可能只需要知道客户端是否支持 SPDY

    为了说明这一点,让我们总结一下,看看不同 ChannelPipeline 状态期间与客户连接的生命周期。图12.2显示了在 Channel 初始化后的 ChannelPipeline。

    Figure%2012

    Figure 12.2 ChannelPipeline after connection

    现在,这取决于客户端是否支持 SPDY,管道将修改DefaultSpdyOrHttpChooser 来处理协议。之后并不需要添加所需的 ChannelHandler 到 ChannelPipeline,所以删除本身。这个逻辑是由抽象 SpdyOrHttpChooser 类封装,DefaultSpdyOrHttpChooser 父类。

    图12.3显示了支持 SPDY 的 ChannelPipeline 用于连接客户端的配置。

    Figure%2012

    Figure 12.3 ChannelPipeline if SPDY is supported

    每个 ChannelHandler 负责的一小部分工作,这个就是对基于 Netty 构造的应用程序最完美的诠释。每个 ChannelHandler 的职责如表12.3所示。

    Table 12.3 Responsibilities of the ChannelHandlers when SPDY is used

    名称职责
    SslHandler 加解密两端交换的数据
    SpdyFrameDecoder 从接收到的 SPDY 帧中解码字节
    SpdyFrameEncoder 编码 SPDY 帧到字节
    SpdySessionHandler 处理 SPDY session
    SpdyHttpEncoder 编码 HTTP 消息到 SPDY 帧
    SpdyHttpDecoder 解码 SDPY 帧到 HTTP 消息
    SpdyHttpResponseStreamIdHandler 处理基于 SPDY ID 请求和响应之间的映射关系
    SpdyRequestHandler 处理 FullHttpRequest, 用于从 SPDY 帧中解码,因此允许 SPDY 透明传输使用

    当协议是 HTTP(s) 时,ChannelPipeline 看起来相当不同,如图13.4所示。

    Figure%2012

    Figure 13.4 ChannelPipeline if SPDY is not supported

    和之前一样,每个 ChannelHandler 都有职责,定义在表12.4

    Table 12.4 Responsibilities of the ChannelHandlers when HTTP is used

    名称职责
    SslHandler 加解密两端交换的数据
    HttpRequestDecoder 从接收到的 HTTP 请求中解码字节
    HttpResponseEncoder 编码 HTTP 响应到字节

    HttpObjectAggregator 处理 SPDY session HttpRequestHandler | 解码时处理 FullHttpRequest

    所有东西组合在一起

    所有的 ChannelHandler 实现已经准备好,现在组合成一个 SpdyServer

    Listing 12.6 SpdyServer implementation

    public class SpdyServer {
    
        private final NioEventLoopGroup group = new NioEventLoopGroup();  //1
        private final SslContext context;
        private Channel channel;
    
        public SpdyServer(SslContext context) { //2
            this.context = context;
        }
    
        public ChannelFuture start(InetSocketAddress address) {
            ServerBootstrap bootstrap  = new ServerBootstrap(); //3
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new SpdyChannelInitializer(context)); //4
            ChannelFuture future = bootstrap.bind(address); //5
            future.syncUninterruptibly();
            channel = future.channel();
            return future;
        }
    
        public void destroy() { //6
            if (channel != null) {
                channel.close();
            }
            group.shutdownGracefully();
        }
    
        public static void main(String[] args) throws Exception {
            if (args.length != 1) {
                System.err.println("Please give port as argument");
                System.exit(1);
            }
            int port = Integer.parseInt(args[0]);
    
            SelfSignedCertificate cert = new SelfSignedCertificate();
            SslContext context = SslContext.newServerContext(cert.certificate(), cert.privateKey());  //7
            final SpdyServer endpoint = new SpdyServer(context);
            ChannelFuture future = endpoint.start(new InetSocketAddress(port));
    
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    endpoint.destroy();
                }
            });
            future.channel().closeFuture().syncUninterruptibly();
        }
    }
    
    1. 构建新的 NioEventLoopGroup 用于处理 I/O
    2. 传递 SSLContext 用于加密
    3. 新建 ServerBootstrap 用于配置服务器
    4. 配置 ServerBootstrap
    5. 绑定服务器用于接收指定地址的连接
    6. 销毁服务器,用于关闭管道和 NioEventLoopGroup
    7. 从 BogusSslContextFactory 获取 SSLContext 。这是一个虚拟实现进行测试。真正的实现将为 SslContext 配置适当的密钥存储库。

    Netty 启动 SpdyServer 并测试

    2018-08-08 11:00 更新

    您要注意的是,为了能够访问 SslEngine 接口,当您使用 Jetty NPN 库需要提供它的位置通过 bootclasspath 的 JVM 参数。(-Xbootclasspath 选项允许您覆盖标准 JDK 附带的实现类)。

    下面的清单显示了特殊的参数(-Xbootclasspath)使用。

    Listing 12.7 SpdyServer implementation

    java -Xbootclasspath/p:<path_to_npn_boot_jar> ....
    

    最简单的方式是使用 Maven 项目管理:

    Listing 12.8 Compile and start SpdyServer with Maven

    $ mvn clean package exec:exec -Pchapter12-SpdyServer
    [INFO] Scanning for projects...
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] Building netty-in-action 0.1-SNAPSHOT
    [INFO] ------------------------------------------------------------------------
    ...
    ...
    ...
    [INFO]
    [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
    [INFO] Building jar: /Users/norman/Documents/workspace-intellij/netty-in-actionprivate/
    target/netty-in-action-0.1-SNAPSHOT.jar
    [INFO]
    [INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action ---
    

    可以用2个浏览器进行测试,一个支持 SPDY 一个不支持,这里我们用的是 Google Chrome (支持 SPDY) 和 Safari。

    浏览器访问 https://127.0.0.1:9999,会显示 SpdyRequestHandler 的处理结果,如下图

    Figure%2012

    Figure 12.4 SPDY supported by Google Chrome

    Google Chrome 的一个很好的功能是可以统计数据,可以很好的看到连接情况。 在浏览器中访问 chrome://net-internals/#spdy 可以看到详细的统计数据

    Figure%2012

    Figure 12.5 SPDY statistics

    若不支持 SPDY ,比如我们用 Safari 浏览器访问 https://127.0.0.1:9999 ,则响应将会用 HttpRequestHandler 处理

    Figure%2012

    Figure 12.7 SPDY not supported by Safari

    Netty UDP 广播

    2018-08-07 13:57 更新

    我们所有的例子这一点利用传输方式称为“单播”:“将消息发送给一个网络拥有唯一地址的目的地”,这种模式支持连接和无连接协议。

    然而,UDP 提供了额外的传输模式对多个接收者发送消息:

    • 多播:传送给一组主机
    • 广播:传送到网络上的所有主机(或子网)

    示例应用程序在本章将说明使用 UDP 广播发送消息,可以接收到所有主机在同一网络。为此我们将使用特殊的“有限广播”或“零”网络地址255.255.255.255。消息发送到这个地址是规定要在本地网络(0.0.0.0)的所有主机和从不转发到其他网络通过路由器。

    下一节将讨论示例应用程序的设计。

    Netty UDP示例

    2018-08-08 11:00 更新

    接下来,在我们的示例应用程序中将会打开一个文件,将每一行作为消息通过 UDP 发到指定的端口。对于熟悉类Unix操作系统的朋友,则可以将其看成一个非常标准的简化版本“syslog(系统日志)”。UDP是一个完美的适合这样的应用程序,因为文件本身就是存储在文件系统中的,所以UDP可以容忍偶尔丢失一行日志文件的事情。此外,应用程序提供了非常有价值的能力有效地处理大量的数据。

    UDP 广播使添加新事件“监视器”接收日志消息一样简单开始一个指定的端口上侦听器程序。然而,这种轻松的访问也提出了一个潜在的安全问题,指出了为什么 UDP 广播往往是在安全的环境中使用。还要注意广播消息可能只能在本地网络,因为路由器经常阻止他们。

    Publish/Subscribe(发布/订阅)

    应用程序,如 syslog 通常归类为“发布/订阅”;生产者或服务发布事件和多个订阅者可以收到它们。

    整体看下这个应用,如下图:

    Figure%2013

    1. 应用监听新文件内容
    2. 事件通过 UDP 广播
    3. 事件监视器监听并显示内容

    Figure 13.1 Application overview

    应用程序有两个组件:广播器和监视器或(可能有多个实例)。为了简单起见我们不会添加身份验证、验证、加密。

    在下一节中我们将开始探索实现中,我们还将讨论 UDP 和 TCP 应用程序开发之间的差异。

    Netty LogEvent的POJO

    2018-08-07 14:12 更新

    我们知道在消息应用中,数据一般是以 POJO 的形式出现的,除了实际的消息数据,可以保存配置或处理消息。在这个应用程序里,消息的单元是一个“事件”。因为数据来自于一个日志文件,我们就将其称之为LogEvent

    清单13.1显示了这个简单的POJO的细节。

    Listing 13.1 LogEvent message

    public final class LogEvent {
        public static final byte SEPARATOR = (byte) ':';
    
        private final InetSocketAddress source;
        private final String logfile;
        private final String msg;
        private final long received;
    
        public LogEvent(String logfile, String msg) { //1
            this(null, -1, logfile, msg);
        }
    
        public LogEvent(InetSocketAddress source, long received, String logfile, String msg) {  //2
            this.source = source;
            this.logfile = logfile;
            this.msg = msg;
            this.received = received;
        }
    
        public InetSocketAddress getSource() { //3
            return source;
        }
    
        public String getLogfile() { //4
            return logfile;
        }
    
        public String getMsg() {  //5
            return msg;
        }
    
        public long getReceivedTimestamp() {  //6
            return received;
        }
    }
    
    1. 构造器用于出站消息
    2. 构造器用于入站消息
    3. 返回发送 LogEvent 的 InetSocketAddress 的资源
    4. 返回用于发送 LogEvent 的日志文件的名称
    5. 返回消息的内容
    6. 返回 LogEvent 接收到的时间

    Netty如何写广播器

    2018-08-08 11:01 更新

    本节的内容是要写一个广播器。下图展示了广播一个 DatagramPacket 在每个日志实体里面的方法:

    Figure%2013

    1. 日志文件
    2. 日志文件中的日志实体
    3. 一个 DatagramPacket 保持一个单独的日志实体

    Figure 13.2 Log entries sent with DatagramPackets

    图13.3表示一个 LogEventBroadcaster 的 ChannelPipeline 的高级视图,说明了 LogEvent 是如何流转的。

    Figure%2013

    Figure 13.3 LogEventBroadcaster: ChannelPipeline and LogEvent flow

    正如我们所看到的,所有的数据传输都封装在 LogEvent 消息里。LogEventBroadcaster 写这些通过在本地端的管道,发送它们通过ChannelPipeline 转换(编码)为一个定制的 ChannelHandler 的DatagramPacket 信息。最后,他们通过 UDP 广播并被远程接收。

    编码器和解码器

    编码器和解码器将消息从一种格式转换为另一种,深度探讨在第7章中进行。我们探索 Netty 提供的基础类来简化和实现自定义 ChannelHandler 如 LogEventEncoder 在这个应用程序中。

    下面展示了 编码器的实现

    Listing 13.2 LogEventEncoder

    public class LogEventEncoder extends MessageToMessageEncoder<LogEvent> {
        private final InetSocketAddress remoteAddress;
    
        public LogEventEncoder(InetSocketAddress remoteAddress) {  //1
            this.remoteAddress = remoteAddress;
        }
    
        @Override
        protected void encode(ChannelHandlerContext channelHandlerContext, LogEvent logEvent, List<Object> out) throws Exception {
            byte[] file = logEvent.getLogfile().getBytes(CharsetUtil.UTF_8); //2
            byte[] msg = logEvent.getMsg().getBytes(CharsetUtil.UTF_8);
            ByteBuf buf = channelHandlerContext.alloc().buffer(file.length + msg.length + 1);
            buf.writeBytes(file);
            buf.writeByte(LogEvent.SEPARATOR); //3
            buf.writeBytes(msg);  //4
            out.add(new DatagramPacket(buf, remoteAddress));  //5
        }
    }
    
    1. LogEventEncoder 创建了 DatagramPacket 消息类发送到指定的 InetSocketAddress
    2. 写文件名到 ByteBuf
    3. 添加一个 SEPARATOR
    4. 写一个日志消息到 ByteBuf
    5. 添加新的 DatagramPacket 到出站消息

    为什么使用 MessageToMessageEncoder?

    当然我们可以编写自己的自定义 ChannelOutboundHandler 来转换 LogEvent 对象到 DatagramPackets。但是继承自MessageToMessageEncoder 为我们简化和做了大部分的工作。

    为了实现 LogEventEncoder,我们只需要定义服务器的运行时配置,我们称之为“bootstrapping(引导)”。这包括设置各种 ChannelOption 并安装需要的 ChannelHandler 到 ChannelPipeline 中。完成的 LogEventBroadcaster 类,如清单13.3所示。

    Listing 13.3 LogEventBroadcaster

    public class LogEventBroadcaster {
        private final Bootstrap bootstrap;
        private final File file;
        private final EventLoopGroup group;
    
        public LogEventBroadcaster(InetSocketAddress address, File file) {
            group = new NioEventLoopGroup();
            bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioDatagramChannel.class)
                    .option(ChannelOption.SO_BROADCAST, true)
                    .handler(new LogEventEncoder(address)); //1
    
            this.file = file;
        }
    
        public void run() throws IOException {
            Channel ch = bootstrap.bind(0).syncUninterruptibly().channel(); //2
            System.out.println("LogEventBroadcaster running");
            long pointer = 0;
            for (;;) {
                long len = file.length(); 
                if (len < pointer) {
                    // file was reset
                    pointer = len; //3
                } else if (len > pointer) {
                    // Content was added
                    RandomAccessFile raf = new RandomAccessFile(file, "r");
                    raf.seek(pointer);  //4
                    String line;
                    while ((line = raf.readLine()) != null) {
                        ch.writeAndFlush(new LogEvent(null, -1, file.getAbsolutePath(), line));  //5
                    }
                    pointer = raf.getFilePointer(); //6
                    raf.close();
                }
                try {
                    Thread.sleep(1000);  //7
                } catch (InterruptedException e) {
                    Thread.interrupted();
                    break;
                }
            }
        }
    
        public void stop() {
            group.shutdownGracefully();
        }
    
        public static void main(String[] args) throws Exception {
            if (args.length != 2) {
                throw new IllegalArgumentException();
            }
    
            LogEventBroadcaster broadcaster = new LogEventBroadcaster(new InetSocketAddress("255.255.255.255",
                    Integer.parseInt(args[0])), new File(args[1]));  //8
            try {
                broadcaster.run();
            } finally {
                broadcaster.stop();
            }
        }
    }
    
    1. 引导 NioDatagramChannel 。为了使用广播,我们设置 SO_BROADCAST 的 socket 选项
    2. 绑定管道。注意当使用 Datagram Channel 时,是没有连接的
    3. 如果需要,可以设置文件的指针指向文件的最后字节
    4. 设置当前文件的指针,这样不会把旧的发出去
    5. 写一个 LogEvent 到管道用于保存文件名和文件实体。(我们期望每个日志实体是一行长度)
    6. 存储当前文件的位置,这样,我们可以稍后继续
    7. 睡 1 秒。如果其他中断退出循环就重新启动它。
    8. 构造一个新的实例 LogEventBroadcaster 并启动它

    这就是程序的完整的第一部分。可以使用 "netcat" 程序查看程序的结果。在 UNIX/Linux 系统,可以使用 "nc", 在 Windows 环境下,可以在 http://nmap.org/ncat找到

    Netcat 是完美的第一个测试我们的应用程序;它只是监听指定的端口上接收并打印所有数据到标准输出。将其设置为在端口 9999 上监听 UDP 数据如下:

    $ nc -l -u 9999
    

    现在我们需要启动 LogEventBroadcaster。清单13.4显示了如何使用 mvn 编译和运行广播器。pom的配置。pom.xml 配置指向一个文件/var/log/syslog(假设是UNIX / Linux环境)和端口设置为 9999。文件中的条目将通过 UDP 广播到端口,在你开始 netcat 后打印到控制台。

    Listing 13.4 Compile and start the LogEventBroadcaster

    $ mvn clean package exec:exec -Pchapter13-LogEventBroadcaster
    [INFO] Scanning for projects...
    [INFO]
    [INFO] --------------------------------------------------------------------
    [INFO] Building netty-in-action 0.1-SNAPSHOT
    [INFO] --------------------------------------------------------------------
    ...
    ...
    [INFO]
    [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
    [INFO] Building jar: /Users/norman/Documents/workspace-intellij/netty-in-actionprivate/
    target/netty-in-action-0.1-SNAPSHOT.jar
    [INFO]
    [INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action -
    LogEventBroadcaster running
    

    当调用 mvn 时,在系统属性中改变文件和端口值,指定你想要的。清单13.5 设置日志文件 到 /var/log/mail.log 和端口 8888。

    Listing 13.5 Compile and start the LogEventBroadcaster

    $ mvn clean package exec:exec -Pchapter13-LogEventBroadcaster /
    -Dlogfile=/var/log/mail.log -Dport=8888 -....
    ....
    [INFO]
    [INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action -
    LogEventBroadcaster running
    

    当看到 “LogEventBroadcaster running” 说明程序运行成功了。

    netcat 只用于测试,但不适合生产环境中使用。

    Netty如何写监视器

    2018-08-08 11:01 更新

    这一节我们学习监视器的编写:EventLogMonitor ,也就是用来接收事件的程序,用来代替 netcat 。EventLogMonitor 做下面事情:

    • 接收 LogEventBroadcaster 广播的 UDP DatagramPacket
    • 解码 LogEvent 消息
    • 输出 LogEvent 消息

    和之前一样,将实现自定义 ChannelHandler 的逻辑。图13.4描述了LogEventMonitor 的 ChannelPipeline 并表明了 LogEvent 的流经情况。

    Figure%2013

    Figure 13.4 LogEventMonitor

    图中显示我们的两个自定义 ChannelHandlers,LogEventDecoder 和 LogEventHandler。首先是负责将网络上接收到的 DatagramPacket 解码到 LogEvent 消息。清单13.6显示了实现。

    Listing 13.6 LogEventDecoder

    public class LogEventDecoder extends MessageToMessageDecoder<DatagramPacket> {
        @Override
        protected void decode(ChannelHandlerContext ctx, DatagramPacket datagramPacket, List<Object> out) throws Exception {
            ByteBuf data = datagramPacket.content(); //1
            int i = data.indexOf(0, data.readableBytes(), LogEvent.SEPARATOR);  //2
            String filename = data.slice(0, i).toString(CharsetUtil.UTF_8);  //3
            String logMsg =  data.slice(i + 1, data.readableBytes()).toString(CharsetUtil.UTF_8);  //4
    
            LogEvent event = new LogEvent(datagramPacket.recipient(), System.currentTimeMillis(),
                    filename,logMsg); //5
            out.add(event);
        }
    }
    
    1. 获取 DatagramPacket 中数据的引用
    2. 获取 SEPARATOR 的索引
    3. 从数据中读取文件名
    4. 读取数据中的日志消息
    5. 构造新的 LogEvent 对象并将其添加到列表中

    第二个 ChannelHandler 将执行一些首先创建的 LogEvent 消息。在这种情况下,我们只会写入 system.out。在真实的应用程序可能用到一个单独的日志文件或放到数据库。

    下面的清单显示了 LogEventHandler。

    Listing 13.7 LogEventHandler

    public class LogEventHandler extends SimpleChannelInboundHandler<LogEvent> { //1
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace(); //2
            ctx.close();
        }
    
        @Override
        public void channelRead0(ChannelHandlerContext channelHandlerContext, LogEvent event) throws Exception {
            StringBuilder builder = new StringBuilder(); //3
            builder.append(event.getReceivedTimestamp());
            builder.append(" [");
            builder.append(event.getSource().toString());
            builder.append("] [");
            builder.append(event.getLogfile());
            builder.append("] : ");
            builder.append(event.getMsg());
    
            System.out.println(builder.toString()); //4
        }
    }
    
    1. 继承 SimpleChannelInboundHandler 用于处理 LogEvent 消息
    2. 在异常时,输出消息并关闭 channel
    3. 建立一个 StringBuilder 并构建输出
    4. 打印出 LogEvent 的数据

    LogEventHandler 打印出 LogEvent 的一个易读的格式,包括以下:

    • 收到时间戳以毫秒为单位
    • 发送方的 InetSocketAddress,包括IP地址和端口
    • LogEvent 生成绝对文件名
    • 实际的日志消息,代表在日志文件中一行

    现在我们需要安装处理程序到 ChannelPipeline ,如图13.4所示。下一个清单显示了这是如何实现 LogEventMonitor 类的一部分。

    Listing 13.8 LogEventMonitor

    public class LogEventMonitor {
    
        private final Bootstrap bootstrap;
        private final EventLoopGroup group;
        public LogEventMonitor(InetSocketAddress address) {
            group = new NioEventLoopGroup();
            bootstrap = new Bootstrap();
            bootstrap.group(group)  //1
                    .channel(NioDatagramChannel.class)
                    .option(ChannelOption.SO_BROADCAST, true)
                    .handler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel channel) throws Exception {
                            ChannelPipeline pipeline = channel.pipeline();
                            pipeline.addLast(new LogEventDecoder());  //2
                            pipeline.addLast(new LogEventHandler());
                        }
                    }).localAddress(address);
    
        }
    
        public Channel bind() {
            return bootstrap.bind().syncUninterruptibly().channel();  //3
        }
    
        public void stop() {
            group.shutdownGracefully();
        }
    
        public static void main(String[] args) throws Exception {
            if (args.length != 1) {
                throw new IllegalArgumentException("Usage: LogEventMonitor <port>");
            }
            LogEventMonitor monitor = new LogEventMonitor(new InetSocketAddress(Integer.parseInt(args[0])));  //4
            try {
                Channel channel = monitor.bind();
                System.out.println("LogEventMonitor running");
    
                channel.closeFuture().await();
            } finally {
                monitor.stop();
            }
        }
    }
    
    1. 引导 NioDatagramChannel。设置 SO_BROADCAST socket 选项。
    2. 添加 ChannelHandler 到 ChannelPipeline
    3. 绑定的通道。注意,在使用 DatagramChannel 是没有连接,因为这些 无连接
    4. 构建一个新的 LogEventMonitor

    Netty 运行LogEventBroadcaster和LogEventMonitor

    2018-08-07 14:31 更新

    如上所述,我们将使用 Maven 来运行应用程序。这一次你需要打开两个控制台窗口给每个项目。用 Ctrl-C 可以停止它。

    首先我们将启动 LogEventBroadcaster 如清单13.4所示,除了已经构建项目以下命令即可(使用默认值):

    $ mvn exec:exec -Pchapter13-LogEventBroadcaster
    

    和之前一样,这将通过 UDP 广播日志消息。

    现在,在一个新窗口,构建和启动 LogEventMonitor 接收和显示广播消息。

    Listing 13.9 Compile and start the LogEventBroadcaster

    $ mvn clean package exec:exec -Pchapter13-LogEventMonitor
    [INFO] Scanning for projects...
    [INFO]
    [INFO] --------------------------------------------------------------------
    [INFO] Building netty-in-action 0.1-SNAPSHOT
    [INFO] --------------------------------------------------------------------
    ...
    [INFO]
    [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
    [INFO] Building jar: /Users/norman/Documents/workspace-intellij/netty-in-actionprivate/
    target/netty-in-action-0.1-SNAPSHOT.jar
    [INFO]
    [INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action ---
    LogEventMonitor running
    

    当看到 “LogEventMonitor running” 说明程序运行成功了。

    控制台将显示任何事件被添加到日志文件中,如下所示。消息的格式是由LogEventHandler 创建。

    Listing 13.10 LogEventMonitor output

    1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:55:08 dev-linux
    dhclient: DHCPREQUEST of 192.168.0.50 on eth2 to 192.168.0.254 port 67
    1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:55:08 dev-linux
    dhclient: DHCPACK of 192.168.0.50 from 192.168.0.254
    1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:55:08 dev-linux
    dhclient: bound to 192.168.0.50 -- renewal in 270 seconds.
    1364217299382 [/192.168.0.38:63182] [[/var/log/messages] : Mar 25 13:59:38 dev-linux
    dhclient: DHCPREQUEST of 192.168.0.50 on eth2 to 192.168.0.254 port 67
    1364217299382 [/192.168.0.38:63182] [/[/var/log/messages] : Mar 25 13:59:38 dev-linux
    dhclient: DHCPACK of 192.168.0.50 from 192.168.0.254
    1364217299382 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 13:59:38 dev-linux
    dhclient: bound to 192.168.0.50 -- renewal in 259 seconds.
    1364217299383 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 14:03:57 dev-linux
    dhclient: DHCPREQUEST of 192.168.0.50 on eth2 to 192.168.0.254 port 67
    1364217299383 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 14:03:57 dev-linux
    dhclient: DHCPACK of 192.168.0.50 from 192.168.0.254
    1364217299383 [/192.168.0.38:63182] [/var/log/messages] : Mar 25 14:03:57 dev-linux
    dhclient: bound to 192.168.0.50 -- renewal in 285 seconds.
    

    若你没有访问 UNIX syslog 的权限,可以创建 自定义的文件,手动填入内容。下面是 UNIX 命令用 touch 创建一个空文件

    $ touch ~/mylog.log
    

    再次启动 LogEventBroadcaster,设置系统属性

    $ mvn exec:exec -Pchapter13-LogEventBroadcaster -Dlogfile=~/mylog.log
    

    当 LogEventBroadcaster 运行时,你可以手动的添加消息到文件来查看广播到 LogEventMonitor 控制台的内容。使用 echo 和输出的文件

    $ echo ’Test log entry’ >> ~/mylog.log
    

    你可以启动任意个监视器实例,他们都会收到相同的消息。

    Netty 线程模型的总览

    2020-11-26 10:30 更新

    本节将简单介绍一般的线程模型,Netty 中如何使用指定的线程模型,以及 Netty 过去不同的版本中使用的线程模型。你会更好的理解不同的线程模型的所有利弊。

    一个线程模型指定代码执行,给开发人员如何执行他们代码的信息。这很重要,因为它允许开发人员事先知道如何保护他们的代码免受并发执行的副作用。若没有这个知识背景,即使是最好的开发人员都只能是碰运气,希望到最后都能这么幸运,但这几乎是不可能的。进入更多的细节之前,提供一个更好的理解主题的回顾这些天大多数应用程序做什么。

    大多数现代应用程序使用多个线程调度工作,因此让应用程序使用所有可用的系统资源以有效的方式。这使得很多有意义,因为大部分硬件有不止一个甚至多个CPU核心。如果一切都只有一个 Thread 执行,不可能完全使用所提供的资源。为了解决这个问题,许多应用程序执行多个 Thread 的运行代码。在早期的 Java,这样做是通过简单地按需创建新 Thread 时,并发工作需要做。

    但很快就发现,这不是完美的,因为创建 Thread 和回收会给他们带来的开销。在 Java 5 中,我们终于有了所谓的线程池,经常缓存 Thread,用来消除创建和回收 Thread 的开销。这些池由 Executor 接口提供。Java 5 提供了许多有用的实现,在其内部发生显著的变化,但思想都一脉相承的。创建 Thread 和重用他们提交一个任务时执行。这可以帮助创建和回收线程的开销降到最低。

    下图显示使用一个线程池执行一个任务,提交一个任务后会使用线程池中空闲的线程来执行,完成任务后释放线程并将线程重新放回线程池:

    Figure%2015

    1. Runnable 表示要执行的任务。这可能是任何东西,从一个数据库调用文件系统清理。
    2. 之前 runnable 移交到线程池。
    3. 闲置的线程被用来执行任务。当一个线程运行结束之后,它将回到闲置线程的列表新任务需要运行时被重用。
    4. 线程执行任务

    Figure 15.1 Executor execution logic

    这个修复 Thread 创建和回收的开销,不需要每个新任务创建和销毁新的 Thread 。

    但使用多个 Thread 提供了资源和管理成本,作为一个副作用,引入了太多的上下文切换。这种会随着运行的线程的数量和任务执行的数量的增加而恶化。尽管使用多个线程在开始时似乎不是一个问题,但一旦你把真正工作负载放在系统上,可以会遭受到重击。

    除了这些技术的限制和问题,其他问题可能发生在相关的维护应用程序 / 框架在未来或在项目的生命周期里。有效地说,增加应用程序的复杂性取决于对比。当状态简单时,写一个多线程应用程序是一个辛苦的工作!你能解决这个问题吗?在实际的场景中需要多个 Thread 规模,这是一个事实。让我们看看 Netty 是解决这个问题。

    Netty事件循环EventLoop

    2018-08-08 11:03 更新

    事件循环的意思就是:它运行在一个循环中,直到它停止。网络框架需要需要在一个循环中为一个特定的连接运行事件,所以这符合网络框架的设计。在Netty之前,已经有其他框架和实现这么做了。

    下面的清单显示了典型的 EventLoop 逻辑。请注意这是为了更好的说明这个想法而不是单单展示 Netty 实现本身。

    Listing 14.1 Execute task in EventLoop

    while (!terminated) {
        List<Runnable> readyEvents = blockUntilEventsReady(); //1
        for (Runnable ev: readyEvents) {
            ev.run(); //2
        }
    }
    
    1. 阻塞直到事件可以运行
    2. 循环所有事件,并运行他们

    在 Netty 中使用 EventLoop 接口代表事件循环,EventLoop 是从EventExecutor 和 ScheduledExecutorService 扩展而来,所以可以将任务直接交给 EventLoop 执行。类关系图如下:

    Figure%2015

    Figure 15.2 EventLoop class hierarchy

    EventLoop 是完全由一个 Thread,从未改变。为了更合理利用资源,根据配置和可用的内核, Netty 可以使用多个 EventLoop。

    事件/任务执行顺序

    一个重要的细节关于事件和任务的执行顺序是,事件/任务执行顺序按照FIFO(先进先出)。这是必要的,因为否则事件不能按顺序处理,所处理的字节将不能保证正确的顺序。这将导致问题,所以这个不是所允许的设计。

    Netty 4 中的 I/O 和事件处理

    Netty 使用 I/O 事件,b被各种 I/O 操作运输本身所触发。 这些 I/O 操作,例如网络 API 的一部分,由Java 和底层操作系统提供。

    一个区别在于,一些操作(或者事件)是由 Netty 的本身的传输实现触发的,一些是由用户自己。例如读事件通常是由传输本身在读取一些数据时触发。相比之下,写事件通常是由用户本身,例如,当调用 Channel.write(…)。

    究竟需要做一次处理一个事件取决于事件的性质。经常会读网络栈的数据转移到您的应用程序。有时它会在另一个方向做同样的事情,例如,把数据从应用程序到网络堆栈(内核)发送到它的远端。但不限于这种类型的事务;重要的是,所使用的逻辑是通用的,灵活地处理各种各样的用例。

    I/O 和事件处理的一个重要的事情在 Netty 4,是每一个 I/O 操作和事件总是由 EventLoop 本身处理,以及分配给 EventLoop 的 Thread。

    我们应该注意,Netty 不总是使用我们描述的线程模型(通过 EventLoop 抽象)。在下一节中,你会了解 Netty 3 中使用的线程模型。这将帮助你理解为什么现在用新的线程模型以及为什么使用取代了 Netty 3 中仍然使用的旧模式。

    Netty 3 中的 I/O 操作

    在以前的版本中,线程模型是不同的。Netty 保证只将入站(以前称为 upstream)事件在执行 I/O Thread 执行 (I/O Thread 现在在 Netty 4 叫 EventLoop )。所有的出站(以前称为 downstream)事件被调用Thread 处理,这可能是 I/O Thread 也可以能是其他 Thread。 这听起来像一个好主意,但原来是容易出错,因为处理 ChannelHandler需要小心的出站事件同步,因为它没有保证只有一个线程运行在同一时间。这可能会发生如果你触发 downstream 事件同时在一个管道时;例如,您 调用 Channel.write(..) 在不同的线程。

    除了需要负担同步 ChannelHandler,这个线程模型的另一个问题是你可能需要去掉一个入站事件作为一个出站事件的结果,例如 Channel.write(..) 操作导致异常。在这种情况下,exceptionCaught 必须生成并抛出去。乍看之下这不像是一个问题,但我们知道, exceptionCaught 由入站事件涉及,会让你知道问题出在哪里。问题是,事实上,你现在的情况是在调用 Thread 上执行,但 exceptionCaught 事件必须交给工作线程来执行,这样上下文切换是必须的。

    相比之下,Netty 4 新线程模型根本没有这些问题,因为一切都在同一个EventLoop 在同一 Thread 中 执行。这消除了需要同步ChannelHandler ,并且使它更容易为用户理解执行。

    现在你知道 EventLoop 如何执行任务,它的时间来快速浏览下 Netty 的各种内部功能。

    Netty 线程模型的内部

    Netty 的内部实现使其线程模型表现优异,它会检查正在执行的 Thread 是否是已分配给实际 Channel (和 EventLoop),在 Channel 的生命周期内,EventLoop 负责处理所有的事件。

    如果 Thread 是相同的 EventLoop 中的一个,讨论的代码块被执行;如果线程不同,它安排一个任务并在一个内部队列后执行。通常是通过EventLoop 的 Channel 只执行一次下一个事件,这允许直接从任何线程与通道交互,同时还确保所有的 ChannelHandler 是线程安全,不需要担心并发访问问题。

    下图显示在 EventLoop 中调度任务执行逻辑,这适合 Netty 的线程模型:

    Figure%2015

    1. 应在 EventLoop 中执行的任务
    2. 任务传递到执行方法后,执行检查来检测调用线程是否是与分配给 EventLoop 是一样的
    3. 线程是一样的,说明你在 EventLoop 里,这意味着可以直接执行的任务
    4. 线程与 EventLoop 分配的不一样。当 EventLoop 事件执行时,队列的任务再次执行一次

    15.5 EventLoop execution logic/flow

    设计是非常重要的,以确保不要把任何长时间运行的任务放在执行队列中,因为长时间运行的任务会阻止其他在相同线程上执行的任务。这多少会影响整个系统依赖于 EventLoop 实现用于特殊传输的实现。

    传输之间的切换在你的代码库中可能没有任何改变,重要的是:切勿阻塞 I/O 线程。如果你必须做阻塞调用(或执行需要长时间才能完成的任务),使用 EventExecutor。

    下一节将讲解一个在应用程序中经常使用的功能,就是调度执行任务(定期执行)。Java对这个需求提供了解决方案,但 Netty 提供了几个更好的方案

    Netty利用EventLoop实现调度任务执行

    2018-08-08 11:05 更新

    我们每隔一段时间就需要调度任务执行,或许你想要注册一个任务在客户端完成连接5分钟后执行,一个比较常见的用例是将一个信息发送给远端,看下远端是否有反应,如果没有就可以关闭通道(连接)并且释放资源。

    本节的内容是介绍如何使用强大的 EventLoop 实现任务调度,还会简单介绍 Java API的任务调度,以方便和 Netty 比较加深理解。

    使用普通的 Java API 调度任务

    在 Java 中使用 JDK 提供的 ScheduledExecutorService 实现任务调度。使用 Executors 提供的静态方法创建 ScheduledExecutorService,有如下方法

    Table 15.1 java.util.concurrent.Executors-Static methods to create a ScheduledExecutorService

    方法描述
    newScheduledThreadPool(int corePoolSize) newScheduledThreadPool(int corePoolSize,ThreadFactorythreadFactory) 创建一个新的

    ScheduledThreadExecutorService 用于调度命令来延迟或者周期性的执行。 corePoolSize 用于计算线程的数量 newSingleThreadScheduledExecutor() newSingleThreadScheduledExecutor(ThreadFact orythreadFactory) | 新建一个 ScheduledThreadExecutorService 可以用于调度命令来延迟或者周期性的执行。它将使用一个线程来执行调度的任务

    下面的 ScheduledExecutorService 调度任务 60 执行一次

    Listing 15.4 Schedule task with a ScheduledExecutorService

    ScheduledExecutorService executor = Executors
            .newScheduledThreadPool(10); //1
    
    ScheduledFuture<?> future = executor.schedule(
            new Runnable() { //2
                @Override
                public void run() {
                    System.out.println("Now it is 60 seconds later");  //3
                }
            }, 60, TimeUnit.SECONDS);  //4
    // do something
    //
    
    executor.shutdown();  //5
    
    1. 新建 ScheduledExecutorService 使用10个线程
    2. 新建 runnable 调度执行
    3. 稍后运行
    4. 调度任务60秒后执行
    5. 关闭 ScheduledExecutorService 来释放任务完成的资源

    使用 EventLoop 调度任务

    使用 ScheduledExecutorService 工作的很好,但是有局限性,比如在一个额外的线程中执行任务。如果需要执行很多任务,资源使用就会很严重;对于像 Netty 这样的高性能的网络框架来说,严重的资源使用是不能接受的。Netty 对这个问题提供了很好的方法。

    Netty 允许使用 EventLoop 调度任务分配到通道,如下面代码:

    Listing 15.5 Schedule task with EventLoop

    Channel ch = null; // Get reference to channel
    ScheduledFuture<?> future = ch.eventLoop().schedule(
            new Runnable() {
                @Override
                public void run() {
                    System.out.println("Now its 60 seconds later");
                }
            }, 60, TimeUnit.SECONDS);
    
    1. 新建 runnable 用于执行调度
    2. 稍后执行
    3. 调度任务60秒后运行

    如果想任务每隔多少秒执行一次,看下面代码:

    Listing 15.6 Schedule a fixed task with the EventLoop

    Channel ch = null; // Get reference to channel
    ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
            new Runnable() {
                @Override
                public void run() {
                    System.out.println("Run every 60 seconds");
                }
            }, 60, 60, TimeUnit.SECONDS);
    
    1. 新建 runnable 用于执行调度
    2. 将运行直到 ScheduledFuture 被取消
    3. 调度任务60秒运行

    取消操作,可以使用 ScheduledFuture 返回每个异步操作。 ScheduledFuture 提供一个方法用于取消一个调度了的任务或者检查它的状态。一个简单的取消操作如下:

    ScheduledFuture<?> future = ch.eventLoop()
    .scheduleAtFixedRate(..); //1
    // Some other code that runs...
    future.cancel(false); //2
    
    1. 调度任务并获取返回的 ScheduledFuture
    2. 取消任务,阻止它再次运行

    调度的内部实现

    Netty 内部实现其实是基于George Varghese 提出的 “Hashed and hierarchical timing wheels: Data structures to efficiently implement timer facility(散列和分层定时轮:数据结构有效实现定时器)”。这种实现只保证一个近似执行,也就是说任务的执行可能不是100%准确;在实践中,这已经被证明是一个可容忍的限制,不影响多数应用程序。所以,定时执行任务不可能100%准确的按时执行。

    为了更好的理解它是如何工作,我们可以这样认为:

    • 在指定的延迟时间后调度任务;
    • 任务被插入到 EventLoop 的 Schedule-Task-Queue(调度任务队列);
    • 如果任务需要马上执行,EventLoop 检查每个运行;
    • 如果有一个任务要执行,EventLoop 将立刻执行它,并从队列中删除;
    • EventLoop 等待下一次运行,从第4步开始一遍又一遍的重复。

    因为这样的实现计划执行不可能100%正确,对于多数用例不可能100%准备的执行计划任务;在 Netty 中,这样的工作几乎没有资源开销。

    但是如果需要更准确的执行呢?很容易,你需要使用ScheduledExecutorService 的另一个实现,这不是 Netty 的内容。记住,如果不遵循 Netty 的线程模型协议,你将需要自己同步并发访问。

    Netty I/O和EventLoop/Thread的分配细节

    2018-08-07 15:55 更新

    Netty 使用一个包含 EventLoop 的 EventLoopGroup 为 Channel 的 I/O 和事件服务。EventLoop 创建并分配方式不同基于传输的实现。异步实现使用只有少数 EventLoop(和 Threads)共享于 Channel 之间 。这允许最小线程数服务多个 Channel,不需要为他们每个人都有一个专门的 Thread。

    图15.7显示了如何使用 EventLoopGroup。

    .jpg)

    1. 所有的 EventLoop 由 EventLoopGroup 分配。这里它将使用三个EventLoop 实例
    2. 这个 EventLoop 处理所有分配给它管道的事件和任务。每个EventLoop 绑定到一个 Thread
    3. 管道绑定到 EventLoop,所以所有操作总是被同一个线程在 Channel 的生命周期执行。一个管道属于一个连接

    Figure 15.7 Thread allocation for nonblocking transports (such as NIO and AIO)

    如图所述,使用有 3个 EventLoop (每个都有一个 Thread ) EventLoopGroup 。EventLoop (同时也是 Thread )直接当 EventLoopGroup 创建时分配。这样保证资源是可以使用的

    这三个 EventLoop 实例将会分配给每个新创建的 Channel。这是通过EventLoopGroup 实现,管理 EventLoop 实例。实际实现会照顾所有EventLoop 实例上均匀的创建 Channel (同样是不同的 Thread)。

    一旦 Channel 是分配给一个 EventLoop,它将使用这个 EventLoop 在它的生命周期里和同样的线程。你可以,也应该,依靠这个,因为它可以确保你不需要担心同步(包括线程安全、可见性和同步)在你 ChannelHandler实现。

    但是这也会影响使用 ThreadLocal,例如,经常使用的应用程序。因为一个EventLoop 通常影响多个 Channel,ThreadLocal 将相同的 Channel 分配给 EventLoop。因此,它适合状态跟踪等等。它仍然可以用于共享重或昂贵的对象之间的 Channel ,不再需要保持状态,因此它可以用于每个事件,而不需要依赖于先前 ThreadLocal 的状态。

    EventLoop 和 Channel

    我们应该注意,在 Netty 4 , Channel 可能从 EventLoop 注销稍后又从不同 EventLoop 注册。这个功能是不赞成,因为它在实践中没有很好的工作

    语义跟其他传输略有不同,如 OIO(Old Blocking I/O)运输,可以看到如图14.8所示。

    .jpg)

    1. 所有 EventLoop 从 EventLoopGroup 分配。每个新的 channel 将会获得新的 EventLoop
    2. EventLoop 分配给 channel 用于执行所有事件和任务
    3. Channel 绑定到 EventLoop。一个 channel 属于一个连接

    Figure 15.8 Thread allocation of blocking transports (such as OIO)

    你可能会注意到这里,一个 EventLoop (也是一个 Thread)创建每个 Channel。你可能被用来从开发网络应用程序是基于常规阻塞I/O在使用java.io.* 包。但即使语义变化在这种情况下,有一件事仍然是相同的:每个 I/O 通道将由一次只有一个线程来处理,这是一个线程增强 Channel 的 EventLoop。可以依靠这个硬性的规则,使 Netty 的框架很容易与其他网络框架进行比较。

  • 相关阅读:
    Spring框架总结(二)
    java.lang.ClassCastException: com.liuyang.annocation.UserAction cannot be cast to com.liuyang.annocation2.UserAction at com.liuyang.annocation2.App.test
    Spring框架总结(一)
    Error creating bean with name 'us' defined in class path resource [com/liuyang/test/DI/beans2.xml]: Cannot resolve reference to bean 'daoa' while setting bean property 'daoa'; nested exception is org.
    互联网系统架构的演进
    重新理解:ASP.NET 异步编程(转)
    Git初级使用教程(转)
    JavaScript开发规范要求
    大型网站架构演化发展历程(转)
    Bootstrap 学习(1)
  • 原文地址:https://www.cnblogs.com/hanease/p/16197991.html
Copyright © 2020-2023  润新知