В последнее время у нас появилась необходимость отдавать файлы Web-сервисом XML, написанном на C#. Принимать данные файлы нужно клиентам, написанным на php, а также Windows-приложению на .NET.
Путём поиска в интернете и нехитрых манипуляций был написан следующий код на C#:
[WebMethod()]
public void GetFile()
{
//отдаём тестовый файл
using(FileStream fs = new FileStream("temp.txt", FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(new byte[] { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}, 0, 16);
fs.Flush();
fs.Close();
}
using(FileStream fs = new FileStream("temp.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
{
sendDataToClient(fs, "temp.txt", 1024, 128);
fs.Close();
}
}
private int sendDataToClient(Stream outStream, string filename, int maxSpeedKbPerSec, int chunkSize)
{
// всего байт отослано
int bytesSend = 0;
// размер "порции"
int _chunkSize = 1024 * chunkSize;
// 300Kb
// время, за которое нужно отдать порцию
int sleep = 1000 * (_chunkSize / 1024) / maxSpeedKbPerSec;
System.Web.HttpContext Context = System.Web.HttpContext.Current;
// увеличим макс время жизни страницы - 100 минут
Context.Server.ScriptTimeout = 6000;
// очищаем все заголовки
Context.Response.Clear();
// раздаём mp3
Context.Response.ContentType = "audio/mpeg";
Context.Response.AddHeader("Content-Disposition", "attachment; filename=" + System.Web.HttpUtility.UrlEncode(Context.Request.ContentEncoding.GetBytes(filename)));
try
{
int startAt = 0;
if (Context.Request.Headers["Range"] != null)
{
Context.Response.StatusCode = 206;
Context.Response.StatusDescription = "Partial Content";
startAt = int.Parse(Context.Request.Headers["Range"].Replace("bytes=", "").Replace("-", ""), System.Globalization.NumberStyles.AllowLeadingWhite | System.Globalization.NumberStyles.AllowTrailingWhite);
}
// Всего нужно будет отдать клиенту
long _dataToWrite = outStream.Length;
if (startAt > 0)
{
Context.Response.AddHeader("Content-Range", string.Format("bytes{0}-{1}/{2}", startAt, _dataToWrite - 1, _dataToWrite));
// Скорректируем размер выводимого потока
_dataToWrite -= startAt;
}
else
{
startAt = 0;
}
// Установим размер контента, отдаваемого клиенту
Context.Response.AddHeader("Content-Length", _dataToWrite.ToString());
// На всякий случай установим Entity Tag, идентифицирующий ресурс
Context.Response.AddHeader("ETag", (filename + outStream.Length).GetHashCode().ToString("x"));
// Установим указатель на байт, начиная с которого будем читать
outStream.Position = startAt;
// Буфер для вывода данных по частям
byte[] buffer = new Byte[_chunkSize];
// Текущая длина
int length;
while (_dataToWrite > 0 && Context.Response.IsClientConnected)
{
DateTime from = DateTime.Now;
// Считаем данные в буфер и получим размер считанного блока
length = outStream.Read(buffer, 0, _chunkSize);
// Выведем данные в поток вывода
Context.Response.OutputStream.Write(buffer, 0, length);
// Скорректируем размер, который осталось вывести
_dataToWrite -= length;
// Скинем данные в поток вывода
Context.Response.Flush();
// если клиент отключился, то останавливаем отдачу данных
if (!Context.Response.IsClientConnected)
break;
// вычисляем сколько нам осталось "спать"
int tosleep = sleep - (int)DateTime.Now.Subtract(from).TotalMilliseconds;
// если нужно, то "засыпаем"
if (tosleep > 0)
Thread.Sleep(tosleep);
//учитываем отданыне данные
bytesSend += length;
}
}
catch
(Exception ex)
{
// вай-вай-вай
Context.Response.StatusCode = 500;
Context.Response.StatusDescription = ex.Message;
}
finally
{
Context.Response.End();
}
return bytesSend;
}
Через браузер работает замечательно, открывается окно сохранения файла, всё прекрасно.
На php в свою очередь был написан следующий клиент:
#!/usr/local/bin/php
<?
define('APP_SOAP_WSDL', 'http://xxxxx/MediaService/MediaService.asmx?WSDL');
define('APP_SOAP_XSI','http://www.w3.org/2001/XMLSchema-instance');
define('APP_SOAP_XSD', 'http://www.w3.org/2001/XMLSchema');
define('APP_SOAP_NAMESPACE', 'http://xxxxx/Services/MediaService/');
define('APP_SOAP_VER', SOAP_1_2);
ini_set("soap.wsdl_cache_enabled", "0");
$client = new SoapClient(APP_SOAP_WSDL, array('trace' => true,
'exceptions' => 0,
'location' => 'http://xxxxx/MediaService/MediaService.asmx',
'xsi' => APP_SOAP_XSI,
'xsd' => APP_SOAP_XSD,
'soap' => APP_SOAP_NAMESPACE,
'style' => SOAP_DOCUMENT,
'use' => SOAP_LITERAL,
'soap_version' => APP_SOAP_VER
));
$parametrs = array('parameters' => array());
$result = $client->__SoapCall('GetFile', $parametrs);
$str = $client->__getLastResponse();
$fp = fopen('test.txt', 'w');
fputs($fp, $str);
exit;
?>
Файл превосходно считывается, сохраняется на диск.
Вопрос заключается в том, что фактически работая с сервисом по протоколу SOAP на момент передачи файла мы опускаемся на уровень протокола HTTP.
С коллегами возник спор, насколько это правильно. С одной стороны, код, приведённый выше, работает быстро и просто (во всяком случае тестовый 16-байтный файл передаётся). С другой стороны, работая с веб-сервисом через SOAP правильнее с точки зрения идеологии подготавливать XML набор данных из таблицы в 1 бинарное поле и записывать туда содержимое в base64 или ещё как-нибудь.
Будем рады, если вы нас рассудите или предложите какой-то иной способ отдачи файла веб-сервисом.