在查看iteye的时候,发现了这么一个帖子:文件上传:服务器如何终止客户端继续上传。在原文中,提到了这么一个问题:该场景为:当上传文件不满足要求(服务器端检查:如文件超过大小限制,扩展名不正确等),服务器在等待文件上传完成之前立即返回,目的是终止浏览器继续上传。
这样就不必需要待到客户端在把所有数据都上传到服务器端之后,再给客户端返回一个上传不合法的提示,同时,这也是一个避免大量战胜客户端内存和服务器端内存的一个方法。
然而实际情况,并非如此,尽管在服务器端已经关闭了接收客户端上传数据的流,并且已经返回了结果,但这并不能阻止客户端继续上传未上传的数据。究其原因,还是因为在服务器端,处理的输入流并不是直接表示当前请求的输入流,关闭一个请求处理的输入流并不代表关闭此次请求的输入流,所以客户端仍然会继续上传剩下的数据。
笔者做了一个简单的测试,在本机建立一个使用struts2的应用,设定最大上传数据大小为6*1024*1024(即6M)大小。当上传一个大于6M的数据时,服务器端报如一个数据过大的异常。但这并不能演示客户端的情况。于是,将上传数据扩大为600M(即一个oracle的安装文件)。在这次,服务器快速的返回了,然而客户端浏览器确已不再有反应了,进度条还在继续,还在上传中。过了好一会儿,才最终显示由服务器端返回的数据。
在帖子中,提到一个处理这种问题的方法,即想办法找到表示当前请求的socket,当数据不合法时,直接关掉当前的socket,来关掉此次请求。由于笔者使用的是tomcat作为一个web应用服务器,所以通过反射的方法 找到了当前请求的socket,然后直接关掉此socket,来演示一下直接关掉请求的结果。处理代码如下所示:在JakataMultiPartRequest的parse(HttpServletRequest servletRequest, String saveDir)方法中,通过在catch异常的处理结果中添加以下处理代码:
try { Field field = RequestFacade.class.getDeclaredField("request"); field.setAccessible(true); InputStream inputStream = ((InternalInputBuffer)((Request)field.get(( ((RequestFacade)servletRequest)))) .getCoyoteRequest().getInputBuffer()).getInputStream(); Field sf = inputStream.getClass().getDeclaredField("socket"); sf.setAccessible(true); Socket socket = (Socket)sf.get(inputStream); OutputStream outputStream = socket.getOutputStream(); outputStream.write("too long to upload".getBytes()); outputStream.flush(); inputStream.close(); }catch(Exception ex) { ex.printStackTrace(); throw new RuntimeException(ex); }
即直接关掉在tomcat中表示当前请求处理的socket。有了这个之后,界面上的反应确实有变化了。
在以上代码中,本意是想在数据超过大小的时候,往客户端写一条数据信息,并关掉当前socket(关掉inputStream即可关掉当前socket),并期望浏览器上显示这里回显的数据。然而,结果并不是这样,实际的结果如下:
因为请求socket被关闭了,所以界面上直接显示以上信息,即连接被关闭了,并没有出现想要的结果。
在网上搜索了一番,结论即是,浏览器可以探知到此次请求被中断,所以能够快速地响应。然而,由于请求数据还未完成,所以连接被关闭之后,并不能读取到需要显示的数据,或者是在读取过程中失败了,导致客户端不能显示由服务器端主动关闭socket前发出的响应信息。
接下来,就要验证这种情况了,以及如何读取到由服务器返回的错误信息了。
最开始使用了URLConnection来模拟http请求,即使用HttpURLConnection发起一个请求,并向请求中发送相应的数据。然而在实际测试时,发现在进行传输数据时,并不能在urlConnection的outputStream数据输出之前来获取一个inputStream,也不能获取到errorStream,即不能直接读取到由服务器端返回的错误信息了。因此,使用了socket来模拟此次数据请求。
首先建立连接,这是最基本的:
Socket socket = new Socket(); socket.connect(new InetSocketAddress(8080)); final InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream();
开启一个后台线程,从inputStream中读取数据,保证由服务器端发送回来的数据,在socket关闭之前,都被读取到了。
new Thread() { @Override public void run() { ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { while(!k) { Thread.sleep(100); } byte[] bytes = new byte[1024]; int i = 0; while((i = inputStream.read(bytes)) != -1) stream.write(bytes, 0, i); } catch(Exception e) { System.out.println("error->" + e.getMessage()); } finally { System.out.println("-KKKK->" + new String(stream.toByteArray())); } } }.start();
准备上传数据长度以及boundary,因为是要上传文件数据
String boundary = "\r\n-----------------------------1234567890--\r\n"; String c_FileData = "-----------------------------1234567890\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: text/plain\r\n\r\n"; String uploadFileName = String.format(c_FileData, "zip", "x.zip"); File uploadFile = new File("/home/flym/x.zip"); long ContentLength = uploadFileName.getBytes().length + uploadFile.length() + boundary.getBytes().length;
继续书写,http头信息,这些信息即是http协议中的标准部分了,如果自己进行http处理,还是要多了解一下http协议才能写:
//header outputStream.write("POST /login/do/login.q HTTP/1.1\r\n".getBytes()); outputStream.write("Accept: */*\r\n".getBytes()); outputStream.write("Accept-Language: zh-cn\r\n".getBytes()); outputStream.write( "Content-Type: multipart/form-data; boundary=---------------------------1234567890\r\n".getBytes()); outputStream.write("Accept-Encoding: gzip, deflate\r\n".getBytes()); outputStream.write("User-Agent: Mozilla/4.0 \r\n".getBytes()); outputStream.write("Host: 127.0.0.1:8080\r\n".getBytes()); outputStream.write(String.format("Content-Length: %d\r\n", ContentLength).getBytes()); outputStream.write("Connection: Keep-Alive\r\n".getBytes()); outputStream.write("Cache-Control: no-cache\r\n".getBytes()); outputStream.write("\r\n".getBytes());
准备发送正文,这里为了测试一下,服务器端的错误信息是在什么时候发送回来的,对上传数据的处理作了一些小小的修改,即不再一次性写所有数据,而是一部分一部分地写(并且未做仔细的数据越界检查)。
//post content outputStream.write(uploadFileName.getBytes()); try { byte[] bytes = FileCopyUtils.copyToByteArray(uploadFile); int k = bytes.length / 100; for(int i = 0;i < 100;i++) { System.out.println("->" + i); outputStream.write(bytes, i * k, (i + 1) * k); } outputStream.write(boundary.getBytes()); outputStream.flush(); outputStream.close(); } finally { T2.k = true; }
最终运行结果如下:
由上面的输出可以看出以下几点:
- 服务器端处理数据异常,并不需要等到所有数据上传之后才会进行处理,即实际上在接收到content-length以及content-type,即可进行上传数据判断了。
- 在服务器端发现错误,并且关闭socket之后,客户端能够即时发现这个异常,并且终止数据上传(发生异常,上传肯定被终止了)
- 在服务器端发现错误,并且有返回数据到客户端,那么客户端在读取数据时,即可读取到此服务器端发回的数据
那么,由实际情况可以看出。如果要单独处理文件上传的话,可以使用客户端直接使用socket上传文件的方法,而不再通过http协议进行请求构建。当发现错误时,并不直接继续上传数据,可以直接中断请求,并且读取返回的错误信息。这些都是浏览器所不能代表的。
回到原来的问题,如果客户端真上传了超过规定的数据大小,又该怎么办呢?一种情况,即是在可以接受的范围之后,接收所有上传数据之后,再由服务器端通知客户端数据非法。第二种情况,就是通过某路途径直接关闭当前请求了,这种处理对客户端不友好,但至少可以避免大量的网络传递。
当然,如果一个系统,需要大量的数据上传,通过单独的上传组件(非http控件)来进行数据上传,那是最好的选择了。当前国内名大邮箱服务商不正是通过控件来进行大数据附件处理,即可传递大数据量数据,又可避免超过大小的数据量传输。
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/web-programe-server-can-not-prevent-client-continue-to-upload-data-conception.html
Very NB! 解决一个难题!