社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
HTTP(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。我们用的tomcat、jetty、jobss等各种服务器,其实就是一个服务端容器,方便我们直接部署我们应用,让用户通过使用客户端(WEB浏览器、手机H5、app等)借住HTTP协议,来完成信息的提交和获取。这篇文章简单总结一下HTTP协议,然后用Netty来实现一个HTTP服务端实例。
一,HTTP协议,是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统,是基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。下边我们分几个方面来看下。
1,网络层次模型:HTTP协议是在网络层次模型中的应用层,这里来看下OSI七层模型详解(https://blog.csdn.net/yaopeng_2005/article/details/7064869 ),TCP、IP详解学习笔记(https://www.cnblogs.com/fengzanfeng/articles/1339347.html )通过这两篇文章来了解一下各层协议。
2,HTTP协议的主要特点:a,支持Client/Server模式;b,简单,客户端向服务器请求服务时,只需指定服务URL,携带必要的请求参数或者消息体;c,灵活,HTTP允许传输任意类型的数据对象,传输内容类型有HTTP消息头中的Content-Type加以标识;d,无状态,表示HTTP协议对于事物处理没有记忆能力。
3,HTTP协议的URL格式:http://host[":"port][abs_path]。
4,HTTP请求消息(HttpRequest)组成:a,HTTP请求行,格式为Method(请求方法) Request-URL(统一资源标识符) HTTP-Version(协议版本) CRLF(回车和换行)。其中Method请求方法有:GET、POST、HEAD、PUT、DELETE、TRACE、CONNECT、OPTIONS等; b,HTTP消息头 ; c,HTTP请求正文。我们看下这个格式图:
5,HTTP响应消息(HttpResponse):也是又三个部分组成:状态行、消息报头、响应正文。其中状态行格式为:HTTP-Version Status-Code Reason-Phrase CRLF。这里说下状态码:其中1**:指示信息,表示请求已接受,继续处理;2**:成功,表示请求已被成功接收、理解、处理;3**:重定向,要完成请求必须进行更进一步的操作;4**:客户端错误,请求有语法错误或者无法实现;5**:服务端错误,服务器在处理请求过程中发生了错误。
好,这里再推荐两篇文章,写的比较详细,看参考一起学习:简单教程(http://www.runoob.com/http/http-tutorial.html) 、图解传说中的HTTP协议(https://blog.csdn.net/agzhchren/article/details/79173491)
二,简单小结了Http协议后,我们来看下如何通过Netty来实现HTTP服务端的开发,这样我们就可以直接通过WEB浏览器或其它客户端,直接调用我们的程序了。由于Netty天生是异步事件驱动的架构,因此基于NIO TCP协议栈开发的HTTP协议栈也是异步非阻塞的。下边来看个读取服务器用户目录下的清单的例子。里边写了相关注释:
/**
* @author liujiahan
* @Title: HttpFileServer
* @Copyright: Copyright (c) 2018
* @Description: 基于Netty开发的HTTP Server端
* @Created on 2018/10/27
* @ModifiedBy:
*/
public class HttpFileServer {
private static final String DEFAULT_URL = "/src/main/java/com/ljh/netty/";
/**
* 入口方法,Netty编写服务端的基本流程
* @param port 端口号
* @param url 访问地址
*/
public void run(final int port, final String url) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//1,http请求消息解码器
ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
//2,HttpObjectAggregator将多个消息转换为单一的FullHttpRequest或者FullHttpResponse
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
//3,http响应编码器
ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
//4,ChunkedWriteHandler 支持异步发送大的码流
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
//5,业务处理
ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
}
});
ChannelFuture future = b.bind("localhost", port).sync();
System.out.println("http文件目录服务器启动,网址是:" + "http://localhost:" + port + url);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
int port = 8091;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
}
}
String url = DEFAULT_URL;
if (args.length > 1) {
url = args[1];
}
new HttpFileServer().run(port, url);
}
}
/**
* @author liujiahan
* @Title: HttpFileServerHandler
* @Copyright: Copyright (c) 2018
* @Description: 具体的业务处理流程
* @Created on 2018/10/27
* @ModifiedBy:
*/
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private String url;
public HttpFileServerHandler(String url) {
this.url = url;
}
/**
* 1,接受到消息后的处理过程
* @param ctx
* @param request
* @throws Exception
*/
@Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
//验证解码
if (!request.decoderResult().isSuccess()) {
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
return;
}
//验证请求方法
if (request.method() != HttpMethod.GET) {
sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
return;
}
//uri地址合法的验证
final String uri = request.uri();
final String path = sanitizeUri(uri);
if (path == null) {
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
//file地址的验证
File file = new File(path);
if (file.isHidden() || !file.exists()) {
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
//如果是个文件夹的处理内容,sendlist下文件夹下的内容
if (file.isDirectory()) {
if (uri.endsWith("/")) {
sendListing(ctx, file);
} else {
sendRedirect(ctx, uri + "/");
}
return;
}
//如果不是文件类型请求,返回错误
if (!file.isFile()) {
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
//使用欧冠随机文件读写类以只读的方式打开文件
RandomAccessFile randomAccessFile = null;
try {
randomAccessFile = new RandomAccessFile(file, "r");
} catch (FileNotFoundException fnfd) {
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
//如果直接打开或者下载文件,获取文件的长度,构造HTTP消息
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpHeaderUtil.setContentLength(response, fileLength);
setContentTypeHeader(response, file);
//将应答header设置为keep-alive
if (HttpHeaderUtil.isKeepAlive(request)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture = null;
//将文件写到发送缓存区
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
//添加ChannelProgressiveFutureListener 监听,operationProgressed、operationComplete两个方法
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
if (total < 0) {
System.err.println("transfer progress:" + progress);
} else {
System.err.println("transfer progress:" + progress + "/" + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future) throws Exception {
System.out.println("transfer complete");
}
});
//如果使用chunked编码,最后需要发送一个编码结束的空消息体,表示结束
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!HttpHeaderUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
/**
* 2,异常处理
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
if (ctx.channel().isActive()) {
sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
/*************************************下边为封装的工具方法**************************************/
/**
* 不安全URI的正则限制
*/
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&"].*");
/**
* 审核Uri
*
* @param uri
* @return
*/
private String sanitizeUri(String uri) {
try {
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException e1) {
throw new Error();
}
}
//各种校验
if (!uri.startsWith(url)) {
return null;
}
if (!uri.startsWith("/")) {
return null;
}
uri = uri.replace('/', File.separatorChar);
if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".") || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
return null;
}
return System.getProperty("user.dir") + File.separator + uri;
}
/**
* 正则限制
*/
private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\.]*");
/**
* 发送相应的信息
*
* @param ctx
* @param dir
*/
private static void sendListing(ChannelHandlerContext ctx, File dir) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
String dirPath = dir.getPath();
//拼装响应体的信息
StringBuilder builder = new StringBuilder();
builder.append("<!DOCTYPE html>rn");
builder.append("<html><head><title>");
builder.append(dirPath);
builder.append("目录:");
builder.append("</title></head><body>rn");
builder.append("<h3>");
builder.append(dirPath).append(" 目录:");
builder.append("</h3>rn");
builder.append("<ul>");
builder.append("<li>链接:<a href=" ../")..</a></li>rn");
for (File f : dir.listFiles()) {
if (f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
}
builder.append("<li>链接:<a href="");
builder.append(name);
builder.append("">");
builder.append(name);
builder.append("</a></li>rn");
}
builder.append("</ul></body></html>rn");
ByteBuf buffer = Unpooled.copiedBuffer(builder, CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* 重定向
*
* @param ctx
* @param newUri
*/
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
response.headers().set(HttpHeaderNames.LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* 返回错误信息
* @param ctx
* @param status
*/
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure:" + status.toString() + "rn", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* 设置ContentType
* @param response
* @param file
*/
private static void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getPath()));
}
}
好,一个简单的Netty开发的HTTP协议的服务端就这样了。只有不断的使用的,不断的学习,不断的理解,不断的反复,很多东西才会有恍然大悟的感觉,继续学习。。。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!