使用Netty手撸一个简单的网关

一、基础代码

  首先使用BIO单线程、BIO多线程、BIO线程池的方式启动端口为8080、8081、8082三个服务,为后续做准备,同时编写客户端调用,使用HttpClient和OkHttpClient来分别调用。

  1、BIO模式单线程模式

public class HttpServer01 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        while (true){
            try{
                Socket socket = serverSocket.accept();
                service(socket);
            }catch (Exception e){
                e.printStackTrace();
            }

        }
    }

    private static void service(Socket socket) {
        String body = "hello lcl-nio-001";
        try{
            PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
            printWriter.println("HTTP/1.1 200 OK");
            printWriter.println("Content-Type:text/html;charset=utf-8");
            printWriter.println("Content-Length:" + body.getBytes().length);
            printWriter.println();
            printWriter.write(body);
            printWriter.flush();
            printWriter.close();
            socket.close();
        }catch (Exception e){

        }
    }

  2、多线程模式

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8081);
        while (true){
            try{
                Socket socket = serverSocket.accept();
                new Thread(() -> {
                    service(socket);
                }).start();
            }catch (Exception e){
                e.printStackTrace();
            }

        }
    }

  3、线程池模式

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8082);
        ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*4);
        while (true){
            try{
                Socket socket = serverSocket.accept();
                executorService.submit(()->service(socket));
            }catch (Exception e){
                e.printStackTrace();
            }

        }
    }

  4、HttpClient

 Request request = new Request.Builder()
                   .url(url)
                   .get()
                   .build();
        System.out.println(url);
        final Call call = client.newCall(request);
        Response response = call.execute();
        String result = response.body().string();

 

  5、OkHttpClient

     HttpGet request = new HttpGet(url);
        try (CloseableHttpClient httpClient = HttpClients.createDefault();
             CloseableHttpResponse response = httpClient.execute(request)) {
            String result = EntityUtils.toString(response.getEntity());
            //System.out.println(url);
             System.out.println(result);
             return result;
        }

 二、网关 V1.0:实现简单请求转发

  网关的核心功能就是转发、拦截过滤、路由,同时要保证高性能,那么网关 V1.0 首先关注点在于使用Netty实现消息的转发。

   1、首先创建一个Handler,根据请求路径不同,添加不同的返回数据,具体的数据,根据传入的url地址使用OkHttpClient进行调用,使用返回结果拼装上不同的自定义字符串后返回。

  由于是 V1.0 版本,所以先直接取了url集合中的第一个,没有做路由处理,后面版本会做优化。

public class MyHttpHandler extends ChannelInboundHandlerAdapter {

    private List<String> urlList;

    public MyHttpHandler(List<String> urlList){
        this.urlList = urlList;
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
        String uri = fullHttpRequest.getUri();
        if("/test".equals(uri)){
            handlerTest(ctx, fullHttpRequest, "hello lcl");
        }else {
            handlerTest(ctx, fullHttpRequest, "hello other");
        }
        ctx.fireChannelRead(msg);
    }

    private void handlerTest(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, String body) {
        MyOkHttpClient okHttpClient = new MyOkHttpClient();
        FullHttpResponse response = null;


        try {
            String value = okHttpClient.testHttpGet(this.urlList.get(0)) + body;
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(value.getBytes("UTF-8")));
            response.headers().set("Content-Type", "application/json");
            response.headers().set("Content-Length", response.content().readableBytes());
        } catch (Exception e) {
            System.out.printf("处理异常" + e.getMessage());
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
        }finally {
            if(fullHttpRequest != null){
                if(!HttpUtil.isKeepAlive(fullHttpRequest)){
                    ctx.write(response).addListener(ChannelFutureListener.CLOSE);
                }else {
                    response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE);
                    ctx.write(response);
                }
            }
        }
    }
}

 

  2、创建ChannelPipeline初始化类,在Pipeline最后添加上上面创建的handler

public class MyHttpInitializer extends ChannelInitializer<SocketChannel> {

    private List<String> urlList;

    public MyHttpInitializer(List<String> urlList){
        this.urlList = urlList;
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new HttpObjectAggregator(1024*1024));
        pipeline.addLast(new MyHttpHandler(urlList));
    }
}

 

  3、启动NettyServer

   创建ServerBootstrap,设置相关的参数,将上面创建的初始化类设置到childHander中。

public class MyNettyHttpServer {

    private int port;
    private List<String> urlList;

    public MyNettyHttpServer(int port, List<String> urlList){
        this.port = port;
        this.urlList = urlList;
    }


    public void start() {
        EventLoopGroup bossGroup = new NioEventLoopGroup(2);
        EventLoopGroup workerGroup = new NioEventLoopGroup(16);


        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childOption(ChannelOption.SO_REUSEADDR, true)
                    .childOption(ChannelOption.SO_RCVBUF, 21*1024)
                    .childOption(ChannelOption.SO_SNDBUF, 32*1024)
                    .childOption(EpollChannelOption.SO_REUSEPORT, true)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
            serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new MyHttpInitializer(urlList));

            Channel channel = serverBootstrap.bind(port).channel();
            System.out.println("开启netty http服务器,监听地址和端口为 http://127.0.0.1:" + port + '/');
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

 

 

  4、主函数启动时,初始化HttpServer,并设置监听端口和需要转发的Url

@SpringBootApplication
public class MyGatewayApplication {

    public static void main(String[] args) {
        int port = 8888;
        String url = "http://localhost:8080,http://localhost:8081";
        SpringApplication.run(MyGatewayApplication.class, args);
        MyNettyHttpServer myNettyHttpServer = new MyNettyHttpServer(port, Arrays.asList(url.split(",")));
        myNettyHttpServer.start();
    }
}

 

  5、验证

conglongli@localhost ~ % curl http://localhost:8888/demo
hello lcl-nio-001hello other
conglongli@localhost ~ % curl http://localhost:8888/test
hello lcl-nio-001hello lcl

 

  至此,一个最简单的网关已经完成。

三、网关 V2.0:实现简单拦截过滤

  在网关 V1.0的基础上,V2.0要加入拦截过滤的功能,主要是两个方面,一方面是对请求的拦截过滤,另一方面是对返回的拦截过滤。

  1、首先创建对应的请求响应过滤接口,然后各实现一个添加Header的过滤器。

public interface HttpRequestFilter {
    void filter(FullHttpRequest request, ChannelHandlerContext ctx);
}
public interface HttpResponseFilter {
    void filter(FullHttpResponse response);
}
public class HeaderHttpHttpRequestFilter implements HttpRequestFilter {
    @Override
    public void filter(FullHttpRequest request, ChannelHandlerContext ctx) {
        request.headers().set("testkey", "lcl");
        System.out.printf("=========" + request.headers().get("testkey"));
    }
}
public class HeaderHttpHttpResponseFilter implements HttpResponseFilter {
    @Override
    public void filter(FullHttpResponse response) {
        response.headers().set("testkey","ml");
    }
}

 

  2、创建一个HttpOutboundHandler,在其中主要就是拦截请求,调用上面的filter对header做设置,设置完成后才发起http调用,调用完成后,对于response做过滤,设置头信息。

public class HttpOutboundHandler {

    private List<String> urlList;
    HttpResponseFilter responseFilter = new HeaderHttpHttpResponseFilter();

    public HttpOutboundHandler(List<String> urlList){
        this.urlList = urlList;
    }


    public void handler(FullHttpRequest fullHttpRequest, ChannelHandlerContext ctx, HttpRequestFilter filter){
        filter.filter(fullHttpRequest, ctx);
        MyOkHttpClient okHttpClient = new MyOkHttpClient();
        FullHttpResponse response = null;
        try {
            String value = okHttpClient.testHttpGet(this.urlList.get(0));
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(value.getBytes("UTF-8")));

            response.headers().set("Content-Type", "application/json");
            response.headers().set("Content-Length", response.content().readableBytes());
            responseFilter.filter(response);
        } catch (Exception e) {
            System.out.printf("处理异常" + e.getMessage());
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
        }finally {
            if(fullHttpRequest != null){
                if(!HttpUtil.isKeepAlive(fullHttpRequest)){
                    ctx.write(response).addListener(ChannelFutureListener.CLOSE);
                }else {
                    response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE);
                    ctx.write(response);
                }
            }
        }
    }
}

 

   3、修改 V1.0的MyhttpHander,直接调用上面的handler方法

public class MyHttpHandler extends ChannelInboundHandlerAdapter {

    private List<String> urlList;

    private HttpOutboundHandler httpOutboundHandler;

    HttpRequestFilter httpRequestFilter = new HeaderHttpHttpRequestFilter();

    public MyHttpHandler(List<String> urlList){
        this.urlList = urlList;
        this.httpOutboundHandler = new HttpOutboundHandler(this.urlList);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
        String uri = fullHttpRequest.getUri();
//        if("/test".equals(uri)){
//            handlerTest(ctx, fullHttpRequest, "hello lcl");
//        }else {
//            handlerTest(ctx, fullHttpRequest, "hello other");
//        }
        httpOutboundHandler.handler(fullHttpRequest, ctx, httpRequestFilter);
        ctx.fireChannelRead(msg);
    }

四、网关 V3.0:实现简单路由

  在 V2.0 中添加了拦截过滤,在 V3.0 中添加路由策略

  1、编写一个Router接口和实现类,实现路由功能

public interface HttpEndpointRouter {
    String getUrl(List<String> urlList);
}

  实现类可以有多种,例如轮询、随机、最小连接数等等,这里实现一个最简单的随机

public class RandomHttpRouter implements HttpEndpointRouter{
    @Override
    public String getUrl(List<String> urlList) {
        Random random = new Random(System.currentTimeMillis());
        int index =  random.nextInt(urlList.size());
        return urlList.get(index);
    }
}

  2、然后修改 V2.0 中获取url的方法即可

FullHttpResponse response = null;
        try {
            String uri = fullHttpRequest.getUri();
            String url = router.getUrl(this.urlList) + uri;
            String value = okHttpClient.testHttpGet(url);
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(value.getBytes("UTF-8")));

            response.headers().set("Content-Type", "application/json");
            response.headers().set("Content-Length", response.content().readableBytes());
            responseFilter.filter(response);

  在路由这一块,还有很多可以优化的,例如是否要判断ip是否可用等。

 五、高性能优化:使用线程池和异步调用

  上面基本上已经涵盖了转发、拦截过滤、路由这些最基本的简单实现,然后就是如何保证网关的高性能。

  上面使用的是同步调用的方式,那么为了提高性能,可以使用异步和线程池的方式进行调用,同时使用Future获取返回信息。

  首先构建线程池,在OkHttpOutboundHandler的构造函数中初始化线程池

    public OkHttpOutboundHandler(List<String> urlList){
        this.urlList = urlList;

        // 定义线程池及参数
        int corePoolSize = Runtime.getRuntime().availableProcessors();
        long keepAliveTime = 1000;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2048);
        ThreadFactory threadFactory = new NamedThreadFactory("proxy-service");
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
        proxyServicePool = new ThreadPoolExecutor(corePoolSize,corePoolSize,keepAliveTime,
                TimeUnit.MILLISECONDS, workQueue, threadFactory, handler);
    }

 

   在其中定义了一个线程工厂,主要就是用来生成线程,并设置线程名称和是否是守护线程。

public class NamedThreadFactory implements ThreadFactory {
    private final String prefixName;
    private final ThreadGroup threadGroup;
    private final boolean demon;
    private final AtomicInteger threadNumber = new AtomicInteger(1);


    public NamedThreadFactory(String prefixName) {
        this(prefixName,false);
    }

    public NamedThreadFactory(String prefixName, boolean demon) {
        this.prefixName = prefixName;
        this.demon = demon;
        SecurityManager securityManager = System.getSecurityManager();
        this.threadGroup = securityManager != null ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup();
    }

    @Override
    public Thread newThread(@NotNull Runnable r) {
        Thread t = new Thread(threadGroup, r,prefixName+"-thread-"+threadNumber.incrementAndGet(), 0);
        t.setDaemon(demon);
        return t;
    }
}

 

  在handler方法中,对Request进行拦截过滤,根据路由策略选择一个服务,最后将远程调用人体提交到线程池

    public void handler(FullHttpRequest fullHttpRequest, ChannelHandlerContext ctx, HttpRequestFilter filter){
        filter.filter(fullHttpRequest, ctx);
        String uri = fullHttpRequest.getUri();
        String url = router.getUrl(this.urlList) + uri;
        proxyServicePool.submit(() -> fetchhttpGet(fullHttpRequest, ctx, url));
    }

 

  在fetchhttpget方法中与之前没有区别,直接使用远程调用,在调用的最后,对Response进行拦截过滤,最后使用ctx.flush()方法将返回结果通知给客户端。

    private void fetchhttpGet(FullHttpRequest fullHttpRequest, ChannelHandlerContext ctx, String url) {
        MyOkHttpClient okHttpClient = new MyOkHttpClient();
        FullHttpResponse response = null;
        try {
            String value = okHttpClient.testHttpGet(url);
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(value.getBytes("UTF-8")));

            response.headers().set("Content-Type", "application/json");
            response.headers().set("Content-Length", response.content().readableBytes());
            responseFilter.filter(response);
        } catch (Exception e) {
            System.out.printf("处理异常" + e.getMessage());
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
        }finally {
            if(fullHttpRequest != null){
                if(!HttpUtil.isKeepAlive(fullHttpRequest)){
                    ctx.write(response).addListener(ChannelFutureListener.CLOSE);
                }else {
                    //response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE);
                    ctx.write(response);
                }
            }
            ctx.flush();
        }
    }

 

  这样就完成了通过线程池提高并发和性能的处理,但是对于http调用来说,仍然是同步的,接下来采用异步请求客户端来提高并发性能。

  首先在初始化OkHttpOutboundHandler的构造函数中初始化客户端

   private CloseableHttpAsyncClient httpClient;
    HttpResponseFilter responseFilter = new HeaderHttpHttpResponseFilter();
    HttpEndpointRouter router = new RandomHttpRouter();

    public OkHttpOutboundHandler(List<String> urlList){
        this.urlList = urlList;

        // 定义线程池及参数
        int corePoolSize = Runtime.getRuntime().availableProcessors();
        long keepAliveTime = 1000;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2048);
        ThreadFactory threadFactory = new NamedThreadFactory("proxy-service");
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
        proxyServicePool = new ThreadPoolExecutor(corePoolSize,corePoolSize,keepAliveTime,
                TimeUnit.MILLISECONDS, workQueue, threadFactory, handler);


        IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
                .setConnectTimeout(1000)
                .setSoTimeout(1000)
                .setIoThreadCount(corePoolSize)
                .setRcvBufSize(32*1024).build();

        httpClient = HttpAsyncClients.custom().setMaxConnTotal(40)
                .setMaxConnPerRoute(8)
                .setDefaultIOReactorConfig(ioReactorConfig)
                .setKeepAliveStrategy(((httpResponse, httpContext) -> 6000))
                .build();
        httpClient.start();
    }

 

   然后在fetchhttpGet方法中使用客户端的execute方法将请求异步提交,并且使用Future异步获取结果,在结果监听中,如果调用成功则处理调用结果

    private void fetchGet(FullHttpRequest fullHttpRequest, ChannelHandlerContext ctx, String url) {
        final HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE);
        httpClient.execute(httpGet, new FutureCallback<org.apache.http.HttpResponse>() {
            @Override
            public void completed(org.apache.http.HttpResponse httpResponse) {
                try {
                    handlerResponse(fullHttpRequest, ctx, httpResponse);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
@Override
public void failed(Exception e) { httpGet.abort(); e.printStackTrace(); } @Override public void cancelled() { httpGet.abort(); } }); }

 

   如果调用成功,则调用handlerResponse方法处理调用结果,在该方法中就是和前面一样的对结果进行拦截过滤,并最后使用ctx.flush()将结果返回给客户端。

    private void handlerResponse(FullHttpRequest fullHttpRequest, ChannelHandlerContext ctx, org.apache.http.HttpResponse httpResponse) throws Exception {
        FullHttpResponse response = null;
        try {
            byte[] body = EntityUtils.toByteArray(httpResponse.getEntity());
            //String value = okHttpClient.testHttpGet(url);
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(body));

            response.headers().set("Content-Type", "application/json");
            response.headers().set("Content-Length", response.content().readableBytes());
            responseFilter.filter(response);
        } catch (Exception e) {
            System.out.printf("处理异常" + e.getMessage());
            response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT);
        }finally {
            if(fullHttpRequest != null){
                if(!HttpUtil.isKeepAlive(fullHttpRequest)){
                    ctx.write(response).addListener(ChannelFutureListener.CLOSE);
                }else {
                    //response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderNames.KEEP_ALIVE);
                    ctx.write(response);
                }
            }
            ctx.flush();
        }
    } 

 

posted @ 2022-05-21 18:02  李聪龙  阅读(1780)  评论(1编辑  收藏  举报