一个完整的Http请求包括客户端(常常为浏览器)请求和服务器响应两大部分,那么你清楚在这个过程中底层都做了哪些事情吗?又如HTTP请求的短连接和长连接底层的区别是什么?再如何基于Netty定制开发符合特定业务场景的HTTP监听器 ... 等等这些问题都是今天我们要解决的问题。
其中在HTTP1.1及以上版本,开启keep-alive, 步骤1和步骤7只做一次。
HTTP短连接和长连接
短链接执行流程
HTTP 是无状态的,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接, 但任务结束就中断连接。
长连接执行流程
注: 使用http1.0开启keep-alived或http1.1 时,虽保持了TCP的长连接(默认300s), http请求的信息和状态是不会保存的,客户端仍然需使用额外的手段缓存这些信息如:Session,Cookie等;未改变http请求单向和无状态的特性;
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个 TCP 连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话 那么处理速度会降低很多,所以每个操作完后都不断开,处理时直接发送数据包 就 OK 了,不用建立 TCP 连接。
数据库的连接用长连接, 如果用短连接频繁的通信会造成 socket 错 误,而且频繁的 socket 创建也是对资源的浪费。
而像 WEB 网站的 http 服务一般都用短链接,因为 长连接对于服务端来说会 耗费一定的资源,而像 WEB 网站这么频繁的成千上万甚至上亿客户端的连接用 短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个 用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁 操作情况下需用短连好。
Netty在HTTP请求和包装上,典型的包括:
以下我们就来应用Netty为我们提供的开箱即用的功能完成我们的设想。
/** * @author andychen https://blog.51cto.com/14815984 * @description:HTTP监听器启动类 */ public class HttpListener { //主线程组 public static final EventLoopGroup mainGroup = new NioEventLoopGroup(); //工作线程组 public static final EventLoopGroup workGroup = new NioEventLoopGroup(); //启动对象 public static final ServerBootstrap bootStrap = new ServerBootstrap(); /** * 监听器启动入口 * @param args */ public static void main(String[] args) { if(0 < args.length) { try { //监听器主机 final String host = args[0]; //监听端口 final int port = Integer.parseInt(args[1]); //证书文件 String certFileName = args[2].trim(); //私钥文件 String keyFileName = args[3].trim(); final ChannelFuture future = bootStrap.group(mainGroup, workGroup) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(host, port)) .childHandler(new ChannelInitializerExt(certFileName, keyFileName)) .bind().sync(); System.out.println("监听端:"+port+"已启动..."); future.channel().closeFuture().sync();//阻塞至通道关闭 }catch (InterruptedException e) { e.printStackTrace(); } finally { mainGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } } }
/** * @author andychen https://blog.51cto.com/14815984 * @description:ChannelInitializer通道初始化器扩展类 */ public class ChannelInitializerExt extends ChannelInitializer<Channel> { /** * 证书全名称(包含路径) */ private final String cerFileName; /** * 证书私钥(包括路径) */ private final String keyFileName; public ChannelInitializerExt(String cerFileName, String keyFileName) { this.cerFileName = cerFileName; this.keyFileName = keyFileName; } /** * 通道初始化 * 初始化各种ChannelHandler * @param channel * @throws Exception */ protected void initChannel(Channel channel) throws Exception { ChannelPipeline pipeline = channel.pipeline(); /** * 添加入站请求处理,同时兼容http和https请求 */ pipeline.addFirst(new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf)msg; //判断协议头:https数据流的第一位是十六进制“16”,转换成十进制是22 if(Constant.FIRST_BYTE_VAL == buf.getByte(0)){ //SSL支持 SslContext context = buildSslContext(cerFileName, keyFileName); SSLEngine engine = context.newEngine(UnpooledByteBufAllocator.DEFAULT); pipeline.addBefore("encoder_decoder", "ssl", new SslHandler(engine)); } ctx.pipeline().remove(this); super.channelRead(ctx, msg); } }); //包括HttpRequestDecoder解码器和HttpResponseEncoder编码器 pipeline.addLast("encoder_decoder", new HttpServerCodec()); //handler聚合,此handler必须 pipeline.addLast("aggregator", new HttpObjectAggregator(Constant.MAX_CONTENT_LEN)); //支持压缩传输 pipeline.addLast("compressor", new HttpContentCompressor()); //业务handler pipeline.addLast(new HttpChannelHandler()); } /** * 构建ssl上下文 * @param certFileName 证书文件名 * @param keyFileName 证书私钥 * @return * @throws SSLException */ private static SslContext buildSslContext(final String certFileName, final String keyFileName) throws SSLException { File crtFile = null; File keyFile = null; try { crtFile = new File(certFileName); keyFile = new File(keyFileName); // /** // * 方式一:采用内置自带证书(适合用于本地测试) // */ // SelfSignedCertificate ssc = new SelfSignedCertificate(); // return SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); /** * 方式二:映射安全证书和KEY */ return SslContextBuilder.forServer(crtFile, keyFile) .clientAuth(ClientAuth.NONE) .sslProvider(SslProvider.OPENSSL) .build(); }finally { crtFile = null; keyFile = null; } } }
/** * @author andychen https://blog.51cto.com/14815984 * @description:HTTP监听器业务处理器 */ public class HttpChannelHandler extends ChannelInboundHandlerAdapter { /** * 测试请求地址 */ private static final String REQ_URL = "/index"; //请求名称 private static final String REQ_PARA_NAME = "name"; /** * 监听器接收网络数据 * @param ctx 通道上下文 * @param msg 消息 * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String data = null; //http请求 FullHttpRequest request = (FullHttpRequest)msg; //(1)请求地址 String uri = request.uri(); /* 验证请求是否为约定地址,这里可以做成各种请求映射表 这里只说明思路 */ if(!uri.startsWith(REQ_URL)){ data = Constant.HTML_TEMP.replace("{0}", "请求地址:["+uri+"]不存在(404)"); this.response(ctx, data, HttpResponseStatus.NOT_FOUND); return; } //解析请求参数 Map<String, String> params = parseRequestPara(uri); if(!params.containsKey(REQ_PARA_NAME)){ data = Constant.HTML_TEMP.replace("{0}", "请求参数错误(401)"); this.response(ctx, data, HttpResponseStatus.BAD_REQUEST); return; } //****其它验证逻辑******* //(2)请求头 HttpHeaders headers = request.headers(); System.out.println("请求头:"+headers); //(2)请求主体 String body = request.content().toString(CharsetUtil.UTF_8); System.out.println("请求body:"+body); //(3)请求方法 HttpMethod method = request.method(); //(4)处理请求 this.proce***equest(ctx, method); } /** * 异常捕获 * @param ctx 处理器上下文 * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } /** * 解析请求参数 * @param uri 请求地址 */ private static Map<String,String> parseRequestPara(final String uri) { Map<String,String> map = new HashMap<>(); QueryStringDecoder decoder = new QueryStringDecoder(uri); decoder.parameters().entrySet().forEach(entry -> { map.put(entry.getKey(), entry.getValue().get(0)); }); System.out.println("请求参数:"+decoder.parameters()); return map; } /** * 处理HTTP请求 * @param method 方法 * @return */ private void proce***equest(final ChannelHandlerContext ctx, final HttpMethod method){ Random r = new Random(); String content = Constant.ARTICLES[r.nextInt(Constant.ARTICLES.length)]; //处理GET请求 if(HttpMethod.GET.equals(method)){ this.response(ctx, content, HttpResponseStatus.OK); return; } //处理POST请求 if(HttpMethod.POST.equals(method)){ //其它逻辑... return; } //PUT请求 if(HttpMethod.PUT.equals(method)){ //其它逻辑... return; } //DELETE请求 if(HttpMethod.DELETE.equals(method)){ //其它逻辑... return; } } /** * http响应 * @param ctx * @param content * @param status */ private void response(ChannelHandlerContext ctx, String content, HttpResponseStatus status){ //写入数据到缓冲 ByteBuf data = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8); //设置响应信息 FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, data); response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8"); //写入对端并监听通道关闭事件 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } }
总结
以上代码实战中,接收请求的处理部分不是所有的请求方法类型都对应实现,但处理均有类似之处,参照实现即可。在工作中碰到需要定制开发轻量级HTTP监听实现我们的后端业务时,我们就可以考虑这种定制化的场景,比较灵活,可以在此基础上插拔更多需要的业务类插件。更多关于Netty的其它实战,请继续关注!