• Netty学习笔记(一):接收nodejs模拟表单上传的文件


    好久不写博客了,也好久不写代码了,这两天临时遇上一个事情,觉得不难,加上觉得手有些生,就动手做了一下,结果遇上了不少坑,有新坑,有老坑,痛苦无比,现在总算差不多了,赶紧记录下来,希望以后不再重复这种痛苦。

    事情很简单,用nodejs模拟表单提交,上传文件到netty服务器。

    1、netty的参考资料很多,目前有netty3,netty4两个版本,netty5出到alpha 2版本,不知道怎么的,就不更新了,官网也注明不支持了,所以我采用的是netty4.1.19版,目前最新的。
    参考的资料大致如下

        1)http://netty.io/wiki/index.html,官方的文档,都写的很经典,值得学习,里面的例子snoop对我帮助很大

        2)https://www.programcreek.com/,一个示例代码的网站。

    netty的代码基本都是照抄第二个网站的内容,具体地址是https://www.programcreek.com/java-api-examples/index.php?source_dir=netty4.0.27Learn-master/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java。共有三个文件,HttpUploadServer.java,HttpUploadServerHandler.java,HttpUploadServerInitializer.java
    2、nodejs本身比较简单,但也花了不少时间研究。上传文件可选的组件也很多,有form-data,request甚至官方的API,使用起来都不复杂,本来选择的是form-data,但是用起来也遇到了不少问题,最终使用的还是request,request使用起来非常简单,我主要参考了如下内容。
         1)http://www.open-open.com/lib/view/open1435301679966.html 中文的,介绍的比较详细。
         2)https://github.com/request/request 这是官方网站,内容最全,最权威。

    3、详细环境
       1)Windows 10专业版
       2)Spring Tool Suite 3.9.1,其实用eclipse也可以
       3)Netty 4.1.19
       4)Nodejs 8.9.3
    4、目标
       1)Netty程序
            a)同时支持post、get方法。
            b)将cookie、get参数和post参数保存到map里,如果是文件上传,则将其保存到临时目录,返回web地址,供客户访问。
       2)nodejs
            a)同时支持get、post方法。
            b)可以设置cookie,因为上传文件肯定是需要登录的,sessionID一般是保存在cookie里面。
    5、预期思路
       1)先解决netty的服务端问题,客户端先用浏览器测试。
       2)再解决nodejs的问题。
    6、解决过程和踩的坑
       1)Netty
           a)Netty编程本身不难,但是相对来说要底层一些,如果经常做web开发的人,可能容易困惑,但熟悉一下就好了。
                一般来说,netty服务端程序分为三个程序,如下
               Server:启动线程,保定端口。
               Initializer:初始化流处理器,即将接收到的字节流先进行编码,形成对象,供后续解码器处理,我们需要关注的东西不多,在这个程序里,我们拿到手的已经是解析好的http对象了,只要按照我们的思路处理就可以了。
               Handler:是我们自己的逻辑,在这个例子里就是解析对象,形成map,将文件保存到磁盘上而已。
          b)首先是pom文件,如下

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    
    	<groupId>io-netty</groupId>
    	<artifactId>io-netty-example</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>jar</packaging>
    
    	<name>io-netty-example</name>
    	<url>http://maven.apache.org</url>
    
    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    	</properties>
    
    	<dependencies>
    		<!-- https://mvnrepository.com/artifact/io.netty/netty-codec-http -->
    		<dependency>
    			<groupId>io.netty</groupId>
    			<artifactId>netty-all</artifactId>
    			<version>4.1.19.Final</version>
    		</dependency>
    		<dependency>
    			<groupId>com.alibaba</groupId>
    			<artifactId>fastjson</artifactId>
    			<version>1.2.44</version>
    		</dependency>
    		<dependency>
    			<groupId>junit</groupId>
    			<artifactId>junit</artifactId>
    			<version>3.8.1</version>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    </project>
    

      

          c)Server非常简单,代码也不多,如下

    package io.netty.example.http.upload; 
     
    import io.netty.bootstrap.ServerBootstrap; 
    import io.netty.channel.Channel; 
    import io.netty.channel.EventLoopGroup; 
    import io.netty.channel.nio.NioEventLoopGroup; 
    import io.netty.channel.socket.nio.NioServerSocketChannel; 
    import io.netty.handler.logging.LogLevel; 
    import io.netty.handler.logging.LoggingHandler; 
    import io.netty.handler.ssl.SslContext; 
    import io.netty.handler.ssl.util.SelfSignedCertificate; 
     
    /**
     * A HTTP server showing how to use the HTTP multipart package for file uploads and decoding post data. 
     */ 
    public final class HttpUploadServer { 
     
        static final boolean SSL = System.getProperty("ssl") != null; 
        static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8090")); 
     
        public static void main(String[] args) throws Exception { 
            // Configure SSL. 
            final SslContext sslCtx; 
            if (SSL) { 
                SelfSignedCertificate ssc = new SelfSignedCertificate(); 
                sslCtx = SslContext.newServerContext(ssc.certificate(), ssc.privateKey()); 
            } else { 
                sslCtx = null; 
            } 
     
            EventLoopGroup bossGroup = new NioEventLoopGroup(1); 
            EventLoopGroup workerGroup = new NioEventLoopGroup(); 
            try { 
                ServerBootstrap b = new ServerBootstrap(); 
                b.group(bossGroup, workerGroup); 
                b.channel(NioServerSocketChannel.class); 
                b.handler(new LoggingHandler(LogLevel.INFO)); 
                b.childHandler(new HttpUploadServerInitializer(sslCtx)); //调用Initializer
     
                Channel ch = b.bind(PORT).sync().channel(); 
     
                System.err.println("Open your web browser and navigate to " + 
                        (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/'); 
     
                ch.closeFuture().sync(); 
            } finally { 
                bossGroup.shutdownGracefully(); 
                workerGroup.shutdownGracefully(); 
            } 
        } 
    }

         d)Initializer代码,需要注意的是流处理器,

    /*
     * Copyright 2012 The Netty Project 
     * 
     * The Netty Project licenses this file to you under the Apache License, 
     * version 2.0 (the "License"); you may not use this file except in compliance 
     * with the License. You may obtain a copy of the License at: 
     * 
     *   http://www.apache.org/licenses/LICENSE-2.0 
     * 
     * Unless required by applicable law or agreed to in writing, software 
     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
     * License for the specific language governing permissions and limitations 
     * under the License. 
     */
    package io.netty.example.http.upload;
    
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.handler.codec.http.HttpContentCompressor;
    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.ssl.SslContext;
    
    public class HttpUploadServerInitializer extends ChannelInitializer<SocketChannel> {
    
        private final SslContext sslCtx;
    
        public HttpUploadServerInitializer(SslContext sslCtx) {
            this.sslCtx = sslCtx;
        }
    
        @Override
        public void initChannel(SocketChannel ch) {
            ChannelPipeline pipeline = ch.pipeline();
    
            if (sslCtx != null) {
                pipeline.addLast(sslCtx.newHandler(ch.alloc()));
            }
    
            pipeline.addLast(new HttpRequestDecoder()); //处理Request
            // Uncomment the following line if you don't want to handle HttpChunks.
            //pipeline.addLast(new HttpObjectAggregator(1048576)); //将对象组装为FullHttpRequest
            pipeline.addLast(new HttpResponseEncoder());  //处理Response
    
            // Remove the following line if you don't want automatic content compression.
            pipeline.addLast(new HttpContentCompressor());  //压缩
    
            pipeline.addLast(new HttpUploadServerHandler());
        }
    }

     

           这里需要注意一点,采用HttpRequestDecoder处理器,会将一个Request对象解析成三个对象HttpRequest、HttpCotent、LastHttpContent,这三个对象大致是这样的,HttpRequest是地址信息和头部信息,其中包括get方式传送的参数和cookie信息;HttpContent是消息体,即Body部分,即post方式form提交的内容;LastHttpContent则是消息体的末尾,即提示消息体结束,也就是整个请求结束。

          但是需要注意的是,使用HttpObjectAggregator处理器,可以将Request对象处理为FullRequest,但我测试了一下,不知道为什么,竟然卡死了,所以只好用这种笨办法,以后研究一下,这次先这样吧。

          e)Handler的代码有些长,不过还是贴出来吧。       

    /*
     * Copyright 2012 The Netty Project 
     * 
     * The Netty Project licenses this file to you under the Apache License, 
     * version 2.0 (the "License"); you may not use this file except in compliance 
     * with the License. You may obtain a copy of the License at: 
     * 
     *   http://www.apache.org/licenses/LICENSE-2.0 
     * 
     * Unless required by applicable law or agreed to in writing, software 
     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
     * License for the specific language governing permissions and limitations 
     * under the License. 
     */
    package io.netty.example.http.upload;
    
    import io.netty.buffer.ByteBuf;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelFutureListener;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.handler.codec.http.Cookie;
    import io.netty.handler.codec.http.CookieDecoder;
    import io.netty.handler.codec.http.DefaultFullHttpResponse;
    import io.netty.handler.codec.http.FullHttpResponse;
    import io.netty.handler.codec.http.HttpContent;
    import io.netty.handler.codec.http.HttpHeaders;
    import io.netty.handler.codec.http.HttpMethod;
    import io.netty.handler.codec.http.HttpObject;
    import io.netty.handler.codec.http.HttpRequest;
    import io.netty.handler.codec.http.HttpResponseStatus;
    import io.netty.handler.codec.http.HttpVersion;
    import io.netty.handler.codec.http.LastHttpContent;
    import io.netty.handler.codec.http.QueryStringDecoder;
    import io.netty.handler.codec.http.ServerCookieEncoder;
    import io.netty.handler.codec.http.multipart.Attribute;
    import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
    import io.netty.handler.codec.http.multipart.DiskAttribute;
    import io.netty.handler.codec.http.multipart.DiskFileUpload;
    import io.netty.handler.codec.http.multipart.FileUpload;
    import io.netty.handler.codec.http.multipart.HttpDataFactory;
    import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
    import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException;
    import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
    import io.netty.handler.codec.http.multipart.InterfaceHttpData;
    import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType;
    import io.netty.util.CharsetUtil;
    
    import java.io.File;
    import java.io.IOException;
    import java.net.URI;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.Set;
    import java.util.UUID;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    import com.alibaba.fastjson.JSON;
    
    import static io.netty.buffer.Unpooled.*;
    import static io.netty.handler.codec.http.HttpHeaders.Names.*;
    
    public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    
        private static final Logger logger = Logger.getLogger(HttpUploadServerHandler.class.getName());
    
        private HttpRequest request;
    
        private boolean readingChunks;
    
        private final StringBuilder responseContent = new StringBuilder();
    
        private static final HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); // Disk
                                                                                                                    // if
                                                                                                                    // size
                                                                                                                    // exceed
    
        private HttpPostRequestDecoder decoder;
    
        //////
        private String tempPath = "d:/upload/";  //文件保存目录
    
        private String url_path = "http://localhost/upload/";   //文件临时web目录
        private String errorJson;
        private Map<String, Object> mparams = new HashMap<>();  //将参数保存到map里面
    
        static {
            DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file
                                                                // on exit (in normal
                                                                // exit)
            DiskFileUpload.baseDirectory = null; // system temp directory
            DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on
                                                            // exit (in normal exit)
            DiskAttribute.baseDirectory = null; // system temp directory
        }
    
        @Override
        public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
            if (decoder != null) {
                decoder.cleanFiles();
            }
        }
    
    //处理输入对象,会执行三次,分别是HttpRequest、HttpContent、LastHttpContent @Override
    public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { if (msg instanceof HttpRequest) { HttpRequest request = this.request = (HttpRequest) msg; URI uri = new URI(request.getUri()); if (!uri.getPath().equals("/formpostmultipart")) { errorJson = "{code:-1}"; writeError(ctx, errorJson); return; } // new getMethod // for (Entry<String, String> entry : request.headers()) { // responseContent.append("HEADER: " + entry.getKey() + '=' + entry.getValue() + // " "); // } // new getMethod Set<Cookie> cookies; String value = request.headers().get(COOKIE); if (value == null) { cookies = Collections.emptySet(); } else { cookies = CookieDecoder.decode(value); } for (Cookie cookie : cookies) { mparams.put(cookie.getName(), cookie.getValue()); } // add System.out.println(JSON.toJSONString(mparams)); QueryStringDecoder decoderQuery = new QueryStringDecoder(request.getUri()); Map<String, List<String>> uriAttributes = decoderQuery.parameters(); // add mparams.putAll(uriAttributes); System.out.println(JSON.toJSONString(mparams)); // for (Entry<String, List<String>> attr: uriAttributes.entrySet()) { // for (String attrVal: attr.getValue()) { // responseContent.append("URI: " + attr.getKey() + '=' + attrVal + " "); // } // } // responseContent.append(" "); if (request.getMethod().equals(HttpMethod.GET)) { // GET Method: should not try to create a HttpPostRequestDecoder // So stop here // responseContent.append(" END OF GET CONTENT "); // Not now: LastHttpContent will be sent writeResponse(ctx.channel()); return; } try { decoder = new HttpPostRequestDecoder(factory, request); } catch (ErrorDataDecoderException e1) { e1.printStackTrace(); responseContent.append(e1.getMessage()); writeResponse(ctx.channel()); ctx.channel().close(); errorJson = "{code:-2}"; writeError(ctx, errorJson); return; } readingChunks = HttpHeaders.isTransferEncodingChunked(request); // responseContent.append("Is Chunked: " + readingChunks + " "); // responseContent.append("IsMultipart: " + decoder.isMultipart() + " "); if (readingChunks) { // Chunk version // responseContent.append("Chunks: "); readingChunks = true; } } // check if the decoder was constructed before // if not it handles the form get if (decoder != null) { if (msg instanceof HttpContent) { // New chunk is received HttpContent chunk = (HttpContent) msg; try { decoder.offer(chunk); } catch (ErrorDataDecoderException e1) { e1.printStackTrace(); // responseContent.append(e1.getMessage()); writeResponse(ctx.channel()); ctx.channel().close(); errorJson = "{code:-3}"; writeError(ctx, errorJson); return; } // responseContent.append('o'); // example of reading chunk by chunk (minimize memory usage due to // Factory) readHttpDataChunkByChunk(ctx); // example of reading only if at the end if (chunk instanceof LastHttpContent) { writeResponse(ctx.channel()); readingChunks = false; reset(); } } } else { writeResponse(ctx.channel()); } } private void reset() { request = null; // destroy the decoder to release all resources decoder.destroy(); decoder = null; } /** * Example of reading request by chunk and getting values from chunk to chunk * * @throws IOException */
    //处理post数据
    private void readHttpDataChunkByChunk(ChannelHandlerContext ctx) throws IOException { try { while (decoder.hasNext()) { InterfaceHttpData data = decoder.next(); if (data != null) { try { // new value writeHttpData(ctx, data); } finally { data.release(); } } } } catch (EndOfDataDecoderException e1) { // end // responseContent.append(" END OF CONTENT CHUNK BY CHUNK "); mparams.put("code", "-2"); } } //解析post属性,保存文件,写入map private void writeHttpData(ChannelHandlerContext ctx, InterfaceHttpData data) throws IOException { if (data.getHttpDataType() == HttpDataType.Attribute) { Attribute attribute = (Attribute) data; String value; try { value = attribute.getValue(); } catch (IOException e1) { // Error while reading data from File, only print name and error e1.printStackTrace(); // responseContent.append(" BODY Attribute: " + // attribute.getHttpDataType().name() + ": " // + attribute.getName() + " Error while reading value: " + e1.getMessage() + // " "); errorJson = "{code:-4}"; writeError(ctx, errorJson); return; } mparams.put(attribute.getName(), attribute.getValue()); System.out.println(JSON.toJSONString(mparams)); } else { if (data.getHttpDataType() == HttpDataType.FileUpload) { FileUpload fileUpload = (FileUpload) data; if (fileUpload.isCompleted()) { System.out.println(fileUpload.length()); if (fileUpload.length() > 0) { String orign_name = fileUpload.getFilename(); String file_name = UUID.randomUUID() + "." + orign_name.substring(orign_name.lastIndexOf(".") + 1); fileUpload.renameTo(new File(tempPath + file_name)); mparams.put(data.getName(), url_path + file_name); System.out.println(JSON.toJSONString(mparams)); } } else { errorJson = "{code:-5}"; writeError(ctx, errorJson); } } } } //写入response,返回给客户 private void writeResponse(Channel channel) { // Convert the response content to a ChannelBuffer. ByteBuf buf = copiedBuffer(JSON.toJSONString(mparams), CharsetUtil.UTF_8); responseContent.setLength(0); // Decide whether to close the connection or not. boolean close = HttpHeaders.Values.CLOSE.equalsIgnoreCase(request.headers().get(CONNECTION)) || request.getProtocolVersion().equals(HttpVersion.HTTP_1_0) && !HttpHeaders.Values.KEEP_ALIVE.equalsIgnoreCase(request.headers().get(CONNECTION)); // Build the response object. FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); if (!close) { // There's no need to add 'Content-Length' header // if this is the last response. response.headers().set(CONTENT_LENGTH, buf.readableBytes()); } Set<Cookie> cookies; String value = request.headers().get(COOKIE); if (value == null) { cookies = Collections.emptySet(); } else { cookies = CookieDecoder.decode(value); } if (!cookies.isEmpty()) { // Reset the cookies if necessary. for (Cookie cookie : cookies) { response.headers().add(SET_COOKIE, ServerCookieEncoder.encode(cookie)); } } // Write the response. ChannelFuture future = channel.writeAndFlush(response); // Close the connection after the write operation is done if necessary. if (close) { future.addListener(ChannelFutureListener.CLOSE); } } //返回错误信息,也是写入response private void writeError(ChannelHandlerContext ctx, String errorJson) { ByteBuf buf = copiedBuffer(errorJson, CharsetUtil.UTF_8); // Build the response object. FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf); response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); response.headers().set(CONTENT_LENGTH, buf.readableBytes()); // Write the response. ctx.channel().writeAndFlush(response); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.log(Level.WARNING, responseContent.toString(), cause); ctx.channel().close(); } }

         虽然代码很多,但是最需要注意的只有四个方法:    

    channelRead0(ChannelHandlerContext ctx, HttpObject msg):处理输入内容,会执行三次,分别是HttpRequest、HttpContent、LastHttpContent,依次处理。
    readHttpDataChunkByChunk(ChannelHandlerContext ctx):解析HttpContent时调用,即消息体时,具体执行过程在函数writeHttpData中   
    writeResponse(Channel channel):写入response,这里调用了fastjson将map转换为json字符串。

          f)上传的html文件     

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>HTML5的标题</title>
    </head>
    <body>
        <form  action="http://127.0.0.1:8090/formpostmultipart?a=张三&b=李四"  method="post"  enctype="multipart/form-data">
           <input type="text" name="name" value="shiyq"/>
           <br>
           <input type="text" name="name" value="历史地理"/>
           <br>
           <input type=file  name="file"/>
           <br>
           <input type="submit" value="上传"/>
        </form>
    </body>
    </html>

          g)启动HttpUploadServer,然后在浏览器里访问upload.html,返回结果如下

    {
        "name": "历史地理",
        "a": [
            "张三"
        ],
        "b": [
            "李四"
        ],
        "file": "http://localhost/upload/13d45df8-d6c7-4a7a-8f21-0251efeca240.png"
    }
    

      

          这里要注意的是,地址栏传递的参数是个数组,即参数名可以重复,form里面的值不可以,只能是一个。

       2)NodeJS

           nodejs相对要简单一些,但是也更让人困惑,主要遇到了两个问题。

           a)请求地址包含中文的情况,这个其实是个老问题,很容易解决,但是却卡住了半天,看来很久不写程序就是不行啊。最后的解决办法就是进行url编码。

           b)cookie设置的问题,form-data模块没有说明cookie设置的问题,官方API的request也言之不详,幸好request写的比较明白,但是默认还不开启,需要设置,还是老话,三天不写程序手就生了。

          c)环境非常简单,只需要安装request模块就可以了,命令为npm install request,尽量不要装在全局,在我的Windows 10上出现找不到模块的现象,最后安装到当前目录才解决,最后的代码如下      

    var fs = require('fs');
    var request = require('request').defaults({jar:true});  //不要忘记npm install request,不要忘记设置jar:true,否则无法设置cookied
    
    var file_path="D:/Documents/IMG_20170427_121431.jpg"
    var formData = {
        name:"路送双",
        code:"tom",
        my_file:fs.createReadStream(file_path)
    }
    
    var url = encodeURI("http://localhost:8090/formpostmultipart?a=王二&a=张三&b=李四");//对中文编码
    var j = request.jar();
    var cookie = request.cookie('key1=value1');
    var cookie1 = request.cookie('key2=value2');
    j.setCookie(cookie, url);
    j.setCookie(cookie1, url);
    
    request.post({url:url, jar:j, formData: formData}, function optionalCallback(err, httpResponse, body) {
      if (err) {
        return console.error('upload failed:', err);
      }
      console.log( body);
    });

         需要注意cookie的设置,不仅需要设置jar属性为true,还需要调用多次setCookie,还需要在request.post中指定参数,挺麻烦的。

          d)返回结果如下

    {
        "key1": "value1",
        "key2": "value2",
        "a": [
            "王二",
            "张三"
        ],
        "b": [
            "李四"
        ],
        "code": "tom",
        "my_file": "http://localhost/upload/8d8e2f9f-7513-4844-9614-0d7fb7a33a6e.jpg",
        "name": "路送双"
    }

    7、结论

         其实是个很简单的问题,不过研究过程有些长,而且很笨拙,如果用FullHttpRequest,代码会少很多,以后再研究吧。

  • 相关阅读:
    Elasticsearch常用插件集合(转)
    istio 安装与bookinfo示例运行(转)
    kubernetes发布tomcat服务,通过deployment,service布署(转)
    记一次ceph集群的严重故障 (转)
    Linux centos 7 安装NFS服务
    Centos7下使用Ceph-deploy快速部署Ceph分布式存储-操作记录(转)
    docker 安装 jmeter
    idea 2018.1破解激活方法,有效期至2099年
    MySQL和Oracle的区别
    SpringCloud面试题
  • 原文地址:https://www.cnblogs.com/stone-fly/p/8127960.html
Copyright © 2020-2023  润新知