使用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(); } }
-----------------------------------------------------------
---------------------------------------------
朦胧的夜 留笔~~