Netty 轻松实现文件上传功能
今天我们来完成一个使用netty进行文件传输的任务。在实际项目中,文件传输通常采用ftp或者http附件的方式。事实上通过tcp socket+file的方式进行文件传输也有一定的应用场景,尽管不是主流,但是掌握这种文件传输方式还是比较重要的,特别是针对两个跨主机的jvm进程之间进行持久化数据的相互交换。
而使用netty来进行文件传输也是利用netty天然的优势:零拷贝功能。很多同学都听说过netty的”零拷贝”功能,但是具体体现在哪里又不知道,下面我们就简要介绍下:
netty的“零拷贝”主要体现在如下三个方面:
- netty的接收和发送bytebuffer采用direct buffers,使用堆外直接内存进行socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(heap buffers)进行socket读写,jvm会将堆内存buffer拷贝一份到直接内存中,然后才写入socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- netty提供了组合buffer对象,可以聚合多个bytebuffer对象,用户可以像操作一个buffer那样方便的对组合buffer进行操作,避免了传统通过内存拷贝的方式将几个小buffer合并成一个大的buffer。
- netty的文件传输采用了transferto方法,它可以直接将文件缓冲区的数据发送到目标channel,避免了传统通过循环write方式导致的内存拷贝问题。
具体的分析在此就不多做介绍,有兴趣的可以查阅相关文档。我们还是把重点放在文件传输上。netty作为高性能的服务器端异步io框架必然也离不开文件读写功能,我们可以使用netty模拟http的形式通过网页上传文件写入服务器,当然要使用http的形式那你也用不着netty!大材小用。
netty4中如果想使用http形式上传文件你还得借助第三方jar包:okhttp。使用该jar完成http请求的发送。但是在netty5 中已经为我们写好了,我们可以直接调用netty5的api就可以实现。所以netty4和5的差别还是挺大的,至于使用哪个,那就看你们公司选择哪一个了!本文目前使用netty4来实现文件上传功能。下面我们上代码:
pom文件:
<dependency> <groupid>io.netty</groupid> <artifactid>netty-all</artifactid> <version>4.1.5.final</version> </dependency>
server端:
import io.netty.bootstrap.serverbootstrap; import io.netty.channel.channel; import io.netty.channel.channelfuture; import io.netty.channel.channelinitializer; import io.netty.channel.channeloption; import io.netty.channel.eventloopgroup; import io.netty.channel.nio.nioeventloopgroup; import io.netty.channel.socket.nio.nioserversocketchannel; import io.netty.handler.codec.serialization.classresolvers; import io.netty.handler.codec.serialization.objectdecoder; import io.netty.handler.codec.serialization.objectencoder; public class fileuploadserver { public void bind(int port) throws exception { eventloopgroup bossgroup = new nioeventloopgroup(); eventloopgroup workergroup = new nioeventloopgroup(); try { serverbootstrap b = new serverbootstrap(); b.group(bossgroup, workergroup).channel(nioserversocketchannel.class).option(channeloption.so_backlog, 1024).childhandler(new channelinitializer<channel>() { @override protected void initchannel(channel ch) throws exception { ch.pipeline().addlast(new objectencoder()); ch.pipeline().addlast(new objectdecoder(integer.max_value, classresolvers.weakcachingconcurrentresolver(null))); // 最大长度 ch.pipeline().addlast(new fileuploadserverhandler()); } }); channelfuture f = b.bind(port).sync(); f.channel().closefuture().sync(); } finally { bossgroup.shutdowngracefully(); workergroup.shutdowngracefully(); } } public static void main(string[] args) { int port = 8080; if (args != null && args.length > 0) { try { port = integer.valueof(args[0]); } catch (numberformatexception e) { e.printstacktrace(); } } try { new fileuploadserver().bind(port); } catch (exception e) { e.printstacktrace(); } } }
server端handler:
import io.netty.channel.channelhandlercontext; import io.netty.channel.channelinboundhandleradapter; import java.io.file; import java.io.randomaccessfile; public class fileuploadserverhandler extends channelinboundhandleradapter { private int byteread; private volatile int start = 0; private string file_dir = "d:"; @override public void channelread(channelhandlercontext ctx, object msg) throws exception { if (msg instanceof fileuploadfile) { fileuploadfile ef = (fileuploadfile) msg; byte[] bytes = ef.getbytes(); byteread = ef.getendpos(); string md5 = ef.getfile_md5();//文件名 string path = file_dir + file.separator + md5; file file = new file(path); randomaccessfile randomaccessfile = new randomaccessfile(file, "rw"); randomaccessfile.seek(start); randomaccessfile.write(bytes); start = start + byteread; if (byteread > 0) { ctx.writeandflush(start); } else { randomaccessfile.close(); ctx.close(); } } } @override public void exceptioncaught(channelhandlercontext ctx, throwable cause) { cause.printstacktrace(); ctx.close(); } }
client端:
import io.netty.bootstrap.bootstrap; import io.netty.channel.channel; import io.netty.channel.channelfuture; import io.netty.channel.channelinitializer; import io.netty.channel.channeloption; import io.netty.channel.eventloopgroup; import io.netty.channel.nio.nioeventloopgroup; import io.netty.channel.socket.nio.niosocketchannel; import io.netty.handler.codec.serialization.classresolvers; import io.netty.handler.codec.serialization.objectdecoder; import io.netty.handler.codec.serialization.objectencoder; import java.io.file; public class fileuploadclient { public void connect(int port, string host, final fileuploadfile fileuploadfile) throws exception { eventloopgroup group = new nioeventloopgroup(); try { bootstrap b = new bootstrap(); b.group(group).channel(niosocketchannel.class).option(channeloption.tcp_nodelay, true).handler(new channelinitializer<channel>() { @override protected void initchannel(channel ch) throws exception { ch.pipeline().addlast(new objectencoder()); ch.pipeline().addlast(new objectdecoder(classresolvers.weakcachingconcurrentresolver(null))); ch.pipeline().addlast(new fileuploadclienthandler(fileuploadfile)); } }); channelfuture f = b.connect(host, port).sync(); f.channel().closefuture().sync(); } finally { group.shutdowngracefully(); } } public static void main(string[] args) { int port = 8080; if (args != null && args.length > 0) { try { port = integer.valueof(args[0]); } catch (numberformatexception e) { e.printstacktrace(); } } try { fileuploadfile uploadfile = new fileuploadfile(); file file = new file("c:/1.txt"); string filemd5 = file.getname();// 文件名 uploadfile.setfile(file); uploadfile.setfile_md5(filemd5); uploadfile.setstarpos(0);// 文件开始位置 new fileuploadclient().connect(port, "127.0.0.1", uploadfile); } catch (exception e) { e.printstacktrace(); } } }
client端handler:
import io.netty.channel.channelhandlercontext; import io.netty.channel.channelinboundhandleradapter; import java.io.filenotfoundexception; import java.io.ioexception; import java.io.randomaccessfile; public class fileuploadclienthandler extends channelinboundhandleradapter { private int byteread; private volatile int start = 0; private volatile int lastlength = 0; public randomaccessfile randomaccessfile; private fileuploadfile fileuploadfile; public fileuploadclienthandler(fileuploadfile ef) { if (ef.getfile().exists()) { if (!ef.getfile().isfile()) { system.out.println("not a file :" + ef.getfile()); return; } } this.fileuploadfile = ef; } public void channelactive(channelhandlercontext ctx) { try { randomaccessfile = new randomaccessfile(fileuploadfile.getfile(), "r"); randomaccessfile.seek(fileuploadfile.getstarpos()); lastlength = (int) randomaccessfile.length() / 10; byte[] bytes = new byte[lastlength]; if ((byteread = randomaccessfile.read(bytes)) != -1) { fileuploadfile.setendpos(byteread); fileuploadfile.setbytes(bytes); ctx.writeandflush(fileuploadfile); } else { system.out.println("文件已经读完"); } } catch (filenotfoundexception e) { e.printstacktrace(); } catch (ioexception i) { i.printstacktrace(); } } @override public void channelread(channelhandlercontext ctx, object msg) throws exception { if (msg instanceof integer) { start = (integer) msg; if (start != -1) { randomaccessfile = new randomaccessfile(fileuploadfile.getfile(), "r"); randomaccessfile.seek(start); system.out.println("块儿长度:" + (randomaccessfile.length() / 10)); system.out.println("长度:" + (randomaccessfile.length() - start)); int a = (int) (randomaccessfile.length() - start); int b = (int) (randomaccessfile.length() / 10); if (a < b) { lastlength = a; } byte[] bytes = new byte[lastlength]; system.out.println("-----------------------------" + bytes.length); if ((byteread = randomaccessfile.read(bytes)) != -1 && (randomaccessfile.length() - start) > 0) { system.out.println("byte 长度:" + bytes.length); fileuploadfile.setendpos(byteread); fileuploadfile.setbytes(bytes); try { ctx.writeandflush(fileuploadfile); } catch (exception e) { e.printstacktrace(); } } else { randomaccessfile.close(); ctx.close(); system.out.println("文件已经读完--------" + byteread); } } } } // @override // public void channelread(channelhandlercontext ctx, object msg) throws // exception { // system.out.println("server is speek :"+msg.tostring()); // fileregion filer = (fileregion) msg; // string path = "e://apk//apkmd5.txt"; // file fl = new file(path); // fl.createnewfile(); // randomaccessfile rdafile = new randomaccessfile(path, "rw"); // fileregion f = new defaultfileregion(rdafile.getchannel(), 0, // rdafile.length()); // // system.out.println("this is" + ++counter + "times receive server:[" // + msg + "]"); // } // @override // public void channelreadcomplete(channelhandlercontext ctx) throws // exception { // ctx.flush(); // } public void exceptioncaught(channelhandlercontext ctx, throwable cause) { cause.printstacktrace(); ctx.close(); } // @override // protected void channelread0(channelhandlercontext ctx, string msg) // throws exception { // string a = msg; // system.out.println("this is"+ // ++counter+"times receive server:["+msg+"]"); // } }
我们还自定义了一个对象,用于统计文件上传进度的:
import java.io.file; import java.io.serializable; public class fileuploadfile implements serializable { private static final long serialversionuid = 1l; private file file;// 文件 private string file_md5;// 文件名 private int starpos;// 开始位置 private byte[] bytes;// 文件字节数组 private int endpos;// 结尾位置 public int getstarpos() { return starpos; } public void setstarpos(int starpos) { this.starpos = starpos; } public int getendpos() { return endpos; } public void setendpos(int endpos) { this.endpos = endpos; } public byte[] getbytes() { return bytes; } public void setbytes(byte[] bytes) { this.bytes = bytes; } public file getfile() { return file; } public void setfile(file file) { this.file = file; } public string getfile_md5() { return file_md5; } public void setfile_md5(string file_md5) { this.file_md5 = file_md5; } }
输出为:
块儿长度:894
长度:8052
-----------------------------894
byte 长度:894
块儿长度:894
长度:7158
-----------------------------894
byte 长度:894
块儿长度:894
长度:6264
-----------------------------894
byte 长度:894
块儿长度:894
长度:5370
-----------------------------894
byte 长度:894
块儿长度:894
长度:4476
-----------------------------894
byte 长度:894
块儿长度:894
长度:3582
-----------------------------894
byte 长度:894
块儿长度:894
长度:2688
-----------------------------894
byte 长度:894
块儿长度:894
长度:1794
-----------------------------894
byte 长度:894
块儿长度:894
长度:900
-----------------------------894
byte 长度:894
块儿长度:894
长度:6
-----------------------------6
byte 长度:6
块儿长度:894
长度:0
-----------------------------0
文件已经读完--------0process finished with exit code 0
这样就实现了服务器端文件的上传,当然我们也可以使用http的形式。
server端:
import io.netty.bootstrap.serverbootstrap; import io.netty.channel.channelfuture; import io.netty.channel.eventloopgroup; import io.netty.channel.nio.nioeventloopgroup; import io.netty.channel.socket.nio.nioserversocketchannel; public class httpfileserver implements runnable { private int port; public httpfileserver(int port) { super(); this.port = port; } @override public void run() { eventloopgroup bossgroup = new nioeventloopgroup(1); eventloopgroup workergroup = new nioeventloopgroup(); serverbootstrap serverbootstrap = new serverbootstrap(); serverbootstrap.group(bossgroup, workergroup); serverbootstrap.channel(nioserversocketchannel.class); //serverbootstrap.handler(new logginghandler(loglevel.info)); serverbootstrap.childhandler(new httpchannelinitlalizer()); try { channelfuture f = serverbootstrap.bind(port).sync(); f.channel().closefuture().sync(); } catch (interruptedexception e) { e.printstacktrace(); } finally { bossgroup.shutdowngracefully(); workergroup.shutdowngracefully(); } } public static void main(string[] args) { httpfileserver b = new httpfileserver(9003); new thread(b).start(); } }
server端initializer:
import io.netty.channel.channelinitializer; import io.netty.channel.channelpipeline; import io.netty.channel.socket.socketchannel; import io.netty.handler.codec.http.httpobjectaggregator; import io.netty.handler.codec.http.httpservercodec; import io.netty.handler.stream.chunkedwritehandler; public class httpchannelinitlalizer extends channelinitializer<socketchannel> { @override protected void initchannel(socketchannel ch) throws exception { channelpipeline pipeline = ch.pipeline(); pipeline.addlast(new httpservercodec()); pipeline.addlast(new httpobjectaggregator(65536)); pipeline.addlast(new chunkedwritehandler()); pipeline.addlast(new httpchannelhandler()); } }
server端hadler:
import static io.netty.handler.codec.http.httpheaders.names.content_type; import static io.netty.handler.codec.http.httpresponsestatus.bad_request; import static io.netty.handler.codec.http.httpresponsestatus.forbidden; import static io.netty.handler.codec.http.httpresponsestatus.internal_server_error; import static io.netty.handler.codec.http.httpresponsestatus.not_found; import static io.netty.handler.codec.http.httpversion.http_1_1; import io.netty.buffer.unpooled; import io.netty.channel.channelfuture; import io.netty.channel.channelfuturelistener; import io.netty.channel.channelhandlercontext; import io.netty.channel.channelprogressivefuture; import io.netty.channel.channelprogressivefuturelistener; import io.netty.channel.simplechannelinboundhandler; import io.netty.handler.codec.http.defaultfullhttpresponse; import io.netty.handler.codec.http.defaulthttpresponse; import io.netty.handler.codec.http.fullhttprequest; import io.netty.handler.codec.http.fullhttpresponse; import io.netty.handler.codec.http.httpchunkedinput; import io.netty.handler.codec.http.httpheaders; import io.netty.handler.codec.http.httpresponse; 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.stream.chunkedfile; import io.netty.util.charsetutil; import io.netty.util.internal.systempropertyutil; 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 javax.activation.mimetypesfiletypemap; public class httpchannelhandler extends simplechannelinboundhandler<fullhttprequest> { public static final string http_date_format = "eee, dd mmm yyyy hh:mm:ss zzz"; public static final string http_date_gmt_timezone = "gmt"; public static final int http_cache_seconds = 60; @override protected void channelread0(channelhandlercontext ctx, fullhttprequest request) throws exception { // 监测解码情况 if (!request.getdecoderresult().issuccess()) { senderror(ctx, bad_request); return; } final string uri = request.geturi(); final string path = sanitizeuri(uri); system.out.println("get file:"+path); if (path == null) { senderror(ctx, forbidden); return; } //读取要下载的文件 file file = new file(path); if (file.ishidden() || !file.exists()) { senderror(ctx, not_found); return; } if (!file.isfile()) { senderror(ctx, forbidden); return; } randomaccessfile raf; try { raf = new randomaccessfile(file, "r"); } catch (filenotfoundexception ignore) { senderror(ctx, not_found); return; } long filelength = raf.length(); httpresponse response = new defaulthttpresponse(httpversion.http_1_1, httpresponsestatus.ok); httpheaders.setcontentlength(response, filelength); setcontenttypeheader(response, file); //setdateandcacheheaders(response, file); if (httpheaders.iskeepalive(request)) { response.headers().set("connection", httpheaders.values.keep_alive); } // write the initial line and the header. ctx.write(response); // write the content. channelfuture sendfilefuture = ctx.write(new httpchunkedinput(new chunkedfile(raf, 0, filelength, 8192)), ctx.newprogressivepromise()); //sendfuture用于监视发送数据的状态 sendfilefuture.addlistener(new channelprogressivefuturelistener() { @override public void operationprogressed(channelprogressivefuture future, long progress, long total) { if (total < 0) { // total unknown system.err.println(future.channel() + " transfer progress: " + progress); } else { system.err.println(future.channel() + " transfer progress: " + progress + " / " + total); } } @override public void operationcomplete(channelprogressivefuture future) { system.err.println(future.channel() + " transfer complete."); } }); // write the end marker channelfuture lastcontentfuture = ctx.writeandflush(lasthttpcontent.empty_last_content); // decide whether to close the connection or not. if (!httpheaders.iskeepalive(request)) { // close the connection when the whole content is written out. lastcontentfuture.addlistener(channelfuturelistener.close); } } @override public void exceptioncaught(channelhandlercontext ctx, throwable cause) { cause.printstacktrace(); if (ctx.channel().isactive()) { senderror(ctx, internal_server_error); } ctx.close(); } private static final pattern insecure_uri = pattern.compile(".*[<>&\"].*"); private static string sanitizeuri(string uri) { // decode the path. try { uri = urldecoder.decode(uri, "utf-8"); } catch (unsupportedencodingexception e) { throw new error(e); } if (!uri.startswith("/")) { return null; } // convert file separators. uri = uri.replace('/', file.separatorchar); // simplistic dumb security check. // you will have to do something serious in the production environment. if (uri.contains(file.separator + '.') || uri.contains('.' + file.separator) || uri.startswith(".") || uri.endswith(".") || insecure_uri.matcher(uri).matches()) { return null; } // convert to absolute path. return systempropertyutil.get("user.dir") + file.separator + uri; } private static void senderror(channelhandlercontext ctx, httpresponsestatus status) { fullhttpresponse response = new defaultfullhttpresponse(http_1_1, status, unpooled.copiedbuffer("failure: " + status + "\r\n", charsetutil.utf_8)); response.headers().set(content_type, "text/plain; charset=utf-8"); // close the connection as soon as the error message is sent. ctx.writeandflush(response).addlistener(channelfuturelistener.close); } /** * sets the content type header for the http response * * @param response * http response * @param file * file to extract content type */ private static void setcontenttypeheader(httpresponse response, file file) { mimetypesfiletypemap m = new mimetypesfiletypemap(); string contenttype = m.getcontenttype(file.getpath()); if (!contenttype.equals("application/octet-stream")) { contenttype += "; charset=utf-8"; } response.headers().set(content_type, contenttype); } }
client端:
import io.netty.bootstrap.bootstrap; import io.netty.channel.channelfuture; import io.netty.channel.channelinitializer; import io.netty.channel.channeloption; import io.netty.channel.eventloopgroup; import io.netty.channel.nio.nioeventloopgroup; import io.netty.channel.socket.socketchannel; import io.netty.channel.socket.nio.niosocketchannel; import io.netty.handler.codec.http.defaultfullhttprequest; import io.netty.handler.codec.http.httpheaders; import io.netty.handler.codec.http.httpmethod; import io.netty.handler.codec.http.httprequestencoder; import io.netty.handler.codec.http.httpresponsedecoder; import io.netty.handler.codec.http.httpversion; import io.netty.handler.stream.chunkedwritehandler; import java.net.uri; public class httpdownloadclient { /** * 下载http资源 向服务器下载直接填写要下载的文件的相对路径 * (↑↑↑建议只使用字母和数字对特殊字符对字符进行部分过滤可能导致异常↑↑↑) * 向互联网下载输入完整路径 * @param host 目的主机ip或域名 * @param port 目标主机端口 * @param url 文件路径 * @param local 本地存储路径 * @throws exception */ public void connect(string host, int port, string url, final string local) throws exception { eventloopgroup workergroup = new nioeventloopgroup(); try { bootstrap b = new bootstrap(); b.group(workergroup); b.channel(niosocketchannel.class); b.option(channeloption.so_keepalive, true); b.handler(new childchannelhandler(local)); // start the client. channelfuture f = b.connect(host, port).sync(); uri uri = new uri(url); defaultfullhttprequest request = new defaultfullhttprequest( httpversion.http_1_1, httpmethod.get, uri.toasciistring()); // 构建http请求 request.headers().set(httpheaders.names.host, host); request.headers().set(httpheaders.names.connection, httpheaders.values.keep_alive); request.headers().set(httpheaders.names.content_length, request.content().readablebytes()); // 发送http请求 f.channel().write(request); f.channel().flush(); f.channel().closefuture().sync(); } finally { workergroup.shutdowngracefully(); } } private class childchannelhandler extends channelinitializer<socketchannel> { string local; public childchannelhandler(string local) { this.local = local; } @override protected void initchannel(socketchannel ch) throws exception { // 客户端接收到的是httpresponse响应,所以要使用httpresponsedecoder进行解码 ch.pipeline().addlast(new httpresponsedecoder()); // 客户端发送的是httprequest,所以要使用httprequestencoder进行编码 ch.pipeline().addlast(new httprequestencoder()); ch.pipeline().addlast(new chunkedwritehandler()); ch.pipeline().addlast(new httpdownloadhandler(local)); } } public static void main(string[] args) throws exception { httpdownloadclient client = new httpdownloadclient(); //client.connect("127.0.0.1", 9003,"/file/pppp/1.doc","1.doc"); // client.connect("zlysix.gree.com", 80, "http://zlysix.gree.com/helloweb/download/20m.apk", "20m.apk"); client.connect("www.ghost64.com", 80, "http://www.ghost64.com/qqtupian/zixunimg/local/2017/05/27/1495855297602.jpg", "1495855297602.jpg"); } }
client端handler:
import java.io.file; import java.io.fileoutputstream; import io.netty.buffer.bytebuf; import io.netty.channel.channelhandlercontext; import io.netty.channel.channelinboundhandleradapter; import io.netty.handler.codec.http.httpcontent; //import io.netty.handler.codec.http.httpheaders; import io.netty.handler.codec.http.httpresponse; import io.netty.handler.codec.http.lasthttpcontent; import io.netty.util.internal.systempropertyutil; /** * @author:yangyue * @description: * @date: created in 9:15 on 2017/5/28. */ public class httpdownloadhandler extends channelinboundhandleradapter { private boolean readingchunks = false; // 分块读取开关 private fileoutputstream foutputstream = null;// 文件输出流 private file localfile = null;// 下载文件的本地对象 private string local = null;// 待下载文件名 private int succcode;// 状态码 public httpdownloadhandler(string local) { this.local = local; } @override public void channelread(channelhandlercontext ctx, object msg) throws exception { if (msg instanceof httpresponse) {// response头信息 httpresponse response = (httpresponse) msg; succcode = response.getstatus().code(); if (succcode == 200) { setdownloadfile();// 设置下载文件 readingchunks = true; } // system.out.println("content_type:" // + response.headers().get(httpheaders.names.content_type)); } if (msg instanceof httpcontent) {// response体信息 httpcontent chunk = (httpcontent) msg; if (chunk instanceof lasthttpcontent) { readingchunks = false; } bytebuf buffer = chunk.content(); byte[] dst = new byte[buffer.readablebytes()]; if (succcode == 200) { while (buffer.isreadable()) { buffer.readbytes(dst); foutputstream.write(dst); buffer.release(); } if (null != foutputstream) { foutputstream.flush(); } } } if (!readingchunks) { if (null != foutputstream) { system.out.println("download done->"+ localfile.getabsolutepath()); foutputstream.flush(); foutputstream.close(); localfile = null; foutputstream = null; } ctx.channel().close(); } } /** * 配置本地参数,准备下载 */ private void setdownloadfile() throws exception { if (null == foutputstream) { local = systempropertyutil.get("user.dir") + file.separator +local; //system.out.println(local); localfile = new file(local); if (!localfile.exists()) { localfile.createnewfile(); } foutputstream = new fileoutputstream(localfile); } } @override public void exceptioncaught(channelhandlercontext ctx, throwable cause) throws exception { system.out.println("管道异常:" + cause.getmessage()); cause.printstacktrace(); ctx.channel().close(); } }
这里客户端我放的是网络连接,下载的是一副图片,启动服务端和客户端就可以看到这个图片被下载到了工程的根目录下。
到此这篇关于netty 轻松实现文件上传的文章就介绍到这了,更多相关netty 文件上传内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
上一篇: 带你入门Java的数组