Netty——高性能IO框架(1)

是什么

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients

netty是在java的nio基础上的一个异步、基于事件驱动的高性能、高可用、开源、使用java编写的网络IO框架,实现原理是reactor的主从模式,开发者可以使用netty来快速搭建一个高性能、高可用服务器

在dubbo、elasticsearch……中都使用了它

为什么要有它

解决了nio的一些缺点:

  1. nio的API使用麻烦(比如:byteBuffer,每次都需要开发者调用flip())

  2. 使用nio实现高性能的服务器,需要开发者熟悉多线程编程 手动编写线程来实现异步、reactor模式

  3. nio的可用性并不是很好,需要开发者处理tcp粘包半包、异常码流等各种情况

netty的特点:

  1. 支持http、tcp、udp、websocket、protobuf、文件、以及字节数组,支持nio、bio

  2. 支持零拷贝、解决了tcp粘包半包

  3. netty的API使用简单(比如:自带线程池来实现异步处理、简化了负责监听的nioServerSocket的监听操作……),有大量开箱即用的组件,不需要手动造轮子,提高了开发效率(比如:负责处理读写的线程组、各种编解码器……)

  4. 完整的SSL/TLS和StartTLS支持

  5. 社区活跃、不断更新

在这里插入图片描述

应用场景

  1. RPC框架中的通信组件,比如dubbo就用netty作为默认的基础通信组件

    互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少, Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用

    典型的应用有:阿里分布式服务框架Dubo的RPC框架使用Dubbo协议进行节点间通信, Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信

  2. 游戏服务器

    网络游戏中服务器和客户端的交互需要延时低,而netty可以是基于tcp的,所以效率高

    无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用

    Netty作为高性能的基础通信组件,提供了tcp/udp和http协议栈,方便定制和开发私有协议栈,账号登录服务器

    地图服务器之间可以方便的通过Netty进行高性能的通信

  3. 大数据领域

    经典的 Hadoop的高性能通信和序列化组件(AVRO实现数据文件共享)的RPC框架,默认采用Netty进行跨界点通信

    它的 Netty Service基于 Netty框架二次封装实现

    (物联网也有用到netty)

demo

用netty做一个简单的http服务器,要实现的目标:

1. 启动服务器
2. 浏览器(也就是客户端)请求url:如 http://localhost:8585/?id=11&pwd=22
3. 服务器获取到请求的参数并返回响应

server端

http服务器要做的事情就是接收客户端发来的数据、对数据进行处理、响应客户端

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;

public class HttpServerPlus {

    private final Logger logger = LoggerFactory.getLogger(HttpServerPlus.class);

    private int port;

    public HttpServerPlus(int port) {
        this.port = port;
    }

    public HttpServerPlus() {
    }

    public void start(){

        EventLoopGroup boss = null;
        EventLoopGroup worker = null;
        try {
            ServerBootstrap boot = new ServerBootstrap();//配置服务器

            boss = new NioEventLoopGroup();//只负责监听有新客户端连接的事件
            // TODO netty只是一个IO框架 只负责基于NIO使用多路复用器高性能的接收客户端发来的数据,至于接收到客户端发来的数据怎么进行处理就是开发者的事了,比如:是同步处理还是异步处理都是开发者考虑的事,当然netty也提供了线程池可以进行异步处理
            worker = new NioEventLoopGroup();// 无参构造方法的线程数是CPU核心数 * 2

            boot.group(boss,worker)//TODO netty简化了将监听到的新client注册到worker线程池中的任意一个eventLoop的selector中的操作
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))//可以在这里绑定端口,也可以在下面的boot.bind()绑定
                    //option() 配置bossGroup

                    // 服务器端TCP内核模块维护有2个队列,我们称之为A,B吧
                    // 客户端向服务端connect的时候,发送带有SYN标志的包(第一次握手)
                    // 服务端收到客户端发来的SYN时,向客户端发送SYN ACK 确认(第二次握手)
                    // 此时TCP内核模块把客户端连接加入到A队列中,然后服务器收到客户端发来的ACK时(第三次握手)
                    // TCP没和模块把客户端连接从A队列移到B队列,连接完成,应用程序的accept会返回
                    // 也就是说accept从B队列中取出完成三次握手的连接

                    // A队列和B队列的长度之和是backlog,当A,B队列的长之和大于backlog时,新连接将会被TCP内核拒绝
                    // 所以,如果backlog过小,可能会出现accept速度跟不上,A.B 队列满了,导致新客户端无法连接,
                    // 注意:backlog对程序支持的连接数并无影响,backlog影响的只是还没有被accept 取出的连接
                    .option(ChannelOption.SO_BACKLOG,1024)//SO_BACKLOG 影响与服务器发起了建立tcp连接的但是服务器还未accept()返回的客户端数量
                    //handler() 配置bossGroup
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,即 禁止使用Nagle算法
                    // Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效
                    // 负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,与TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送
                    // 数据,适用于文件传输。
                    //childOption() 配置workerGroup
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    //childHandler() 配置workerGroup
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                            //配置多个handler对有事件的client进行处理,pipeline将多个handler串联了起来,一个client的channel对应一个pipeline
                            ChannelPipeline pipeline = sc.pipeline();
                            //对http的请求和响应编解码 包括了HttpRequestDecoder、HttpResponseEncoder
                            pipeline.addLast(new HttpServerCodec());
                            //TODO 在长连接中怎么知道数据发送完毕了呢,1、通过content-length判断 传输的数据是否到达content-length的大小,
                            // 2、动态生成的文件没有Content-Length,它是分块传输(chunked),这时候就要根据chunked编码来判断,chunked编码的数据在最后有一个空chunked块,表明本次传输数据结束
                            pipeline.addLast(new ChunkedWriteHandler());//TODO 应该和content-length不用同时存在吧,待证实!
                            // netty对于post的Http请求分为了HttpRequest 和 HttpContent 两个部分,HttpRequest 主要包含请求头、请求方法等信息,HttpContent 主要包含请求体的信息。
                            // HttpObjectAggregator可以将请求合并为单一的一个FullRequest
                            pipeline.addLast(new HttpObjectAggregator(65536));
                            // 这个方法的作用是: http 100-continue用于客户端在发送POST数据给服务器前,征询服务器情况,看服务器是否处理POST的数据,如果不处理,
                            // 客户端则不上传POST数据,如果处理,则POST上传数据。在现实应用中,通过在POST大数据时,才会使用100-continue协议
                            pipeline.addLast(new HttpServerExpectContinueHandler());
                            //自定义handler
                            pipeline.addLast(new ServerRequestHandler());
                        }
                    });

            //绑定端口,开始接收客户端连接
            //netty中的IO操作都是异步的,比如:bind、connect、read/write
            ChannelFuture future = boot.bind().sync();//ChannelFuture可以异步,这里绑定端口是同步 也可以异步绑定端口,然后看回调是否成功

            logger.info("HttpServerPlus 启动了");

            //bind()后的回调
            future.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (future.isSuccess()){

                        logger.info("服务端绑定" + port + "端口成功");

                    } else {
                        logger.error("服务端绑定" + port + "端口失败");
                        Throwable cause = future.cause();
                        logger.error("原因:" + cause.getMessage());
                    }
                }
            });

            //阻塞等待服务器的channel关闭之后再退出main 程序结束,优雅的关闭服务器
            future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {

            //优雅的关闭,释放线程池资源
            boss.shutdownGracefully();
            worker.shutdownGracefully();

            logger.info("ChatClient 关闭了");
        }

    }
}

netty的服务端和客户端的代码都是一个模板,代码就这么多,主要做了:

1. ServerBootstrap 是一个引导类,用来配置服务器
2. EventLoopGroup 是一个线程组 也就是线程池,里面的每个线程都有一个selector多路复用器,监听有事件的连接,服务器配置了2个EventLoopGroup线程组,一个专门用来监听新的客户端连接 里面只有1个线程,就相当于公司的前台负责接待陌生来客,一个负责监听已建立连接的客户端 里面有多个线程,相当于公司的专员 负责有业务往来的熟客
3. 定义服务器使用NIO模式    
4. 一些各种配置项    
5. (重要的来了,服务器要对有事件的客户端做处理了)
	添加通道初始化类,我这里把它写成了一个匿名内部类,也可以单独写一个类去继承ChannelInitializer
	SocketChannel 是一个有事件的客户端channel,一个channel对应一个ChannelPipeline
	ChannelPipeline将所有对有事件的客户端执行的操作都串联了起来,即 一个ChannelPipeline可以有多个handler,netty是事件驱动的,当有事件的客户端channel发送来数据后就会调用ChannelPipeline中定义好的handler进行处理,也就可以把handler看做拦截器,netty提供了大量的handler类方便我们拿来即用,所以我们就只需要编写和自己业务相关的handler就好了
	上面首先使用netty提供的HttpServerCodec 对接收和发送的数据进行编解码,做了一些前置处理,最后使用我们自	定义的handler做处理

handler类

import com.alibaba.fastjson.JSONObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.util.CharsetUtil;
import org.apache.commons.codec.CharEncoding;
import org.apache.commons.codec.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.SocketAddress;
import java.util.List;
import java.util.Map;

public class ServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private final Logger logger = LoggerFactory.getLogger(ServerRequestHandler.class);

    /**
     * 主要处理在channelRead0()中,其他方法都是生命周期中的方法
     * @param ctx
     * @param req
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
        String uri = req.uri();
        HttpMethod method = req.method();
        Channel channel = ctx.channel();
        SocketAddress socketAddress = channel.remoteAddress();

        //用浏览器发起 HTTP 请求时,常常会被 uri = "/favicon.ico" 所干扰,因此最好对其特殊处理
        if (uri.equals("/favicon.ico")) {
            return;
        }

        if (method.equals(HttpMethod.GET)) {

            System.out.println("get request");

            QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri);

            Map<String, List<String>> parameters = queryStringDecoder.parameters();

            parameters.entrySet().forEach(entry->{
                System.out.println("paramName: " + entry.getKey() + " value: " + entry.getValue().toString());
            });

        }else if (method == (HttpMethod.POST)){

            System.out.print("post request ");
            String contentType = getContentType(req);
            if (contentType.indexOf("application/json") >= 0) {

                System.out.println("application/json");

                String s = req.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
                JSONObject jsonObject = JSONObject.parseObject(s);
                for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                    System.out.println("paramName: " + entry.getKey() + " value: " + entry.getValue().toString());
                }

            }else if (contentType.indexOf("application/x-www-form-urlencoded") >= 0){

                System.out.println("application/x-www-form-urlencoded");

                HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(req);
                List<InterfaceHttpData> bodyHttpDatas = decoder.getBodyHttpDatas();

                bodyHttpDatas.forEach(body ->{
                    Attribute data = (Attribute) body;
                    try {
                        System.out.println("paramName: " + data.getName() + " value: " + data.getValue());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });

            }else if (contentType.indexOf("multipart/form-data") >= 0){
                //用于文件上传
                System.out.println("multipart/form-data");
            }

        }

        //因为是http所以使用netty提供的DefaultFullHttpResponse来做响应
        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,Unpooled.copiedBuffer("请求成功",CharsetUtil.UTF_8));

        response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html; charset=UTF-8");
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH,response.content().readableBytes());
        boolean keepAlive = HttpUtil.isKeepAlive(req);
        if (keepAlive) {

        }else {
            response.headers().set(HttpHeaderNames.CONNECTION,keepAlive);
        }

        //ctx.channel() 流经整个pipeline
        ctx.channel().writeAndFlush(response)
                .addListener(ChannelFutureListener.CLOSE)//TODO netty用做http服务器需要有这个关闭客户端连接的配置,因为http是短连接,虽然http1.1默认是长连接
                .addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        Channel ch = future.channel();
                        if (future.isSuccess()) {
                            logger.info("write successful");
                        }else {
                            logger.error("write error");
                            logger.error("cause: " + future.cause().getMessage());
                        }
                    }
                });

    }

    public String getContentType(FullHttpRequest request){
        String s = request.headers().get("Content-Type");
        return s;
    }

    //服务器读取完成的回调
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    //发生异常的回调
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error("exception cause: " + cause.getMessage());
        ctx.close();//关闭通道,也可以写作:ctx.channel().close()
    }
}

自定义的handler需要继承 SimpleChannelInboundHandler 或者 ChannelInboundHandlerAdapter,重写它的channelRead0() 或者 channelRead(),对客户端发来的数据进行处理,其他的方法就是一些生命周期的回调方法

ChannelHandlerContext 是每个handler的上下文
FullHttpRequest 包装了客户端发来的数据
一个ChannelPipeline中除了有多个handler,还有多个ChannelHandlerContext,而每个ChannelHandlerContext对应一个ChannelHandler,可以通过ChannelHandlerContext获取到当前ChannelPipeline、Channel、ChannelHandler

启动http服务器

上面就是http服务器的完整处理流程,现在只需要在main函数中将其启动即可

public class HttpServerPlusApplication {

    public static void main(String[] args) {
        HttpServerPlus server = new HttpServerPlus(8585);// 服务器端口号
        server.start();
    }
}

浏览器访问:http://localhost:8585/?id=11&pwd=22,服务器获取到:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值