概述

Netty本身的是什么,它是什么特性等问题本文暂不做解释,日后学习过程中,切身体会了之后,再来讲一讲。

Netty能干什么,主要如下

  1. 能作为一个RPC的通讯框架,基于socket方式实现服务间的远程调用
  2. 能作为开发长连接业务场景的基础(websocket),实现客户端和服务端的长连接通讯
  3. 可作为http的服务器,类似于tomcat等server容器,只不过它不是基础servlet的,它并没有实现相关servlet的能力,有自己的一套处理方式

示例

对于Netty初学者来说(像我这种人QAQ),Netty的上手是很难的一件事情,官方的文档并不是很友好,网络上的讲解也不是很清楚,所以很直观的就是写一个接收到请求并相应demo来感受一下Netty在处理请求时候的大致流程。以下是Netty的“Hello word”

由于使用的是Gradle构建的项目,引入如下依赖

plugins {
    id 'java'
}

group 'com.leolee'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'

    implementation("io.netty:netty-all:4.1.51.Final")
}

注意Netty的包有很多,我们要直接引入Netty-all就可以了,everything in one肯定没错

需要创建如下三个测试类

  • TestServer
  • TestServerInitializer
  • TestHttpServerHandler
package com.leolee.netty.firstExample;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @ClassName TestServer
 * @Description: 服务端,简单的实现客户端调用服务端的时候返回一个字符串的场景
 * @Author LeoLee
 * @Date 2020/8/22
 * @Version V1.0
 **/
public class TestServer {

    public static void main(String[] args) throws InterruptedException {
        //定义线程组 EventLoopGroup为死循环
        //boss线程组一直在接收客户端发起的请求,但是不对请求做处理,boss会将接收到的请i交给worker线程组来处理
        //实际可以用一个线程组来做客户端的请求接收和处理两件事,但是不推荐
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //启动类定义
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new TestServerInitializer());//子处理器,自定义处理器

            //绑定监听端口
            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            //定义关闭监听
            channelFuture.channel().closeFuture().sync();
        } finally {
            //Netty提供的优雅关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
package com.leolee.netty.firstExample;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;

/**
 * @ClassName TestServerInitializer
 * @Description: 初始化管道并绑定处理器
 * @Author LeoLee
 * @Date 2020/8/22
 * @Version V1.0
 **/
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {

        //声明管道
        ChannelPipeline pipeline = ch.pipeline();
        //在管道最后增加一个处理器:HttpServerCodec,对请求进行编解码用
        pipeline.addLast("HttpServerCodec", new HttpServerCodec());
        //绑定自定义的处理提
        pipeline.addLast("TestHttpServerHandler", new TestHttpServerHandler());
    }
}
package com.leolee.netty.firstExample;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

/**
 * @ClassName TestHttpServerHandler
 * @Description: 自定义请求处理器
 * @Author LeoLee
 * @Date 2020/8/22
 * @Version V1.0
 **/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    /**
     * 功能描述: <br> 读取客户端请求并返回相应
     * @Param: [ctx, msg]
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/8/22 16:03
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {

        //构造返回内容
        ByteBuf content = Unpooled.copiedBuffer("Hello word", CharsetUtil.UTF_8);
        //构造相应包体
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
        //构造响应头
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
        //返回客户端
        ctx.writeAndFlush(response);
    }
}

由于ServerBootstrap启动类实际上是持续在运行的,所以我们运行main方法后控制台如下

netty长连接做负载均衡 netty http长连接_.net

请求http://localhost:8899/

netty长连接做负载均衡 netty http长连接_.net_02

重要组件如下:

  • EventLoopGroup
  • ServerBootstrap
  • 内置处理或自定义处理器

所以总结一下执行流程如下:

  1. 创建并启动ServerBootstrap服务器
  2. ServerBootstrap服务器接收两个线程组,一个负责接收请求,一个负责处理请求
  3. ServerBootstrap要绑定子处理器,可添加内置处理和自定义处理器来处理业务请求

优化

有些人可能看到,浏览器请求发出了两次请求,多的是一次请求是去获取网页的icon图标了,这时候浏览器对服务端监听的8899端口进行了两次请求,因为服务端只监听了8899,所以多出的这次icon请求我们也做了业务处理

所以对自定义处理器改造,如下:

package com.leolee.netty.firstExample;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

import java.net.URI;

/**
 * @ClassName TestHttpServerHandler
 * @Description: 自定义请求处理器
 * @Author LeoLee
 * @Date 2020/8/22
 * @Version V1.0
 **/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    /**
     * 功能描述: <br> 读取客户端请求并返回相应
     * @Param: [ctx, msg]
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/8/22 16:03
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {

        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            System.out.println("这是一次" + request.method().name() + "请求");
            URI uri = new URI(request.uri());
            System.out.println(uri.getPath());
            if (uri.getPath().equals("/favicon.ico")) {
                System.out.println("favicon.ico请求");
                return;
            }

            //构造返回内容
            ByteBuf content = Unpooled.copiedBuffer("Hello word", CharsetUtil.UTF_8);
            //构造相应包体
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
            //构造响应头
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
            //返回客户端
            ctx.writeAndFlush(response);
        }
    }
}

过滤掉浏览器的网页icon请求的处理

netty长连接做负载均衡 netty http长连接_netty长连接做负载均衡_03

回调过程

在自定义处理器 TestHttpSeverHandler 中,其继承了 SimpleChannelInboundHandler 抽象类,SimpleChannelInboundHandler 又继承了 ChannelInboundHandlerAdapter 。我们先忽略SimpleChannelInboundHandler ,来看看 ChannelInboundHandlerAdapter 做了什么事情。

/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.channel;

import io.netty.channel.ChannelHandlerMask.Skip;

/**
 * Abstract base class for {@link ChannelInboundHandler} implementations which provide
 * implementations of all of their methods.
 *
 * <p>
 * This implementation just forward the operation to the next {@link ChannelHandler} in the
 * {@link ChannelPipeline}. Sub-classes may override a method implementation to change this.
 * </p>
 * <p>
 * Be aware that messages are not released after the {@link #channelRead(ChannelHandlerContext, Object)}
 * method returns automatically. If you are looking for a {@link ChannelInboundHandler} implementation that
 * releases the received messages automatically, please see {@link SimpleChannelInboundHandler}.
 * </p>
 */
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {

    /**
     * Calls {@link ChannelHandlerContext#fireChannelRegistered()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelRegistered();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelUnregistered()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelUnregistered();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelActive()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelActive();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelInactive()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelInactive();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelRead(Object)} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.fireChannelRead(msg);
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelReadComplete()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelReadComplete();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireUserEventTriggered(Object)} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelWritabilityChanged()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelWritabilityChanged();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireExceptionCaught(Throwable)} to forward
     * to the next {@link ChannelHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Skip
    @Override
    @SuppressWarnings("deprecation")
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.fireExceptionCaught(cause);
    }
}

该类中有很多回调方法,这些方法都是与事件相关,也就是说当某些时间发生的时候这些方法将会被调用,启动项目之后,用curl来请求服务端会出现以下输出

handler added
channel registered
channel active
这是一次GET请求
/
channel inactive
channel unregistered

这其实是可以理解的,先添加了管道处理器,再注册管道,管道处于活跃状态,处理请求,管道失活,管道注销,但是如果你用浏览器去请求的话结果如下:

handler added
handler added
channel registered
channel registered
channel active
channel active
这是一次GET请求
/
这是一次GET请求
/favicon.ico
favicon.ico请求

由于是两次请求的关系,上面的输出也输出并处理了两次请求,但唯独没有出现两次channel inactive、channel unregistered,当你再次用浏览器请求服务端的时候又会出现如下输出

这是一次GET请求
/
channel inactive
channel unregistered
这是一次GET请求
/favicon.ico
favicon.ico请求

也就是上一次请求的channel inactive、channel unregistered出现了,但是没有handler added、channel registered、channel active,这证明连续的请求是不需要重复这三个过程的,同时也说明浏览器请求和curl请求机制的不一样,curl请求每次都是执行一个简单的、完整的请求过程,当curl命令请求完成后它也会对服务端进行相应的通知,而浏览器并不会,其只在一定时间过后或者是关闭浏览器之后进行这些通知操作。

在整个服务端处理请求的过程中,其实还是会根据请求的协议类型(http1.0 http1.1)来做出区别,当请求时http1.1的时候我们自己可以通过去判断客户端设置的keeplive时间,在这段时间内如果没有客户端的再次请求,则在服务端进行关闭。

/**
     * 功能描述: <br> 读取客户端请求并返回相应
     * @Param: [ctx, msg]
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/8/22 16:03
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {

        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            System.out.println("这是一次" + request.method().name() + "请求");
            URI uri = new URI(request.uri());
            System.out.println(uri.getPath());
            if (uri.getPath().equals("/favicon.ico")) {
                System.out.println("favicon.ico请求");
                return;
            }

            //构造返回内容
            ByteBuf content = Unpooled.copiedBuffer("Hello word", CharsetUtil.UTF_8);
            //构造相应包体
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
            //构造响应头
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
            //返回客户端
            ctx.writeAndFlush(response);

            //可以手动关闭与客户端建立的联系
            if (true) { //在这里判断请求时1.0还是1.1,已经是否超过keeplive时间
                ctx.channel().close();
            }
            
        }
    }