在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 + "\r\nContent-Disposition: form-data; name=\"{0}\";filename=\"{1}\"\r\nContent-Type:application/octet-stream\r\n\r\n";
string header = string.Format(dataFormat, "file", Path.GetFileName(filePath));
byte[] postHeaderBytes = Encoding.UTF8.GetBytes(header);
//結束邊界
byte[] boundaryBytes = Encoding.ASCII.GetBytes("\r\n--" + timeStamp + "--\r\n");
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("\n", pos) + 1;
pos = textBody.indexOf("\n", pos) + 1;
pos = textBody.indexOf("\n", 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("\n"));
fileName = fileName.substring(fileName.indexOf("\n") + 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

