【基于Netty实现Epoll机制的UDP接收服务】

本篇涉及Netty、Linux网络IO等专业知识,适合对该方面有一定应用基础者食用

一、Epoll机制简述

NIO (Non-blocking IO),称之为非阻塞IO,传输过程如下:
在这里插入图片描述

  • Epoll是Linux内核提供的一种高效的事件通知机制,它通过异步非阻塞的方式处理I/O操作,提高系统的性能和扩展性。Epoll的工作原理可以分为以下几个步骤:
    - 创建Epoll实例:首先需要创建一个Epoll实例,通过调用epoll_create()系统调用来创建。该实例用于注册和管理事件。
    - 注册文件描述符:将需要监听的文件描述符(通常是套接字)添加到Epoll实例中,通过调用epoll_ctl()系统调用。可以指定感兴趣的事件类型,如可读事件、可写事件等。
    - 等待事件就绪:调用epoll_wait()系统调用来等待文件描述符上的事件就绪。该调用会阻塞,直到有事件就绪或超时。
    - 处理就绪事件:当有事件就绪时,epoll_wait()会返回就绪事件集合。应用程序可以遍历该集合,处理相应的I/O操作。
  • Epoll的工作原理主要依赖于内核中的三个数据结构:
    • Epoll实例(epoll instance):用于注册和管理事件的数据结构。它在内核中维护着一个红黑树,用于快速查找和管理被注册的文件描述符。
    • 文件描述符集合(file descriptor set):用于存储需要监听的文件描述符。它可以是一个数组或链表的形式,通过epoll_ctl()系统调用将文件描述符添加到集合中。
    • 就绪事件集合(ready event set):用于存储已经就绪的事件。当调用epoll_wait()等待事件时,内核会将就绪的事件添加到该集合中,并返回给应用程序。
  • Epoll的工作原理是基于事件驱动的,只有在事件就绪时才会进行相应的处理。与传统的阻塞式I/O模型相比,Epoll能够处理更多的并发连接,避免了不必要的阻塞和轮询操作,提高了系统的性能和扩展性。

二、代码实现

由于上篇文章【基于Netty实现NIO模型UDP服务端】在性能上还有很大的瓶颈,所以本篇在代码层片进行了优化。采用Epoll机制和多线程的方式在用户空间内快速处理业务逻辑
1. 创建springBoot项目
目录结构:
在这里插入图片描述
2. 导入Maven依赖配置

		<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.53.Final</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.codehaus.janino</groupId>
            <artifactId>janino</artifactId>
            <version>3.0.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

3. 创建UDP服务端并绑定端口

  • SO_REUSEPORT是一个Socket选项,可以在多个进程或线程绑定到同一个端口上时,允许它们共享该端口。它可以提高并发连接的性能,特别是在负载均衡的场景下。
  • 内核层面实现负载均衡
  • 安全层面,监听同一个端口的套接字只能位于同一个用户下面
package com.demo.nettyDemo.udp;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollChannelOption;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;

/**
 * @title
 * @date 2023/7/19 13:44
 **/
public class NettyUdp implements Runnable {
    // UDP服务端口
    private static int PORT = 15001;

    // 缓冲区默认最小值
    private static int DEFAULT_MINIMUM = 512;

    // 缓冲区默认初始值
    private static int DEFAULT_INITIAL = 2048;

    // 缓冲区默认最大值
    private static int DEFAULT_MAXIMUM = 1024 * 1024;

    public static void main(String[] args) {
        new NettyUdp().run();
    }

    @Override
    public void run() {
        // 创建Netty线程组,用于接收请求和处理IO请求
        EventLoopGroup eventLoopGroup = Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup();
        // 创建netty客户端启动器
        Bootstrap b = new Bootstrap();
        // 创建客户端与服务器的交互管道
        Channel channel;
        // 设置线程模型
        b.group(eventLoopGroup)
                // 指明网络通讯的是udp通讯
                .channel(Epoll.isAvailable() ? EpollDatagramChannel.class : NioDatagramChannel.class)
                // 设置netty接收缓冲区大小
                .option(ChannelOption.SO_RCVBUF, DEFAULT_MAXIMUM)
                // 自定义处理接收的数据
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel nch) throws Exception {
                        // 在处理器链的最后添加一个NettyHandler
                        nch.pipeline().addLast(new NettyHandler());
                    }
                });
        // linux平台下支持SO_REUSEPORT特性以提高性能
        if (Epoll.isAvailable()) {
            b.option(EpollChannelOption.SO_REUSEPORT, true);
        }else {
            b.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM));
        }
        try {
            // 绑定端口
            channel = b.bind(PORT).sync().channel();
            // 阻塞主线程,等待服务端关闭
            channel.closeFuture().sync().await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

4. 自定义数据包处理器

package com.demo.nettyDemo.udp;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;

/**
 * @title
 * @date 2023/7/19 14:09
 **/
@Slf4j(topic = "demoLog")
public class NettyHandler  extends SimpleChannelInboundHandler<DatagramPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
        // 接收数据包的内容写入byte数组缓冲区
        ByteBuf buf = msg.content();
        // 读取byte数组缓冲区的长度
        int len = buf.readableBytes();
        // 创建字节数组,用于接收数据
        byte[] data = new byte[len];
        // 获取服务端的IP地址和端口号
        InetSocketAddress inetSocketAddress = msg.sender();
        // 将byte数组缓冲区的内容写入byte数组
        buf.readBytes(data);
        // 将字节数组转为string
        String receive = new String(data, 0, len);
        log.info(inetSocketAddress.getAddress().getHostAddress() + ":" + inetSocketAddress.getPort() + ":" + receive);
    }
}

5. 在启动类中添加UDP服务启动线程

@SpringBootApplication
public class NettyDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(NettyDemoApplication.class, args);
        // 开启线程启动UDP服务端
        // linux系统下使用SO_REUSEPORT特性,使得多个线程绑定同一个端口
        if (Epoll.isAvailable()) {
            int cpuNum = Runtime.getRuntime().availableProcessors();
            for (int i = 0; i < cpuNum; i++) {
                Thread udpThread = new Thread(new NettyUdp());
                udpThread.start();
            }
        }
    }
}

三、丢包率测试

  1. 使用Jmeter请求UDP端口,模拟高并发请求
  2. 监控方法
  1. 测试情况:
    • VMware:CentOS 7 2核4G
    • net.core.rmem_default = 1048576
    • net.core.rmem_max = 1048576
QPS测试时长(分钟)请求总量丢包数丢包率
600051,806,98128,6781.59%
500051,494,1061,1390.08%
400051,180,2842340.02%
30005894,87300.00%
20005599,87300.00%

四、结论

  • 经过并发测试,每秒5000QPS以下,丢包率在万分之8以下,是一个能勉强接收的丢包容忍率。
  • 使用SO_REUSEPORT,多个线程绑定一个端口,可以大幅提高应用处理内核中队列内的数据
  • 增加应用的接收缓冲区大小,可以一定程度提高处理速度,减少因为内核中接收缓冲区满而丢包的几率。

项目源码git地址

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要基于 Netty 实现一个 SOCKS5 服务器,可以按照以下步骤进行: 1. 创建一个 Netty 的 ServerBootstrap 对象,并设置其相关属性,例如监听端口号、处理器等。 2. 在处理器中实现 SOCKS5 协议的解析和处理。对于 SOCKS5 协议,客户端会发送一个 Greeting 消息,服务器需要回复一个 Greeting 消息确认连接。然后客户端会发送一个请求,包括请求类型、目标地址和端口等信息,服务器需要根据请求类型进行相应的处理,例如连接目标地址和端口、绑定到指定的地址和端口等。 3. 在处理器中实现数据的转发,当客户端和目标服务器建立连接后,服务器需要将数据从客户端转发给目标服务器,然后将目标服务器返回的数据转发给客户端。 下面是一个简单的示例代码: ```java public class Socks5Server { public static void main(String[] args) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new Socks5ServerEncoder()); pipeline.addLast(new Socks5InitialRequestDecoder()); pipeline.addLast(new Socks5ServerHandler()); } }); ChannelFuture future = bootstrap.bind(1080).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 在上面的代码中,创建了一个 ServerBootstrap 对象,并设置了监听端口号为 1080,处理器为 Socks5ServerHandler。Socks5ServerHandler 实现了 SOCKS5 协议的解析和处理,以及数据的转发。 需要注意的是,这只是一个简单的示例代码,实际使用中可能需要根据具体需求进行扩展和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值