写点什么

netty 系列之: 搭建 HTTP 上传文件服务器

发布于: 2 小时前

简介上一篇的文章中,我们讲到了如何从 HTTP 服务器中下载文件,和搭建下载文件服务器应该注意的问题,使用的 GET 方法。本文将会讨论一下常用的向服务器提交数据的 POST 方法和如何向服务器上传文件。


GET 方法上传数据按照 HTTP 的规范,PUT 一般是向服务器上传数据,虽然不提倡,但是也可以使用 GET 向服务器端上传数据。


先看下 GET 客户端的构建中需要注意的问题。


GET 请求实际上就是一个 URI,URI 后面带有请求的参数,netty 提供了一个 QueryStringEncoder 专门用来构建参数内容:


// HTTP 请求 QueryStringEncoder encoder = new QueryStringEncoder(get);// 添加请求参数 encoder.addParam("method", "GET");encoder.addParam("name", "flydean");encoder.addParam("site", "www.flydean.com");URI uriGet = new URI(encoder.toString());有了请求 URI,就可以创建 HttpRequest 了,当然这个 HttpRequest 中还需要有对应的 HTTP head 数据:


HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uriGet.toASCIIString());HttpHeaders headers = request.headers();headers.set(HttpHeaderNames.HOST, host);headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);headers.set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP + "," + HttpHeaderValues.DEFLATE);headers.set(HttpHeaderNames.ACCEPT_LANGUAGE, "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");headers.set(HttpHeaderNames.REFERER, uriSimple.toString());headers.set(HttpHeaderNames.USER_AGENT, "Netty Simple Http Client side");headers.set(HttpHeaderNames.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8");


    headers.set(            HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(                    new DefaultCookie("name", "flydean"),                    new DefaultCookie("site", "www.flydean.com"))    );
复制代码


我们知道 HttpRequest 中只有两部分数据,分别是 HttpVersion 和 HttpHeaders。HttpVersion 就是 HTTP 协议的版本号,HttpHeaders 就是设置的 header 内容。


对于 GET 请求来说,因为所有的内容都包含在 URI 中,所以不需要额外的 HTTPContent,直接发送 HttpRequest 到服务器就可以了。


channel.writeAndFlush(request);然后看下服务器端接收 GET 请求之后怎么进行处理。


服务器端收到 HttpObject 对象的 msg 之后,需要将其转换成 HttpRequest 对象,就可以通过 protocolVersion(),uri()和 headers()拿到相应的信息。


对于 URI 中的参数,netty 提供了 QueryStringDecoder 类可以方便的对 URI 中参数进行解析:


//解析 URL 中的参数 QueryStringDecoder decoderQuery = new QueryStringDecoder(request.uri());Map<String, List<String>> uriAttributes = decoderQuery.parameters();for (Entry<String, List<String>> attr: uriAttributes.entrySet()) {for (String attrVal: attr.getValue()) {responseContent.append("URI: ").append(attr.getKey()).append('=').append(attrVal).append("\r\n");}}POST 方法上传数据对于 POST 请求,它比 GET 请求多了一个 HTTPContent,也就是说除了基本的 HttpRequest 数据之外,还需要一个 PostBody。


如果只是一个普通的 POST,也就是 POST 内容都是 key=value 的形式,则比较简单,如果 POST 中包含有文件,那么会比较复杂,需要用到 ENCTYPE=”multipart/form-data”。


netty 提供了一个 HttpPostRequestEncoder 类,用于快速对 request body 进行编码,先看下 HttpPostRequestEncoder 类的完整构造函数:


public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset,EncoderMode encoderMode)其中 request 就是要编码的 HttpRequest,multipart 表示是否是”multipart/form-data”的格式,charset 编码方式,默认情况下是 CharsetUtil.UTF_8。encoderMode 是编码的模式,目前有三种编码模式,分别是 RFC1738,RFC3986 和 HTML5。


默认情况下的编码模式是 RFC1738,这也是大多数 form 提交数据的编码方式。但是它并不适用于 OAUTH,如果要使用 OAUTH 的话,则可以使用 RFC3986。HTML5 禁用了 multipart/form-data 的混合模式。


最后,我们讲讲 HttpDataFactory。factory 主要用来创建 InterfaceHttpData。它有一个 minSize 参数,如果创建的 HttpData 大小大于 minSize 则会存放在磁盘中,否则直接在内存中创建。


InterfaceHttpData 有三种 HttpData 的类型,分别是 Attribute, FileUpload 和 InternalAttribute。


Attribute 就是 POST 请求中传入的属性值。FileUpload 就是 POST 请求中传入的文件,还有 InternalAttribute 是在 encoder 内部使用的,这里不过多讨论。


因此,根据传入的 minSize 参数大小,Attribute 和 FileUpload 可以被分成下面几种:


MemoryAttribute, DiskAttribute or MixedAttributeMemoryFileUpload, DiskFileUpload or MixedFileUpload


在这一节我们先看一下在 POST 请求中并不上传文件的处理方式,首先创建 HTTP request 和 PostBody encoder:


// 构建 HTTP requestHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uriSimple.toASCIIString());


HttpPostRequestEncoder bodyRequestEncoder =new HttpPostRequestEncoder(factory, request, false);


向 request 中添加 headers:


// 添加 headersfor (Entry<String, String> entry : headers) {request.headers().set(entry.getKey(), entry.getValue());}然后再向 bodyRequestEncoder 中添加 form 属性:


// 添加 form 属性 bodyRequestEncoder.addBodyAttribute("method", "POST");bodyRequestEncoder.addBodyAttribute("name", "flydean");bodyRequestEncoder.addBodyAttribute("site", "www.flydean.com");bodyRequestEncoder.addBodyFileUpload("myfile", file, "application/x-zip-compressed", false);注意,上面我们向 bodyRequestEncoder 中添加了 method,name 和 site 这几个属性。然后添加了一个 FileUpload。但是因为我们的编码方式并不是”multipart/form-data”,所以这里传递的只是文件名,并不是整个文件。


最后,我们要调用 bodyRequestEncoder 的 finalizeRequest 方法,返回最终要发送的 request。在 finalizeRequest 的过程中,还会根据传输数据的大小来设置 transfer-encoding 是否为 chunked。


如果传输的内容比较大,则需要分段进行传输,这时候需要设置 transfer-encoding = chunked,否则不进行设置。


最后发送请求:


// 发送请求 channel.write(request);在 server 端,我们同样需要构造一个 HttpDataFactory,然后使用这个 factory 来构造一个 HttpPostRequestDecoder,来对 encoder 出来的数据进行 decode:


HttpDataFactory factory =new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);//POST 请求 decoder = new HttpPostRequestDecoder(factory, request);因为 server 端收到的消息根据发送消息的长度可以能是 HttpContent,也可能是 LastHttpContent。如果是 HttpContent,我们将解析的结果放到一个 StringBuilder 中缓存起来,等接收到 LastHttpContent 再一起发送出去即可。


在收到 HttpContent 之后,我们调用 decoder.offer 方法,对 HttpContent 进行解码:


decoder.offer(chunk);在 decoder 内部有两个存储 HttpData 数据的容器,分别是:


List<InterfaceHttpData> bodyListHttpData 和 Map<String, List<InterfaceHttpData>> bodyMapHttpDatadecoder.offer 就是对 chunk 进行解析,然后将解析过后的数据填充到 bodyListHttpData 和 bodyMapHttpData 中。


解析过后,就可以对解析过后的数据进行读取了。


可以通过 decoder 的 hasNext 和 next 方法对 bodyListHttpData 进行遍历,从而获取到对应的 InterfaceHttpData。


通过 data.getHttpDataType()可以拿到 InterfaceHttpData 的数据类型,上面也讲过了有 Attribute 和 FileUpload 两种类型。


POST 方法上传文件如果要 POST 文件,客户端在创建 HttpPostRequestEncoder 的时候传入 multipart=true 即可:


HttpPostRequestEncoder bodyRequestEncoder =new HttpPostRequestEncoder(factory, request, true);然后分别调用 setBodyHttpDatas 和 finalizeRequest 方法,生成 HttpRequest 就可以向 channel 写入了:


// 添加 body http databodyRequestEncoder.setBodyHttpDatas(bodylist);// finalize request,判断是否需要 chunkrequest = bodyRequestEncoder.finalizeRequest();// 发送请求头 channel.write(request);要注意,如果是 transfer-encoding = chunked,那么这个 HttpRequest 只是请求头的信息,我们还需要手动将 HttpContent 写入到 channel 中:


    // 判断bodyRequestEncoder是否是Chunked,发送请求内容    if (bodyRequestEncoder.isChunked()) {        channel.write(bodyRequestEncoder);    }
复制代码


在 server 端,通过判断 InterfaceHttpData 的 getHttpDataType,如果是 FileUpload 类型,则说明拿到了上传的文件,则可以通过下面的方法来读取到文件的内容:


FileUpload fileUpload = (FileUpload) data;responseContent.append(fileUpload.getString(fileUpload.getCharset()));这样我们就可以在服务器端拿到客户端传过来的文件了。


总结 HTTP 的文件上传需要考虑的问题比较多,大家有不明白的可以参考我的例子。或者留言给我一起讨论。


本文的例子可以参考:learn-netty4


本文已收录于 http://www.flydean.com/21-netty-http-fileupload/


最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!


欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 2 小时前阅读数: 3
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:搭建HTTP上传文件服务器