Netty章节二:Hello world/基本的http服务

功能实现:使用netty构建一个类似于tomcat的web服务器,服务端监听8899端口,当访问8899端口的时候,服务器端给客户端hello world的响应。

服务端代码

启动主程序

public class TestServer {
    public static void main(String[] args) throws Exception {
        /*
            定义两个事件循环组
            NioEventLoopGroup 就是一个死循环,不断接受客户端发起的连接并处理,和tomcat这种服务器一样
            bossGroup 用于接受客户端的连接但是不做处理,会把连接转给workerGroup
            workerGroup 会对连接进行处理,进行对应的业务处理,最后把结果返回给客户端
        */
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //ServerBootstrap用于启动服务端
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            /*
                group() 事件循环组
                channel() 用到的管道 使用反射的方式创建的
                childHandler() 子处理器,自己编写的处理器,
                    请求到来之后由我们自己编写的处理器进行真正的处理
            */
            serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new TestServerInitializer());
            //绑定端口
            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            //关闭的监听
            channelFuture.channel().closeFuture().sync();
        }finally {
            //关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

初始化器 (Initializer)

客户端与服务端一旦连接之后,TestServerInitializer就会被创建 initChannel() 方法就会被调用

public class TestServerInitializer extends ChannelInitializer<SocketChannel> {

    /**
     * 连接(Channel)一旦被注册之后该方法就会被调用
     * 回调方法
     */
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        //一个管道,一个管道当中可以有多个ChannelHandler(拦截器),
        // 每个拦截器做的事情就是针对自己本身的请求或业务情况,完成相应的处理
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("httpServerCodec",new HttpServerCodec());
        pipeline.addLast("testHttpServerHandler",new TestHttpServerHandler());
    }
}

自定义处理器 (Handler)

public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    /**
     * channelRead0 读取客户端发过来的请求,并且向客户端返回响应的方法
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {

        System.out.println(msg.getClass());

        //打印出远程的地址,/0:0:0:0:0:0:0:1: 52434,本地线程的49734端口的线程和netty进行通信
        System.out.println(ctx.channel().remoteAddress());
        //Thread.sleep(8000);   用于测试持续连接(keepalive)
        if(msg instanceof HttpRequest){

            HttpRequest httpRequest = (HttpRequest)msg;
            System.out.println("请求方法名:"+httpRequest.method().name()); 

            URI uri = new URI(httpRequest.uri());
            //使用浏览器访问localhost:8899会发送二次请求,
            //其中有一次是localhost:8899/favicon.ico 这个url请求访问网站的图标
            if("/favicon.ico".equals(uri.getPath())){
                System.out.println("请求favicon.ico");
                return;
            }
            
            //向客户端返回的响应内容
            ByteBuf content =
                    Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8);
            //构建一个响应
            //DefaultFullHttpResponse(http协议的版本,返回的状态码,响应内容);
            FullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, HttpResponseStatus.OK,content);
            //设置response相应的头信息
            response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());

            //返回客户端
            ctx.writeAndFlush(response);
            //手动关闭连接 如果要测试持续连接需要注掉 ctx.channel().close();
            //其实更合理的close连接应该判断是http1.O还是1.1来进行判断请求超时时间来断开channel连接。
            ctx.channel().close();
        }
    }


    /**通道注册时调用*/
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel registered");
        super.channelRegistered(ctx);
    }

    /**通道活跃时调用*/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel active");
        super.channelActive(ctx);
    }

    /**处理程序已添加时调用*/
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handler added");
        super.handlerAdded(ctx);
    }
    /**通道停止活跃时调用*/
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel inactive");
        super.channelInactive(ctx);
    }

    /**通道取消注册时调用*/
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel unregistered");
        super.channelUnregistered(ctx);
    }

    /**连接断开之后*/
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        super.handlerRemoved(ctx);
    }
}

请求测试

curl访问

❯ curl 'localhost:8899'                                                         
Hello World% 

服务端显示

handler added	//通道添加
channel registered	//通道注册
channel active	//通道活跃
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46082
请求方法名:GET
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46082
channel inactive	//通道不活跃
channel unregistered	//通道取消注册

使用curl工具请求服务端,当请求结束结果返回之后通道/连接马上就被close掉了,服务端使用http1.1 同样如此,curl只是一个单次请求响应的工具,并没有使用到http1.1的keepalive 持续连接特性

Google浏览器访问

请输入图片描述

服务器终端显示

//第一次请求
handler added
handler added
channel registered
channel registered
channel active
channel active
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46500
请求方法名:GET
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46500
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46500
请求方法名:GET
请求favicon.ico
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46500

//第二次请求
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46502
请求方法名:GET
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46502
channel inactive	//第一个通道因为在保持时间内没有第二个请求复用该连接,被close
channel unregistered	//第一个通道因为在保持时间内没有第二个请求复用该连接,被close
class io.netty.handler.codec.http.DefaultHttpRequest
/0:0:0:0:0:0:0:1:46502
请求方法名:GET
请求favicon.ico
class io.netty.handler.codec.http.LastHttpContent$1
/0:0:0:0:0:0:0:1:46502

Google浏览器的访问使用到了持续连接的特性,第一次请求 浏览器分别请求了localhost:8899 和localhost:8899/favicon.ico两个路经,所以上面开头就添加/注册了两次通道

  • 不是持续连接吗?为什么会添加/注册两次通道,个人理解是第一个通道还没添加注册成为可用通道状态之前,第二个连接就到达了,就又创建了一个新的通道,可见输出信息也是这样

第一次请求的localhost:8899和localhost:8899/favicon.ico两个请求的通道/连接用的都是同一个建立在46500端口之上的通道,第二次请求的全部操作都使用了另一个通道(46502)

lsof查看端口绑定关系

请求之前

❯ lsof -i:8899                                                                 
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    21617 sakura  241u  IPv6 530665      0t0  TCP *:ospf-lite (LISTEN)	//程序本身

第一次请求之后

❯ lsof -i:8899                                                                 
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
chrome   1858 sakura   47u  IPv6 516947      0t0  TCP localhost:46500->localhost:ospf-lite (ESTABLISHED)
chrome   1858 sakura   53u  IPv6 516948      0t0  TCP localhost:46502->localhost:ospf-lite (ESTABLISHED)
java    21617 sakura  241u  IPv6 530665      0t0  TCP *:ospf-lite (LISTEN)
java    21617 sakura  243u  IPv6 527749      0t0  TCP localhost:ospf-lite->localhost:46500 (ESTABLISHED)
java    21617 sakura  244u  IPv6 524734      0t0  TCP localhost:ospf-lite->localhost:46502 (ESTABLISHED)

第二次请求之后

❯ lsof -i:8899                                                                           
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
chrome   1858 sakura   53u  IPv6 516948      0t0  TCP localhost:46502->localhost:ospf-lite (ESTABLISHED)
java    21617 sakura  241u  IPv6 530665      0t0  TCP *:ospf-lite (LISTEN)
java    21617 sakura  244u  IPv6 524734      0t0  TCP localhost:ospf-lite->localhost:46502 (ESTABLISHED)

请输入图片描述

原文地址:https://www.cnblogs.com/mikisakura/p/12983460.html