Netty学习——使用Netty快速编写一个文件服务器

最终效果在这里插入图片描述

在这里插入图片描述

简介

本Demo是使用Netty快速的创建一个文件服务器。实现了以下功能:

  • 展示目录或文件,当为文件时显示文件大小
  • 点击目录可进入自目录
  • 可返回上一级
  • 点击文件可下载文件

Demo环境:

  • jdk1.8
  • Netty 4.1.74.Final

实现代码

服务端
package vip.huhailong.server;

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;
import vip.huhailong.handler.HttpFileServerHandler;

/**
 * HTTP服务端
 */
public class HttpFileServer {

    private static final String DEFAULT_URL = "/";

    public void run(final int port, final String url) throws Exception{
        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 socketChannel) throws Exception {
                    //http消息解码器
                    socketChannel.pipeline().addLast("http-decoder",new HttpRequestDecoder());
                    //将消息转为单一的FullHttpRequest或者FullHttpResponse,因为http解码器在每个http消息中会生成多个消息对象
                    socketChannel.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65535));
                    //对响应消息进行编码
                    socketChannel.pipeline().addLast("http-encoder",new HttpResponseEncoder());
                    //支持异步发送大大码流,但不占用过多但内存,防止发生Java内存溢出
                    socketChannel.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
                    socketChannel.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));
                }
            });
            ChannelFuture channelFuture = b.bind(port).sync();
            System.out.println("HTTP File Server Url: http://127.0.0.1:"+port+url);
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new HttpFileServer().run(8080,"/");
    }
}

服务端处理类
package vip.huhailong.handler;

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.text.DecimalFormat;
import java.util.Objects;
import java.util.regex.Pattern;

import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpUtil.setContentLength;

/**
 * HTTP File Server Handler
 */
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private final String url;
    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
    private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9.]*");

    public HttpFileServerHandler(String url){
        this.url = url;
    }



    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        System.out.println("come in uri is : "+request.uri());
        /**
         * 对http请求进行解码,如果解码失败则返回400错误
         */
        if(!request.decoderResult().isSuccess()){
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        /**
         * 对请求方法进行判断,如果不是get请求直接返回405错误
         */
        if(request.method() != HttpMethod.GET){
            sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
            return;
        }
        final String uri = request.uri();
        final String path = sanitizeUri(uri);
        if(path == null){
            sendError(ctx, HttpResponseStatus.FORBIDDEN);
            return;
        }
        File file = new File(path);
        if(file.isHidden()||!file.exists()){
            sendError(ctx, HttpResponseStatus.NOT_FOUND);
            return;
        }
        if(file.isDirectory()){
            if(uri.endsWith("/")){
                sendListing(ctx, file);
            }else{
                sendRedirect(ctx, uri+'/');
            }
            return;
        }
        if(!file.isFile()){
            sendError(ctx, HttpResponseStatus.FORBIDDEN);
            return;
        }
        RandomAccessFile randomAccessFile;
        try{
            randomAccessFile = new RandomAccessFile(file,"r");  //只读模式
        } catch (FileNotFoundException e){
            sendError(ctx, HttpResponseStatus.NOT_FOUND);
            return;
        }
        long fileLength = randomAccessFile.length();    //文件大小
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK);
        setContentLength(response,fileLength);
        setContentTypeHeader(response,file);
        if(isKeepAlive(request)){
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        ctx.write(response);
        ChannelFuture sendFileFuture;
        sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) {
                if(total < 0){
                    System.err.println("Transfer progress:"+progress);
                }else{
                    System.err.println("Transfer progress:"+progress+"/"+total);
                }
            }

            @Override
            public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) {
                System.out.println("Transfer complete.");
            }
        });

        ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if(!isKeepAlive(request)){
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        if(ctx.channel().isActive()){
            sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 对url进行编码校验
     * @param uri 访问对url
     * @return 返回校验后对路径
     */
    private String sanitizeUri(String uri){
        try{
            uri = URLDecoder.decode(uri, CharsetUtil.UTF_8.name()); //对URL进行编码
        } catch (UnsupportedEncodingException e){
            try{
                uri = URLDecoder.decode(uri,CharsetUtil.ISO_8859_1.name());
            } catch (UnsupportedEncodingException e2){
                throw new Error(e2.getLocalizedMessage());
            }
        }
        if(!uri.startsWith(url)&&!"/favicon.ico".equals(uri)){
            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 File.separator+uri;
    }

    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");
        StringBuilder buf = new StringBuilder();
        String dirPath = dir.getPath();
        buf.append("<!DOCTYPE html>\r\n");
        buf.append("<html><head><title>");

        buf.append(dirPath);
        buf.append("</title></head><body style='background-color:#eeeeee;'>\r\n");
        buf.append("<h3 style='background-color:#333333; color:white; padding:10px; border-radius:5px;'>");
        buf.append("当前路径:");
        buf.append(dirPath);
        buf.append("</h3>\r\n");
        buf.append("<ul>");
        buf.append("<li><a href=\"../\">⬅︎ 上一级</a></li>\r\n");
        DecimalFormat df = new DecimalFormat("######0.00");
        for(File f : Objects.requireNonNull(dir.listFiles())){
            if(f.isHidden()||!f.exists()){
                continue;
            }
            String name = f.getName();
            if(!ALLOWED_FILE_NAME.matcher(name).matches()){
                continue;
            }
            buf.append("<li><div style=\"display:flex\"><div style='min-width:300px;'>");
            if(f.isDirectory()){
                buf.append("\uD83D\uDCC1");
            }else{
                buf.append("\uD83D\uDCC4");
            }
            buf.append("<a style='margin-left:5px;' href=\"");
            buf.append(name);
            buf.append("\">");
            buf.append(name);
            buf.append("</a></div><div style='margin-left:10px; font-weight:bold;'>");
            if(!f.isDirectory()){
                double v = f.length() / 1024.0;
                buf.append(df.format(v));
                buf.append("KB");
            }
            buf.append("</div></div></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 newUrl){
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.FOUND);
        response.headers().set(HttpHeaderNames.LOCATION,newUrl);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,status,Unpooled.copiedBuffer("Failure: "+status+"\r\n",CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE,HttpHeaderValues.TEXT_PLAIN);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private static void setContentTypeHeader(HttpResponse response, File file){
        MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
        response.headers().set(HttpHeaderNames.CONTENT_TYPE,mimetypesFileTypeMap.getContentType(file.getParent()));
    }

}

通过上面两个文件就可以实现文件服务器了,服务端的代码和前面我们练习的服务端代码写法类似,唯一的区别是在自定义处理类前面我们加了Http协议相关的一些解码器:

  • HttpRequestDecoder:对HTTP请求消息进行解码器。
  • HttpObjectAggregator:将多个消息转换为单一的FullHttpRequest或者FullHttpResponse,因为HTTP解码器在每个HTTP消息中会生成多个消息对象:
    • HttpRequest / HttpResponse
    • HttpContent
    • LastHttpContent
  • HttpResponseEncoder:对HTTP响应消息进行编码。
  • ChunkedWriteHandler:用于支持异步发送大的码流(例如大的文件传输),但不占用过多的内存,防止发生Java内存溢出错误。
  • HttpFileServerHandler:我们自定义的用于文件服务器的逻辑处理。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Netty一个基于Java的高性能网络编程框架,它提供了一种简单而强大的方式来构建可扩展的网络应用程序。如果你想编写一个Netty游戏服务器,可以按照以下步骤进行: 1. 导入Netty库:首先,在你的项目中导入Netty库。你可以在Maven或Gradle中添加相应的依赖项。 2. 创建服务器引导类:创建一个服务器引导类,用于配置和启动你的Netty服务器。这个类将负责设置服务器的各种参数,如端口号、线程模型等。 3. 配置ChannelHandler:Netty使用ChannelHandler来处理网络事件和数据。你需要编写自定义的ChannelHandler来处理游戏逻辑,如接收和处理客户端的请求、发送游戏状态等。 4. 实现业务逻辑:根据你的游戏需求,实现相应的业务逻辑。这可能涉及到游戏规则、玩家管理、战斗逻辑等方面。 5. 处理网络事件:在你的ChannelHandler中,重写相应的方法来处理网络事件,如连接建立、数据接收、连接断开等。你可以根据需要进行相应的处理,如解析数据、更新游戏状态等。 6. 启动服务器:在服务器引导类中,调用相应的方法来启动你的Netty服务器。一旦服务器启动,它将开始监听指定的端口,并等待客户端的连接。 7. 测试和调试:在服务器启动后,你可以使用相应的客户端程序来测试和调试你的游戏服务器。确保服务器能够正确地接收和处理客户端的请求,并返回正确的响应。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@胡海龙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值