基于Netty实现Web容器-Netty版Tomcat(三)

本次所涉及代码已上传至GitHub:
https://github.com/lgli/lgli-netty-tomcat/tree/master
前面已经对于IO的发展历程和基本原理,及其一些小例子已经说得差不多了,今天需要进入主题了,在这之前,再次回忆下各种版本的聊天小程序小例子,因为今天的Netty入门级也是从聊天室开始的。
传送门:
传统IO方式
NIO方式
AIO方式
从最开始的同步阻塞,同步非阻塞,到最后的异步非阻塞
这里简单总结下几种方式:
传统IO《BIO》:
当客户端发起一个TCP连接,客户端通过java.net.ServerSocket#accept获取到一个java.net.Socket对象,于是在服务端启动一个独立的线程来处理这个java.net.Socket的所有内容,包括读写数据;当启动多个客户端连接的时候,服务端的线程数量等同客户端连接数量,这种Thread Per Request模式带来的问题就是大量的线程将给服务器带来巨大的线程开销,为了避免这个问题,可以采用线程池的方式来解决,但是线程池也会带来一个问题,假定线程池线程数量是500,此时刚好有500个客户端连接好服务端并在进行数据交互,此时第501个客户端的请求也是无法处理的,即这种方式不能较好的解决高并发的问题,模式的方式如下图所示:
在这里插入图片描述
NIO
NIO启用了一个多路复用的概念,即将所有连接通道都注册在一个
java.nio.channels.Selector上,然后通过轮询
java.nio.channels.Selector中的状态来进行对应的操作
如下图所示:
在这里插入图片描述
java.nio.channels.Selector的底层模型是一个基于Reactor反应堆的一种实现方式,在这里先提醒这么一个概念,后面会详述这个模型

AIO

AIO则是主要针对于在同步阻塞的等待数据读写方式上的一种改变,即不在同步阻塞的等待底层操作系统对数据的操作结果。而是通过传入回调函数来执行成功或者失败之后应该做的事情,其底层主要基于Proactor模型。

基于相对较复杂的NIO操作,Netty诞生了,除了对NIO进行优化和封装之外,Netty还可以随意切换多种不同的网络协议,提供很多标准的协议、安全、编码解码的支持,解决了很多 NIO 不易用的问题,同时应用于多种大型框架内,比如Dubbo、RocketMQ等。

先来一个基于Netty的聊天室改造

服务端

package com.lgli.netty.chart;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.net.InetSocketAddress;


/**
 * ChartNettyServer
 * @author lgli
 */
public class ChartNettyServer {

    public static void main(String[] args){
        //配置服务端的NIO线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        try{
            bootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    //针对主线程的配置,分配最大线程数
                    .option(ChannelOption.SO_BACKLOG,128)
                    //针对子线程的配置 保持长连接
                    .option(ChannelOption.SO_KEEPALIVE,true)
                    .childHandler(new MyChannelInitial());
            //绑定端口,同步等待成功
            ChannelFuture future = bootstrap.bind(new InetSocketAddress("localhost", 8080)).sync();
            //等待服务端监听端口关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //出现异常,则释放资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


    static class MyChannelInitial extends ChannelInitializer<SocketChannel>{

        protected void initChannel(SocketChannel socketChannel) throws Exception {
            System.out.println("client connection :" + socketChannel.remoteAddress());
            socketChannel.pipeline()
                    .addLast("frame",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
                    .addLast("decode",new StringDecoder())
                    .addLast("encode",new StringEncoder())
                    .addLast("handler",new MyHandler());
        }
    }


    static class MyHandler extends SimpleChannelInboundHandler<String>{

        /**
         * 保存所有的连接
         */
        private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

        /**
         * 接收到数据,发送给其他服务端
         * @param channelHandlerContext 发送数据的通道
         * @param s 发送的数据
         * @throws Exception
         */
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
            Channel inputChannel = channelHandlerContext.channel();
            for(Channel channel : channels){
                if(channel == inputChannel){
                    continue;
                }
                channel.writeAndFlush(inputChannel.remoteAddress()+":"+s+"\n");
            }
        }


        /**
         * 有新的连接连接进来
         * @param ctx
         * @throws Exception
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            Channel inChannel = ctx.channel();
            //通知其他服务器有新的上线了
            for(Channel channel : channels){
                channel.writeAndFlush("欢迎"+inChannel.remoteAddress()+"进入聊天室! \n");
            }
            inChannel.writeAndFlush("欢迎您进入聊天室!\n");
            channels.add(inChannel);
        }

        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            //通知其他服务器,下线了
            Channel outChannel = ctx.channel();
            for(Channel channel : channels){
                if(outChannel == channel){
                    continue;
                }
                channel.writeAndFlush(outChannel.remoteAddress()+"下线了 \n");
            }
            channels.remove(outChannel);
        }
    }
}

这里对代码做一点解释:

Main方法入口

27-28行:配置服务端主线程和工作线程组

29行:创建启动类

31-37行:方法需要传入配置的2个线程组,设置通道类型为

io.netty.channel.socket.nio.NioServerSocketChannel

根据主线程配置,设置最大线程数

针对子线程配置,保持长连接

传入事件分发Handler
在这里插入图片描述
39-40行:绑定服务,端口,等待服务端监听端口关闭

在这里插入图片描述
46-47行:出现异常,关闭线程组,释放资源
在这里插入图片描述
下面看下事件分发Handler:

需要是io.netty.channel.ChannelHandler类型的对象,

这里选择自定一个对象,com.lgli.netty.chart.ChartNettyServer.MyChannelInitial

继承于io.netty.channel.ChannelInitializer

在这里插入图片描述
上图显示了自定义对象和两者之间的关系

56-60行:

TCP数据处理方式

<io.netty.handler.codec.DelimiterBasedFrameDecoder>

这个处理TCP粘包粘包的类,在后续Netty正式篇中会详细说到

加解密方式

<io.netty.handler.codec.string.StringDecoder>

<io.netty.handler.codec.string.StringEncoder>

自定义Handler

<com.lgli.netty.chart.ChartNettyServer.MyHandler>

在这里插入图片描述

自定义Handler主要做了3件事:接收数据时候的处理,有新的连接以及客户端下线的处理

com.lgli.netty.chart.ChartNettyServer.MyHandler#channelRead0

78-86行:接收数据的时候,将接收到的数据,发送给除数据来源的所有其他客户端

在这里插入图片描述
com.lgli.netty.chart.ChartNettyServer.MyHandler#handlerAdded

95-103行:有新的客户端进入,则通知除新的客户端之外的其他客户端,同时,添加新的客户端保存待用

com.lgli.netty.chart.ChartNettyServer.MyHandler#handlerRemoved

106-116行:有客户端下线,则通知其他还在线的所有客户端
在这里插入图片描述

客户端代码:

package com.lgli.netty.chart;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.net.InetSocketAddress;
import java.util.Scanner;

/**
 * ChartNettyClient
 * @author lgli
 */
public class ChartNettyClient {

    public static void main(String[] args) {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        try{
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new MyChannelInitial());
            Channel channel = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync().channel();

            Scanner scanner = new Scanner(System.in);
            while(scanner.hasNextLine()){
                String s = scanner.nextLine();
                if("".equals(s)){
                    continue;
                }
                channel.writeAndFlush(s+"\n");
            }


        }catch (Exception e){
            e.printStackTrace();
        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }

    static class MyChannelInitial extends ChannelInitializer<SocketChannel>{
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            socketChannel.pipeline()
                    .addLast("frame",new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
                    .addLast("decode",new StringDecoder())
                    .addLast("encode",new StringEncoder())
                    .addLast("handler",new MyChannelHandler());
        }
    }

    static class MyChannelHandler extends SimpleChannelInboundHandler<String>{
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String o) throws Exception {
            System.out.println(o);
        }
    }
}

同样的,Main方法开始

23-24行:配置客户端线程组和客户端启动类

26-28行:配置相关参数,具体见服务端说明

31-38行:监听客户端键盘数据,向服务端发送数据

44行:出现异常则退出线程组
在这里插入图片描述
自定义事件分发Handler和服务端类似

TCP数据处理方式

<io.netty.handler.codec.DelimiterBasedFrameDecoder>

加解密方式

<io.netty.handler.codec.string.StringDecoder>

<io.netty.handler.codec.string.StringEncoder>

自定义Handler

<com.lgli.netty.chart.ChartNettyClient.MyChannelHandler>

在这里插入图片描述
自定义Handler,这里选择直接打印输出:

在这里插入图片描述
运行服务端和客户端程序,一个基于Netty的聊天室搭建成功

公众号有视频效果,有需要的可以关注看下
在这里插入图片描述
基于Netty的简易Tomcat实现

这里实现了一个很简单的Tomcat功能,即监听端口,然后接受请求,根据请求分发请求对应的处理器

基本思路:

在这里插入图片描述

启动类:

package com.lgli.netty.tomcat;

import com.lgli.netty.tomcat.http.NettyRequest;
import com.lgli.netty.tomcat.http.NettyResponse;
import com.lgli.netty.tomcat.servlet.NettyServlet;
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.HttpRequest;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;

import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * NettyTomcat
 * @author lgli
 */
public class NettyTomcat {

    private static Map<String, NettyServlet> maps = new HashMap<>(16);

    public NettyTomcat() {
        try{
            //初始化资源信息
            Properties properties = new Properties();
            InputStream in = NettyTomcat.class.getClassLoader().getResourceAsStream("netty-tomcat.properties");
            properties.load(in);
            Enumeration<?> enumeration = properties.propertyNames();
            while(enumeration.hasMoreElements()){
                Object o = enumeration.nextElement();
                if(!(o instanceof String)){
                    continue;
                }
                String url = (String)o;
                if(!url.endsWith("-url")){
                    continue;
                }
                Object urlClass = Class.forName(properties.getProperty(url.replace("url", "class"))).newInstance();
                if(!(urlClass instanceof NettyServlet)){
                    continue;
                }
                maps.put(properties.getProperty(url),(NettyServlet) urlClass);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void monitor() {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        try{
            serverBootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,128)
                    .childOption(ChannelOption.SO_KEEPALIVE,true)
                    .childHandler(new NettyTomcatChannelInitial());
            Channel channel = serverBootstrap.bind(new InetSocketAddress("localhost", 8080)).sync().channel();
            channel.closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    static class NettyTomcatChannelInitial extends ChannelInitializer<SocketChannel>{
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            socketChannel.pipeline().addLast("encode",new HttpResponseEncoder())
                    .addLast("decode",new HttpRequestDecoder())
                    .addLast("handler",new NettyTomcatHandler());
        }
    }

    static class NettyTomcatHandler extends SimpleChannelInboundHandler<Object>{

        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
            if(!(o instanceof HttpRequest)){
                return;
            }
            HttpRequest httpRequest = (HttpRequest) o;
            NettyRequest request = new NettyRequest(channelHandlerContext,httpRequest);
            NettyResponse response = new NettyResponse(channelHandlerContext,httpRequest);
            String url = request.getUrl();
            if(maps.containsKey(url)){
                maps.get(url).service(request,response);
            }else{
                response.write("404 Not Found,找不到资源");
            }
        }
    }




    public static void main(String[] args) {
        new NettyTomcat().monitor();
    }




}

com.lgli.netty.tomcat.NettyTomcat构造方法中,主要是初始化资源信息:

在这里插入图片描述

即读取配置资源文件,同时将请求和对应的类的实例保存在一个Map中
在这里插入图片描述
初始化线程组和初始化启动对象,前面已经提到,这里也不再过多解释:

在这里插入图片描述

需要对这个Handler做一点说明:
在这里插入图片描述
当有客户端<即浏览器>发来请求的时候,

90-93行:这里直接根据Netty中的

io.netty.handler.codec.http.HttpRequest

接收

94-95行:用两个类似平常用的比较多的

HttpServletRequest和HttpServletResponse

实现,即

NettyRequest和NettyResponse

96-101行:根据初始化的请求路径和处理对象类,对比这里接收到的请求路径,获取到处理对象类的实例,执行其service方法

最后主类方法则执行完毕

下面看下自定义的NettyRequest和NettyResponse

NettyRequest:

package com.lgli.netty.tomcat.http;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpRequest;

/**
 * NettyRequest
 * @author lgli
 */
public class NettyRequest {

    private ChannelHandlerContext ctx;

    private HttpRequest request;


    public NettyRequest(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) {
        this.ctx = channelHandlerContext;
        this.request = httpRequest;
    }

    public String getRequestMethod() {
        return request.method().name();
    }

    public String getUrl() {
        return request.uri();
    }
}

NettyRequest主要调用

io.netty.handler.codec.http.HttpRequest

的API操作获取请求名和请求uri

NettyResponse

package com.lgli.netty.tomcat.http;

import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledDirectByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;

/**
 * NettyResponse
 * @author lgli
 */
public class NettyResponse {

    private ChannelHandlerContext ctx;



    public NettyResponse(ChannelHandlerContext channelHandlerContext) {
        this.ctx = channelHandlerContext;
    }


    public void write(String result) {
        if(null == result || "".equalsIgnoreCase(result)
                || "".equalsIgnoreCase(result.trim())){
            return;
        }
        try{
            FullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,
                    Unpooled.wrappedBuffer(result.getBytes("GBK"))
            );
            response.headers().set("Content-Type","text/html");
            ctx.write(response);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            ctx.flush();
            ctx.close();
        }
    }
}

看write方法

32-37行:NettyResponse调用了

io.netty.handler.codec.http.FullHttpResponse

封装返回结果

38行:

io.netty.channel.ChannelHandlerContext

输出数据到客户端《即浏览器》

剩下用到的3个Servlet:

总的父类NettyServlet
在这里插入图片描述
第一个OneServlet

在这里插入图片描述
第二个TwoServlet

在这里插入图片描述
执行NettyTomcat主类,开启基于Netty简易版的Tomcat

公众号有视频效果,有需要的可以关注看下

后续,将深入了解Netty底层及其应用,还有比较关注的Netty性能调优!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值