一,概念介绍
HTTP协议是建立在TCP传输协议之上的应用层的面向对象的协议。
主要特点如下:
1,支持Client/Server模式;
2,简单。客户端向服务端请求服务时,只需指定服务URL,携带必要的请求参数或者消息体;
3,灵活。HTTP允许传输任意类型的数据对象,传输的内容类型由HTTP消息头中的Content-Type加以标记;
4,无状态。指协议对于事务处理没有记忆能力。
http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用默认端口80;abs_path指定请求资源的URI。
HTTP请求消息HttpRequest:
HTTP请求由三部分组成请求行,消息头,请求正文。
请求行格式:Method Request-URI HTTP-Version CRLF
Method表示请求方法,Request-URI表示统一资源标识符,HTTP-Version表示请求的HTTP协议版本,CRLF表示回车和换行。
请求方法:
消息头:
HTTP响应消息HttpResponse
处理完HTTP客户端的请求之后,HTTP服务端返回响应消息给客户端,HTTP响应也是由三个部分组成,分别是:状态行,消息报头,响应正文。
状态行格式:HTTP-Version Status-Code Reason-Phrase CRLF
Status-Code表示服务器返回的响应状态代码,Reason-Phrase表示服务器返回的错误信息。
状态代码由三个数字组成,有如下5中可能取值。
(1)1xx:指示信息。表示请求已接收,继续处理。
(2)2xx:成功。表示请求已被成功接收,理解,接受。
(3)3xx:重定向。要完成请求必须进行更进一步的操作。
(4)4xx:客户端错误。请求有语法错误或请求无法实现。
(5)5xx:服务端错误。服务器未能处理请求。
常用的响应报头:
Location:响应报头域用于重定向接收者到一个新的位置,Location响应报头域常用语更换域名的时候。
Server:响应报头域包含了服务器用来处理请求的软件信息,与User-Agent请求报头域是相对应的。
WWW-Authenticate:必须被包含在401未授权响应消息中,客户端收到401响应消息,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域。
二,实例代码
由于Netty天生是异步事件驱动的框架,因此基于NIO TCP协议栈开发的HTTP协议栈也是异步非阻塞的。
Netty的HTTP协议栈无论在性能上还是可靠性上,都表现优异,非常适合在非Web容器的场景下应用。
实例场景如下:
文件服务器使用HTTP协议对外提供服务,当客户端通过浏览器访问文件服务器时,对访问路径进行检查,检查失败时返回HTTP 403错误,该页无法访问;如果校验通过,以链接的方式打开当前文件目录,每个目录或者文件都是个超链接,可以递归访问。
HTTP服务端开发:
package com.huawei.netty.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* Created by liuzhengqiu on 2017/11/4.
*/
public class HttpFileServer
{
private static final String DEAFAULT_URL = "/src/main/java/com/huawei/netty";
public void run(final int port,final String url) throws Exception
{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try
{
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception
{
socketChannel.pipeline()
.addLast("http-decoder",new HttpRequestDecoder())
.addLast("http-aggregator",new HttpObjectAggregator(65536))
.addLast("http-encoder",new HttpResponseEncoder())
.addLast("http-chunked",new ChunkedWriteHandler())
.addLast("fileServerHanlder",new HttpFileServerHandler(url));
}
});
ChannelFuture future = bootstrap.bind("127.0.0.1",port).sync();
System.out.println("HTTP文件目录服务器启动,网址是:http://127.0.0.1:"+port+url);
future.channel().closeFuture().sync();
}
finally
{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new HttpFileServer().run(8080,DEAFAULT_URL);
}
}
向ChannelPipeline中添加了HTTP请求消息的解码器,随后又添加了HttpObjectAggregator解码器,它的作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse,原因是HTTP解码器在每个HTTP消息中会生成多个消息对象。
ChunkedHandler的主要作用是支持异步发送大的码流,但不占用过多的内存,防止发生Java内存溢出错误。
package com.huawei.netty.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.LOCATION;
import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
/**
* Created by liuzhengqiu on 2017/11/4.
*/
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>
{
private final String url;
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private String sanitizeUri(String uri)
{
try
{
uri = URLDecoder.decode(uri,"UTF-8");
}
catch (UnsupportedEncodingException e)
{
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;
}
public HttpFileServerHandler(String url)
{
this.url = url;
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status)
{
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,status,
Unpooled.copiedBuffer("Failure:"+status.toString()+"\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE,"text/plain;charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws Exception
{
if (!fullHttpRequest.getDecoderResult().isSuccess())
{
sendError(channelHandlerContext,BAD_REQUEST);
return ;
}
if (fullHttpRequest.getMethod() != HttpMethod.GET)
{
sendError(channelHandlerContext,METHOD_NOT_ALLOWED);
return ;
}
final String uri = fullHttpRequest.getUri();
final String path = sanitizeUri(uri);
if (path == null)
{
sendError(channelHandlerContext,FORBIDDEN);
return ;
}
File file = new File(path);
if (file.isHidden() || !file.exists())
{
sendError(channelHandlerContext,NOT_FOUND);
return;
}
if (file.isDirectory())
{
if(uri.endsWith("/"))
{
sendListing(channelHandlerContext,file);
}
else
{
sendRedirect(channelHandlerContext,uri + '/');
}
return;
}
if (!file.isFile())
{
sendError(channelHandlerContext,FORBIDDEN);
return;
}
RandomAccessFile randomAccessFile = null;
try
{
randomAccessFile = new RandomAccessFile(file,"r");
}catch (FileNotFoundException fnfe)
{
sendError(channelHandlerContext,NOT_FOUND);
return;
}
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,OK);
setContentLength(response,fileLength);
setContentTypeHeader(response,file);
if (isKeepAlive(fullHttpRequest))
{
response.headers().set(CONNECTION,HttpHeaders.Values.KEEP_ALIVE);
}
channelHandlerContext.write(response);
ChannelFuture sendFileFuture;
sendFileFuture = channelHandlerContext.write(new ChunkedFile(randomAccessFile,0,fileLength,8192),
channelHandlerContext.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, 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 channelProgressiveFuture) throws Exception
{
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!isKeepAlive(fullHttpRequest))
{
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
private static void setContentTypeHeader(HttpResponse response,File file)
{
MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE,mimetypesFileTypeMap.getContentType(file.getPath()));
}
private static final Pattern ALLOW_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
private static void sendListing(ChannelHandlerContext ctx,File dir)
{
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,OK);
response.headers().set(CONTENT_TYPE,"text/html;charset=UTF-8");
StringBuilder buf = new StringBuilder();
String dirPath = dir.getPath();
buf.append("<!DOCTYPE html>\r\n");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append("目录:");
buf.append("</title></head><body>\r\n");
buf.append("<h3>");
buf.append(dirPath).append(" 目录: ");
buf.append("</h3>\r\n");
buf.append("<ul>");
buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n");
for (File file : dir.listFiles())
{
if (file.isHidden() || !file.canRead())
{
continue;
}
String name = file.getName();
if (!ALLOW_FILE_NAME.matcher(name).matches())
{
continue;
}
buf.append("<li>链接:<a href=\"");
buf.append(name);
buf.append("\">");
buf.append(name);
buf.append("</a></li>\r\n");
}
buf.append("</ul></body></html>\r\n");
ByteBuf buffer = Unpooled.copiedBuffer(buf,CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx,String newUri)
{
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,FOUND);
response.headers().set(LOCATION,newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
启动HTTP文件服务器,运行结果如下:
三,总结
主要介绍了Netty HTTP协议栈的入门级应用。目前最流行的是HTTP+XML开发或者Restful+JSON的形式。后续将进一步介绍