前言html
一个完整的Http请求包括客户端(经常为浏览器)请求和服务器响应两大部分,那么你清楚在这个过程当中底层都作了哪些事情吗?又如HTTP请求的短链接和长链接底层的区别是什么?再如何基于Netty定制开发符合特定业务场景的HTTP监听器 ... 等等这些问题都是今天咱们要解决的问题。
数据库
HTTP请求后端
一次完整的HTTP请求须要经历如下过程:浏览器
其中在HTTP1.1及以上版本,开启keep-alive, 步骤1和步骤7只作一次。
缓存
步骤2和步骤3中请求的报文结构以下:安全
步骤4~步骤6的响应报文结构以下:服务器
HTTP短链接和长链接
网络
短连接执行流程
并发
HTTP 是无状态的,浏览器和服务器每进行一次 HTTP 操做,就创建一次链接, 但任务结束就中断链接。dom
长链接执行流程
注: 使用http1.0开启keep-alived或http1.1 时,虽保持了TCP的长链接(默认300s), http请求的信息和状态是不会保存的,客户端仍然需使用额外的手段缓存这些信息如:Session,Cookie等;未改变http请求单向和无状态的特性;
可能的使用场景
长链接多用于操做频繁,点对点的通信,并且链接数不能太多状况,。每一个 TCP 链接都须要三步握手,这须要时间,若是每一个操做都是先链接,再操做的话 那么处理速度会下降不少,因此每一个操做完后都不断开,处理时直接发送数据包 就 OK 了,不用创建 TCP 链接。
数据库的链接用长链接, 若是用短链接频繁的通讯会形成 socket 错 误,并且频繁的 socket 建立也是对资源的浪费。
而像 WEB 网站的 http 服务通常都用短连接,由于长链接对于服务端来讲会 耗费必定的资源,而像 WEB 网站这么频繁的成千上万甚至上亿客户端的链接用 短链接会更省一些资源,若是用长链接,并且同时有成千上万的用户,若是每一个 用户都占用一个链接的话,那可想而知吧。因此并发量大,但每一个用户无需频繁 操做状况下需用短连好。
Netty基于HTTP包装介绍
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的其它实战,请继续关注!