基于 Netty 的 HTTP 服务器开发

783 阅读6分钟

介绍

Netty 的 HTTP 协议栈无论是在性能上还是可靠性上,都表现优异,非常适合非 Web 容器的场景下应用,相比于传统的 Tomcat,Jetty 等 Web 容器,它更加轻量和小巧,并且灵活性和定制性也很好.下面我们看下 Netty 的核心的 API,并手动实现两个功能建立一个更深的认识。

buffer

顾名思义为缓冲区的意思,主要与 Channel 进行交互,数据是从 Channel 读入缓冲区,再从缓冲区写入 Channel 中

flip()

该方法的主要作用是:反转此缓冲区,将 position 给 limit,然后将 position 置为 0(其实就是切换读写模式)

clear()

清除此缓冲区,将 position 置为 0,把 capacity 的值给 limit

rewind()

重绕此缓冲区,将 position 位置置为 0

directByteBuffer

可以减少一次系统空间到用户空间的拷贝。但 Buffer 的创建和销毁成本更高,不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型并且持久的缓冲区。如果在数据量比较小的中小应用情况下,可以考虑使用 heapBuffer 由 JVM 进行管理。

channel()

表示 I/O 源与目标打开的连接,是双向的,但是不能直接访问数据,只能与 Buffer 进行交互。通过源码我们可以看到 FileChannel 的 read 方法和 write 方法都导致了数据复制了两次。

selector

可使一个单独的线程管理多个 Channel

  • open 方法可以创建 Selector
  • register 方法向多路复用器注册通道,可以用来监听事件(包括:读、写、连接、accept),注册事件后会产生一个 SelectionKey;它表示 SelectableChannel 和 Selector 之间的注册关系。
  • wakeup 方法:使尚未返回的第一个选择操作立即返回。唤醒的原因是注册了新的 Channel 或者事件 Channel 关闭,取消注册。(优先级更高的事件触发,比如定时器事件,希望可以被及时处理)
selector的作用

Selector 在 Linux 的实现类是 EPollSelectorImpl,委托给 EPollArrayWrapper 实现,其中三个 native 方法是对 epoll 的封装,而 EpollSelectorImpl.register 方法,通过调用 epoll_ctl 向 epoll 实例中注册事件,还将注册的文件描述符(fd)与 SelectionKey 的对应关系添加到 fdToKey 中,这个 map 维护了文件描述符与 SelectionKey 的映射。 fdToKey 有时会变得非常大,因为注册到 Selector 上的 Channel 非常多(百万连接),如果过期或失效的 Channel 没有及时关闭。fdToKey 总是串行读取的,而读取是在 select 方法中进行 的,该方法是非线程安全的。

pipe

两个线程之间的单向数据连接,数据会被写到 sink 通道,从 source 通道读取。

功能需求

  1. Netty 服务器在 8080 端口监听
  2. 浏览器发出请求 "http://localhost:8080/"
  1. 服务器可以回复消息给客户端 "Hello!我是 Netty 服务器",并对特定请求资源进行过滤

实现

public class NettyHttpServer {
    //端口号
    private int port;

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

    public void run() throws InterruptedException {
        //1. 创建bossGroup线程组: 处理网络事件--连接事件
        EventLoopGroup bossGroup = null;
        //2. 创建workerGroup线程组: 处理网络事件--读写事件 2*处理器线程数
        EventLoopGroup workerGroup = null;
        try {
            bossGroup = new NioEventLoopGroup(1);
            workerGroup = new NioEventLoopGroup();
            //3. 创建服务端启动助手
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //4. 设置bossGroup线程组和workerGroup线程组
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) //5. 设置服务端通道
                    // 实现为NIO
                    .option(ChannelOption.SO_BACKLOG, 128)//6. 参数设置
                    .childOption(ChannelOption.SO_KEEPALIVE,
                            Boolean.TRUE)//6. 参数设置
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        //7. 创建一个通道初始化对象
                        @Override
                        protected void initChannel(SocketChannel ch) throws
                                Exception {
                            //8. 向pipeline中添加自定义业务处理handler
                            //添加编解码器
                            ch.pipeline().addLast(new HttpServerCodec());
                            // 添加自定义的业务处理类
                            ch.pipeline().addLast(new NettyHTTPServerHandler());
                        }
                    });
            //9. 启动服务端并绑定端口,同时将异步改为同步
            ChannelFuture future = serverBootstrap.bind(port);
            future.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws
                        Exception {
                    if (future.isSuccess()) {
                        System.out.println("端口绑定成功!");
                    } else {
                        System.out.println("端口绑定失败!");
                    }
                }
            });
            System.out.println("HTTP服务端启动成功.");
            //10. 关闭通道(并不是真正意义上关闭,而是监听通道关闭的状态)和关闭连接池
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new NettyHttpServer(8080).run();
    }
}

public class NettyHTTPServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    /**
     * 读取就绪事件
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        // 判断请求是不是http请求
        if(msg instanceof HttpRequest){
            DefaultHttpRequest request = (DefaultHttpRequest) msg;
            System.out.println("浏览器请求路径:"+request.uri());
            if("/favicon.ico".equals(request.uri())){
                System.out.println("图标不响应");
                return;
            }
            // 给浏览器进行响应
            ByteBuf byteBuf = Unpooled.copiedBuffer("Hello!我是 Netty 服务器", CharsetUtil.UTF_8);
            DefaultHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
            // 设置响应头
            response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=utf-8").set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());
            ctx.writeAndFlush(response);
        }
    }
}

基于Netty的WebSocket开发网页版聊天室

WebSocket 介绍

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,客户端和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 WebSocket 应用场景十分广泛:

  1. 社交订阅
  2. 协同编辑/编程
  3. 股票基金报价
  4. 体育实况更新
  5. 多媒体聊天
  6. 在线教育

WebSocket 和 HTTP 的区别

HTTP 协议是用在应用层的协议,他是基于 TCP 协议的,HTTP 协议建立连接也必须要有三次握手才能发送信息。其中HTTP 连接分为短连接,长连接。短连接是每次请求都要三次握手才能发送自己的信息。即每一个 request 对应一个 response。长连接是在一定的期限内保持连接,保持 TCP 连接不断开。 客户端与服务器通信,必须要由客户端先发起,然后服务器返回结果。客户端是主动的,服务器是被动的。 客户端要想实时获取服务端消息就得不断发送长连接到服务端。

而 WebSocket 实现了多路复用,他是全双工通信。在 WebSocket 协议下服务端和客户端可以同时发送信息。建立了 WebSocket 连接之后, 服务端可以主动发送信息到客户端。而且信息当中不必在带有 head 的部分信息了与 HTTP 的长链接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。