编写Netty程序

编写代码

1.创建线程池

一般来说,我们会声明两个线程池,一个线程池用来处理Accept事件,一个是用于处理消息的读写事件。

// 用来处理Accept事件
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 用来处理消息的读写时间
EventLoopGroup workerGroup = new NioEventLoopGroup();

一般我们只需要监听一个端口,所以bossGroup一般声明为1个线程,bossGroup接收到新的连接后会交给workerGroup处理,workerGroup负责监听这些连接的读写事件。

2.创建启动类

启动类有两个,客户端启动类Bootstrap,服务端启动类ServerBootstrap,启动类负责整个Nett 程序的正常运行。

以服务端为例

ServerBootstrap serverBootstrap = new ServerBootstrap();

3.设置线程池

把第一步声明的线程池设置到ServerBootstrap中, bossGroup负责监听Accept连接,workerGroup负责监听连接的读写数据。

serverBootstrap.group(bossGroup, workerGroup)

4.设置ServerSocketChannel

设置Netty程序以什么样的IO模型运行,以NioServerSocketChannel为例

serverBootstrap.channel(NioServerSocketChannel.class);

如果程序运行在Linux系统上,可以使用EpollServerSocketChannel,它使用的是Linux系统上的epoll模型,比select 模型更高效。

这里提到的IO模型是指IO多路复用模型,一共有三个类型:selectpollepoll

select 实现多路复用的方式是,将已连接的Socket都放到一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。

所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。

select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。

poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。

epoll 通过两个方面,很好解决了 select/poll 的问题。

第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。

第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

原文链接:https://blog.csdn.net/qq_34827674/article/details/115619261

5.设置可选参数

设置Netty中可以使用的参数,这些参数都在ChannelOption及其子类中。

这里设置了一个SO_BACKLOG系统参数,它表示的是最大等待连接数量。

serverBootstrap.option(ChannelOption.SO_BACKLOG, 100);

6.设置可选Handler

只能设置一个,它会在SocketChannel建立起来之前执行,后面再分析它的执行时机。

serverBootstrap.handler(new LoggingHandler(LogLevel.INFO))

7.编写并设置子Handler

Netty中的Handler分成两种,一种叫做Inbound,一种叫做Outbound

这里简单地写一个Inbound类型的Handler,它把接收到数据直接写回给客户端。

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 读取数据后写回客户端
        ctx.write(msg);
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

设置子Handler 设置的是SocketChannel对应的Handler,也是只能设置一个,它用于处理SocketChannel的事件。

serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
	@Override
	public void initChannel(SocketChannel ch) throws Exception {
		ChannelPipeline p = ch.pipeline();
		// 可以添加多个子Handler
		p.addLast(new LoggingHandler(LogLevel.INFO));
		p.addLast(new EchoServerHandler());
	}
});

虽然只能设置一个子Handler,但是Netty提供了一种可以设置多个Handler的途径,即使用ChannelInitializer方式,第六步设置 Handler也可以使用这种方式设置多个Handler

这里,我们设置了一个打印日志的LoggingHandler和一个自定义的EchoServerHandler

8.绑定端口

ChannelFuture f = serverBootstrap.bind(PORT).sync();

9.等待服务端端口关闭

等待服务端监听端口关闭,sync () 会阻塞主线程,内部调用的是 Object 的 wait () 方法。

f.channel().closeFuture().sync();

10.关闭线程池

bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

上面设置了ServerSocketChannel的类型,而没有设置SocketChannel的类型。

是因为SocketChannelServerSocketChannel在接受连接之后创建出来的,所以,并不需要单独再设置它的类型,比如,NioServerSocketChannel创建出来的肯定是NioSocketChannel,而EpollServerSocketChannel创建出来的肯定是 EpollSocketChannel

完整代码

服务端代码

/**
 * 1.打开命令行窗口: telnet localhost 8001
 * 2.进入发送消息模式: Ctrl + ]
 * 3.使用send命令发送消息: send hello
 */
public class EchoServer {
    static final int PORT = 8001;
    public static void main(String[] args) {
        // 1. 声明线程池
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 2. 服务端引导器
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 3. 设置线程池
            serverBootstrap.group(bossGroup, workerGroup)
                    // 4. 设置ServerSocketChannel的类型
                    .channel(NioServerSocketChannel.class)
                    // 5. 设置参数
                    .option(ChannelOption.SO_BACKLOG, 100)
                    // 6. 设置ServerSocketChannel对应的Handler,只能设置一个
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 7. 设置SocketChannel对应的Handler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            // 可以添加多个子Handler
                            // p.addLast(new LoggingHandler(LogLevel.INFO));
                            p.addLast(new EchoServerHandler());
                        }
                    });

            // 8. 绑定端口
            ChannelFuture f = serverBootstrap.bind(PORT).sync();
            // 9. 等待服务端监听端口关闭,这里会阻塞主线程
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 10. 优雅地关闭两个线程池
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

EchoServerHandler

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 读取数据后写回客户端
        ctx.write(msg);
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端代码

public class NettyClient {
    static final int PORT = 8001;
    public static void main(String[] args) throws Exception {
        // 工作线程池
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(workerGroup);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    // pipeline.addLast(new LoggingHandler(LogLevel.INFO));
                    pipeline.addLast(new NettyClientHandler());
                }
            });
            // 连接到服务端
            ChannelFuture future = bootstrap.connect(new InetSocketAddress(PORT)).sync();
            System.out.println("connect to server success");

            // 调用后这里会阻塞
            // future.channel().closeFuture().sync();
            
            // 这里实现可以在控制台输入发送信息
            Channel channel = future.channel();
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String msg = scanner.nextLine();
                channel.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
                if ("quit".equals(msg)) {
                    channel.close();
                    break;
                }
            }
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

NettyClientHandler

public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(LocalDateTime.now().toString() + " " + ctx.channel() + " " + ((ByteBuf)msg).toString(CharsetUtil.UTF_8));
    }
}

Netty实现群聊

NettyChatHolder

public class NettyChatHolder {
    static final Map<SocketChannel, Long> userMap = new ConcurrentHashMap<>();
    private static AtomicLong userIdList = new AtomicLong(10000);

    static void join(SocketChannel socketChannel) {
        // 有人加入就给他分配一个id
        Long userId = userIdList.addAndGet(1);
        send(socketChannel, "userId:" + userId);
        for (SocketChannel channel : userMap.keySet()) {
            send(channel, userId + " 加入了群聊");
        }
        // 将当前用户加入到map中
        userMap.put(socketChannel, userId);
    }

    private static void send(SocketChannel socketChannel, String msg) {
        try {
            ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
            ByteBuf writeBuffer = allocator.buffer(msg.getBytes().length);
            writeBuffer.writeCharSequence(msg, Charset.defaultCharset());
            socketChannel.writeAndFlush(writeBuffer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void quit(SocketChannel socketChannel) {
        Long userId = userMap.get(socketChannel);
        send(socketChannel, "您退出了群聊");
        userMap.remove(socketChannel);
        for (SocketChannel channel : userMap.keySet()) {
            if (channel != socketChannel) {
                send(channel, userId + " 退出了群聊");
            }
        }
    }

    public static void propagate(SocketChannel socketChannel, String content) {
        Long userId = userMap.get(socketChannel);
        for (SocketChannel channel : userMap.keySet()) {
            if (channel != socketChannel) {
                send(channel, userId + ":" + content);
            }
        }
    }
}

NettyChatHandler

public class NettyChatHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SocketChannel socketChannel = (SocketChannel)ctx.channel();
        System.out.println("one conn active: " + socketChannel);
        // socketChannel是在ServerBootstrapAcceptor中放到EventLoopGroup中的
        NettyChatHolder.join(socketChannel);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
        /***
         * 如果这里有耗时操作的话, 可以自定义一个线程池来处理这下面的逻辑
         */
        byte[] bytes = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(bytes);
        String content = new String(bytes, StandardCharsets.UTF_8);
        System.out.println(ctx.channel() + ":" + content);
        if (content.equals("quit")) {
            ctx.channel().close();
        } else {
            NettyChatHolder.propagate((SocketChannel) ctx.channel(), content);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        SocketChannel socketChannel = (SocketChannel)ctx.channel();
        System.out.println("one conn inactive: " + socketChannel);
        NettyChatHolder.quit((SocketChannel) ctx.channel());
    }
}

NettyChatServer

public class NettyChatServer {
    static final int PORT = Integer.parseInt(System.getProperty("port", "8001"));
    public static void main(String[] args) {
        // 1. 声明线程池
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 2. 服务端引导器
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 3. 设置线程池
            serverBootstrap.group(bossGroup, workerGroup)
                    // 4. 设置ServerSocketChannel的类型
                    .channel(NioServerSocketChannel.class)
                    // 5. 设置参数
                    .option(ChannelOption.SO_BACKLOG, 100)
                    // 6. 设置ServerSocketChannel对应的Handler,只能设置一个
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 7. 设置SocketChannel对应的Handler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            // 可以添加多个子Handler
                            p.addLast(new LoggingHandler(LogLevel.INFO));
                            p.addLast(new NettyChatHandler());
                        }
                    });

            // 8. 绑定端口
            ChannelFuture f = serverBootstrap.bind(PORT).sync();
            // 9. 等待服务端监听端口关闭,这里会阻塞主线程
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 10. 优雅地关闭两个线程池
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

Netty版本

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.63.Final</version>
</dependency>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值