众所周知,如果需要向WEB服务器上传文件,一般选用下列2种方式。
1. 使用HTTP PUT指令
2. 模拟页面的form提交
第一种需要配置服务器,略过。
第二种需要使用WinInet根据HTTP协议,拼除POST BODY后提交。
对于第二种,在ASP.Net里面特麻烦。
1. 需要模拟页面的VIEWSTATE,模拟不成功就不行
2. ASP.Net对每个请求有最大长度限制,这个值默认为4MB,但可以在web.config中修改
3. 文件在上传过程中并没有直接写入磁盘,而是先放入了内存,等到全部上传结束再写入磁盘。所以如果传输超大的文件对服务器性能影响很大
本文的做法是:
客户端不需要模拟form,将大文件分成等大的小块(如64K),使用多线程将这些小块上传到服务器后,服务器再拼合起来。
---------------------------------------------------------
流程:
1. 客户端:需要向服务器上传一个文件,首先调用服务器的某一个页面(如BeginUpload.aspx),通知此文件的大小(bytes)
2.服务器:服务器收到此请求,首先验证客户端权限,然后在自定义的文件夹中按照请求中提供的大小创建一个空文件,并返回一个唯一标示码到客户端。
3.客户端:收到服务器返回成功后,记录下此次上传的唯一标识码。
4.客户端:将需要上传的这个文件分成大小相等的文件块(如64K)。(这个过程只是一个逻辑上的过程,实际的做法并不需要分块,可以直接使用内存映射文件或者将文件直接读入到虚拟内存以加快速度)
5.客户端:开启一个领导者-跟随者线程池。领导者线程负责要上传文件块的调度,而跟随者线程负责自己分配到的文件块上传。
6.客户端,跟随者线程:读取自己分配到的文件块,向服务器的特定路径或者页面POST文件内容。
这个POST的HEADER或者QueryString里面起码要包含这几个参数:唯一标识码、当前文件块的区间。
7.服务端:收到跟随者线程的请求,以共享方式打开临时文件夹中的文件,写入当前文件块。
8.客户端,领导者线程:检测到文件块全部上传完毕,则向服务器某一个页面报告(如EndUpload.aspx),此文件上传结束,清理资源。
9.服务器:收到文件上传结束的通知,将文件从临时文件夹移动到需要的位置。
10. 服务器周期性地清理临时文件夹中的过期文件。
--------------------------------------------------------------------
上面的流程是多线程分块并行上传的基本流程。也可以在此基础上进一步加入CRC32验证文件完整性的功能。如果要简化流程,不需要分块上传,只需要直接进行第6步操作就可以了。
对于第6部,在服务端,可以使用一个*.aspx页面或者一个IHttpHandler来处理请求。
参考下列代码,参数以及其它部分都已经略掉。
1. protected void Page_Load(object sender, EventArgs e)
2. {
3. Stream stream = Page.Request.InputStream;
4. byte[] buffer = new byte[stream.Length];
5. stream.Read(buffer, 0, buffer.Length);
6. // TO DO: Something else
7. }
对于其它B/S平台,也是类似的方式。
客户端代码,我自己封装了一下,这里只列出关键代码。
- void CHttpClient::OpenConnection( LPCTSTR lpszServer, UINT nPort)
- {
- CloseConnection();
- m_hSession = ::InternetOpen( USER_AGENT
- , INTERNET_OPEN_TYPE_PRECONFIG
- , NULL
- , NULL
- , 0
- );
- if( !m_hSession )
- throw CCustomException(_T("Error: Failed to connect to the server, InternetOpen failed."));
- m_hConnect = ::InternetConnect( m_hSession
- , lpszServer
- , nPort
- , NULL
- , NULL
- , INTERNET_SERVICE_HTTP
- , NULL
- , NULL
- );
- if( !m_hConnect )
- throw CCustomException(_T("Error: Failed to connect to the server, InternetConnect failed."));
- }
- void CHttpClient::CloseConnection(void)
- {
- if( m_hSession )
- {
- ::InternetCloseHandle(m_hSession);
- m_hSession = NULL;
- }
- if( m_hConnect )
- {
- ::InternetCloseHandle(m_hConnect);
- m_hConnect = NULL;
- }
- }
- void CHttpClient::PostBuffer( LPCTSTR lpszPath, LPBYTE lpBuffer, DWORD dwSize)
- {
- ASSERT( m_hSession && m_hConnect );
- HINTERNET hRequest = ::HttpOpenRequest( m_hConnect
- , _T("POST")
- , lpszPath
- , NULL
- , NULL
- , NULL
- , INTERNET_FLAG_NO_CACHE_WRITE
- , 0
- );
- if(!hRequest)
- throw CCustomException(_T("Error: Failed to POST data to server, HttpOpenRequest failed."));
- INTERNET_BUFFERS stBuffers = {0};
- stBuffers.dwStructSize = sizeof(stBuffers);
- stBuffers.Next = NULL;
- stBuffers.lpcszHeader = NULL;
- stBuffers.dwHeadersLength = 0;
- stBuffers.dwHeadersTotal = 0;
- stBuffers.lpvBuffer = NULL;
- stBuffers.dwBufferLength = 0;
- stBuffers.dwBufferTotal = dwSize;
- stBuffers.dwOffsetLow = 0;
- stBuffers.dwOffsetHigh = 0;
- BOOL bRet = ::HttpSendRequestEx( hRequest, &stBuffers, NULL, 0, 0);
- if(!bRet)
- {
- ::InternetCloseHandle(hRequest);
- throw CCustomException(_T("Error: Failed to POST data to server, HttpSendRequestEx failed."));
- }
- DWORD dwSent = 0;
- DWORD dwBytesWritten = 0;
- while(dwSent < dwSize)
- {
- bRet = ::InternetWriteFile( hRequest
- , (LPBYTE)(lpBuffer + dwSent)
- , dwSize - dwSent
- , &dwBytesWritten
- );
- if( bRet )
- dwSent += dwBytesWritten;
- }
- bRet = ::HttpEndRequest(hRequest, NULL, 0, 0);
- ::InternetCloseHandle(hRequest);
- if( !bRet )
- throw CCustomException(_T("Error: Failed to POST data to server, HttpEndRequest failed."));
- }
- const int BUFFER_SIZE = 1024000;
- CHttpClient oClient;
- oClient.OpenConnection( _T("127.0.0.1"), 4638 );
- BYTE * pBuffer = new BYTE[BUFFER_SIZE];
- for (UINT i = 0; i < BUFFER_SIZE; i++)
- {
- pBuffer[i] = i%0xFF;
- }
- oClient.PostBuffer( _T("/1/Default.aspx"), pBuffer, BUFFER_SIZE);
- delete [] pBuffer;