前言
一个完整的Http请求包括客户端(常常为浏览器)请求和服务器响应两大部分,那么你清楚在这个过程中底层都做了哪些事情吗?又如HTTP请求的短连接和长连接底层的区别是什么?再如何基于Netty定制开发符合特定业务场景的HTTP监听器 ... 等等这些问题都是今天我们要解决的问题。
HTTP请求
一次完整的HTTP请求需要经历以下过程:
其中在HTTP1.1及以上版本,开启keep-alive, 步骤1和步骤7只做一次。
步骤2和步骤3中请求的报文结构如下:
步骤4~步骤6的响应报文结构如下:
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在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{
/**
* 证书全名称(包含路径)
*/
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;
}
//解析请求参数
Mapparams = 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 MapparseRequestPara(final String uri) {
Mapmap = 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的其它实战,请继续关注!