在C#客户端用HTTP上传文件到Java服务器
最近在做C / S 开发,需要在C#客户端上传文件到Java后台进行处理。
对于较大的文件我们可以直接用FTP协议传文件,较小的文件则可以向B / S 一样用HTTP上传。
首先,由于要传文件,我们需要用 POST 来发送数据。GET 有长度限制,而且数据跟在URL后面。
既然要发送POST请求,我们先来看看POST 请求的报文格式。
HTTP 报文介绍
先写一个简单的Html 页面发送一个表单来观察它发出的POST 报文,表单中包含一个上传的文件和文件描述的文本。
<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8" /> <title>文件上传</title> </head> <body> <form method="post" enctype="multipart/form-data" action="http://www.baidu.com/form"> <input type="file" name="file"> <input type="text" name="description" <br /> <input type="reset" value="reset"> <input type="submit" value="submit"> </form> </body> </html>
在Chrom 上的报文格式如下:
POST /form HTTP/1.1 Host: www.baidu.com Connection: keep-alive Content-Length: 2417 Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Origin: null Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryM4LGQcTCCIBilnPT Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8 Cookie: BAIDUID=9A110F7F907AEAC501CD156DDE0EA380:FG=1 ------WebKitFormBoundaryM4LGQcTCCIBilnPT Content-Disposition: form-data; name="file"; filename="close.png" Content-Type: image/png 这里包括了图片的二进制数据 ------WebKitFormBoundaryM4LGQcTCCIBilnPT Content-Disposition: form-data; name="description" This is a image ------WebKitFormBoundaryM4LGQcTCCIBilnPT--
HTTP报文由三个部分组成:对报文进行描述的起始行(start line),包含属性的首部(header)块,以及可选的、包含数据的主体(body)部分。
请求报文的起始行格式为<method> <request-URL> <version>
POST /form HTTP/1.1
method :为客户端希望服务器对资源进行的动作,一般为GET、POST、HEAD等。
请求URL:为资源的绝对路径,这里是表单Action决定的。
版本:保报文所使用的Http 版本,如1.1 ,1.0。
HTTP 首部块
可以有零个或多个首部,每个首部都包含一个名字,后面跟着一个冒号( : ),然后是一个可选的空格,接着是一个值,最后是一个CRLF( /r/n )。首部是由一个空行(CRLF)结束的。表示了首部列表的结束和实体主体部分的开始。在自己构造报文时一定要注意加换行和空行,以免造成格式错误。在HTTP 1.1 中,要求有效的请求或响应中必须包含特定的首部。请求首部如下:
Host: www.baidu.com Connection: keep-alive Content-Length: 2417 Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryM4LGQcTCCIBilnPT Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8
简单解释一下几个首部:
Host:接收请求的服务器地址
Connection:允许客户端和服务器指定与请求/响应连接有关的选项,Keep-alive 表示持久连接
Content-Length:实体主体的大小,这个在构造报文的时候一定要设置
CacheControl:控制缓存的行为
Accept:用户代理可处理的媒体类型
User-Agent:HTTP客户端程序的信息,浏览器的信息
Content-Type:实体主体的媒体类型,表单中有文件上传应设置为multipart/form-data。boundary 很重要,这是一个识别文件流的边界,用来标识文件开始和结尾的位置。
Accept-Encoding:是浏览器发给服务器,声明浏览器支持的编码类型
Accept-Language声明浏览器支持的语言
HTTP 数据主体
这部分为HTTP要传输的内容。
开始的boundary 就是在Content-Type中设置的值,boundary用于作为请求参数之间的界限标识,在多个参数之间要有一个明确的界限,这样服务器才能正确的解析到参数。它有格式要求,开头必须是--,不同的浏览器产生的boundary也不同,但前面都要有-- 。
Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。
中间的就是我们传输的数据了。
最后还要加上一个boundary--,不要忘记最后的--。
这样报文就构造结束了。
C# 中发送POST请求
private void UploadRequest(string url, string filePath) { // 时间戳,用做boundary string timeStamp = DateTime.Now.Ticks.ToString("x"); //根据uri创建HttpWebRequest对象 HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(new Uri(url)); httpReq.Method = "POST"; httpReq.AllowWriteStreamBuffering = false; //对发送的数据不使用缓存 httpReq.Timeout = 300000; //设置获得响应的超时时间(300秒) httpReq.ContentType = "multipart/form-data; boundary=" + timeStamp; //文件 FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); BinaryReader binaryReader = new BinaryReader(fileStream); //头信息 string boundary = "--" + timeStamp; string dataFormat = boundary + " Content-Disposition: form-data; name="{0}";filename="{1}" Content-Type:application/octet-stream "; string header = string.Format(dataFormat, "file", Path.GetFileName(filePath)); byte[] postHeaderBytes = Encoding.UTF8.GetBytes(header); //结束边界 byte[] boundaryBytes = Encoding.ASCII.GetBytes(" --" + timeStamp + "-- "); long length = fileStream.Length + postHeaderBytes.Length + boundaryBytes.Length; httpReq.ContentLength = length;//请求内容长度 try { //每次上传4k int bufferLength = 4096; byte[] buffer = new byte[bufferLength]; //已上传的字节数 long offset = 0; int size = binaryReader.Read(buffer, 0, bufferLength); Stream postStream = httpReq.GetRequestStream(); //发送请求头部消息 postStream.Write(postHeaderBytes, 0, postHeaderBytes.Length); while (size > 0) { postStream.Write(buffer, 0, size); offset += size; size = binaryReader.Read(buffer, 0, bufferLength); } //添加尾部边界 postStream.Write(boundaryBytes, 0, boundaryBytes.Length); postStream.Close(); //获取服务器端的响应 using (HttpWebResponse response = (HttpWebResponse)httpReq.GetResponse()) { Stream receiveStream = response.GetResponseStream(); StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8); string returnValue = readStream.ReadToEnd(); MessageBox.Show(returnValue); response.Close(); readStream.Close(); } } catch (Exception ex) { Debug.WriteLine("文件传输异常: "+ ex.Message); } finally { fileStream.Close(); binaryReader.Close(); } }
Java 端接收请求
public Map saveCapture(HttpServletRequest request, HttpServletResponse response, Map config) throws Exception { response.setContentType("text/html;charset=UTF-8"); // 读取请求Body byte[] body = readBody(request); // 取得所有Body内容的字符串表示 String textBody = new String(body, "ISO-8859-1"); // 取得上传的文件名称 String fileName = getFileName(textBody); // 取得文件开始与结束位置 String contentType = request.getContentType(); String boundaryText = contentType.substring(contentType.lastIndexOf("=") + 1, contentType.length()); // 取得实际上传文件的气势与结束位置 int pos = textBody.indexOf("filename=""); pos = textBody.indexOf(" ", pos) + 1; pos = textBody.indexOf(" ", pos) + 1; pos = textBody.indexOf(" ", pos) + 1; int boundaryLoc = textBody.indexOf(boundaryText, pos) - 4; int begin = ((textBody.substring(0, pos)).getBytes("ISO-8859-1")).length; int end = ((textBody.substring(0, boundaryLoc)).getBytes("ISO-8859-1")).length; //保存到本地 writeToDir(fileName,body,begin,end); response.getWriter().println("Success!"); return config; } private byte[] readBody(HttpServletRequest request) throws IOException { // 获取请求文本字节长度 int formDataLength = request.getContentLength(); // 取得ServletInputStream输入流对象 DataInputStream dataStream = new DataInputStream(request.getInputStream()); byte body[] = new byte[formDataLength]; int totalBytes = 0; while (totalBytes < formDataLength) { int bytes = dataStream.read(body, totalBytes, formDataLength); totalBytes += bytes; } return body; } private String getFileName(String requestBody) { String fileName = requestBody.substring(requestBody.indexOf("filename="") + 10); fileName = fileName.substring(0, fileName.indexOf(" ")); fileName = fileName.substring(fileName.indexOf(" ") + 1, fileName.indexOf(""")); return fileName; } private void writeToDir(String fileName, byte[] body, int begin, int end) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("d:/" + fileName); fileOutputStream.write(body, begin, (end - begin)); fileOutputStream.flush(); fileOutputStream.close(); }
在用request.getParameter()取值的时候,要注意传过来的数据的MIME类型。
GET 方式提交的话,表单项都保存Header中,格式是http://localhost:8080/form?key1=value1&key2=value2 这样的字符串。server端通过request.getParameter("key1")是可以取到值的。
POST 方式,如果为 enctype application/x-www-form-urlencoded,表单数据都保存在HTTP的数据主体,格式类似于下面这样:用request.getParameter()是可以取到数据的。
但是如果enctype 为 multipart/form-data,就和上面的方式一样,表单数据保存在HTTP的数据主体,各个数据项之间用boundary隔开。用request.getParameter()是取不到数据的,这时需要通过request.getInputStream来操作流取数据,需要自己对取到的流进行解析,才能得到表单项以及上传的文件内容等信息。
这种需求属于比较共通的功能,所以有很多开源的组件可以直接利用。比 如:apache的fileupload 组件,smartupload等。通过这些开源的upload组件提供的API,就可以直接从request中取 得指定的表单项了。
在返回值时,只能返回字节流或者字符流,不能同时获取response.getWriter()、response.getOutputStream()。
参考:
《HTTP 权威指南》
http://www.cnblogs.com/txw1958/archive/2013/01/11/csharp-HttpWebRequest-HttpWebResponse.html
http://my.oschina.net/Barudisshu/blog/150026?fromerr=aaqkzmRK