вторник, 15 июля 2008 г.

Upload картинки из мидлета на сервер

Сначала следует отметить, что у мидлета есть ограничение: он может передать по HTTP файл размером не больше чем в 2Кб. Так что будем передавать файл кусочками, а потом на сервере склеивать. Кроме того, картинку следует передавать не абы как, а в кодировке base64, иначе, опять же, будут возникать непонятные проблемы передачи (по крайней мере, у меня возникали).

Клиент

Пусть мы каким-то образом получили файл как byte[]. Напишем специальный класс Uploader, с помощью которого будем заливать картинку на сервер. Передавать файл будем фрагментами по 1 Кб. Для каждого фрагмента будем открывать отдельный коннекшен и составлять POST-запрос, содержащий следующие данные:
  • Номер фрагмента (number)
  • Общее количество фрагментов (count)
  • Собственно содержание (content) в виде вложенного файла
Добавим в наш мидлет следующие поля:
/**
* Размер передаваемого фрагмента картинки
*/
private static int CHUNK_SIZE = 1024;

/**
* Граница между разделами в HTTP-запросе
*/
private static String BOUNDARY = "42hjkiqp4279h";

/**
* Адрес, на который мы посылаем запросы
*/
private static String HOST = "http://some_url/Handler.ashx";

/**
* Сообщение, которое мы получаем в случае успешной отправки запроса
*/
private static String UPLOAD_OK_MSG = "Upload OK";
И следующие методы:
/**
* Залитие картинки на сервер
* @param src Массив символов, содержащий картинку
* @return Ответ от сервера: удалось залить, или нет
*/
public String Upload(byte[] src)
{
  String response = "";
  try
  {
    int nchunks = src.length / CHUNK_SIZE;
    System.out.println("size = " + src.length + ", chunks count = " + nchunks);

    String fileId = GetNextPhotoName(5);

    for (int i = 0; i < nchunks; ++i)
    {
      String content = Base64.encode(src, i * CHUNK_SIZE, CHUNK_SIZE);
      response = UploadChunk(fileId, i + 1, nchunks, content);
      if (!IsUploadOk(response))
      {
        break;
      }
    }
    System.out.println("Uploading file " + fileId + ": " + response);
  }
  catch (Exception ex)
  {
    System.out.println(ex.getClass().getName() + " while uploading file: " + ex.getMessage());
  }
  finally
  {
  }
  return response;
}

/**
* Формирование раздела HTTP-запроса, содержащего значение некоторого параметра
* @param name Имя параметра
* @param value Значение параметра
* @return Строка - раздел HTTP-запроса
*/
private static String FormData(String name, Object value)
{
  return "--" + BOUNDARY + "\r\n" +
      "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n";
}

/**
* Заливает на сервер фрагмента картинки
* @param fileId Идентификатор картинки
* @param number Номер фрагмента
* @param count Общее количество фрагментов
* @param content Содержаение фрагмента
* @return Ответ от сервера: удалось залить или нет
*/  
private String UploadChunk(String fileId, int number, int count, String content)
{
  String message =
      FormData("number", new Integer(number)) +
      FormData("count", new Integer(count)) +
      FormData("sessionId", sessionId) +
      "--" + BOUNDARY + "\r\n" +
      "Content-Disposition: form-data; name=\"imagefile\"; filename=\"" + fileId + "\"\r\n" +
      "Content-Transfer-Encoding: base64\r\n" +
      "Content-Type: image/png\r\n\r\n" + content + "\r\n" +
      "--" + BOUNDARY + "--\r\n";
  String response = SendPostData(message);

  System.out.println("Chunk " + fileId + "_" + number + " " + response);
  return response;
}

/**
* Отправляет на сервер POST-запрос
* @param message Текст запроса
* @return Ответ сервера
*/
private static String SendPostData(String message)
{
  HttpConnection conn = null;
  InputStream is = null;
  OutputStream os = null;
  String response = null;
  try
  {          
    conn = (HttpConnection) Connector.open(HOST);

    // Задаем параметры запроса
    conn.setRequestMethod(HttpConnection.POST);
    conn.setRequestProperty("Connection", "Keep-Alive");
    conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);

    byte[] messageBody = message.getBytes();          
    conn.setRequestProperty("Content-Length", Integer.toString(messageBody.length));

    // Посылаем запрос
    os = conn.openOutputStream();
    os.write(messageBody);

    int rc = conn.getResponseCode();          

    if (rc != HttpConnection.HTTP_OK)
    {
      throw new IOException("HTTP responce code: " + rc);
    }

    // Получаем ответ
    is = conn.openInputStream();
    int ch;
    StringBuffer data = new StringBuffer();
    while ((ch = is.read()) != -1)          
    {
      data.append((char) ch);
    }
    response = data.toString();
  }
  catch (Exception e)
  {
    response = "Error while connecting:\n\n" + e.getMessage();
  }
  finally
  {
    try
    {
      if (is != null)
      {
        is.close();
      }              
      if (os != null)
      {
        os.close();
      }
      if (conn != null)
      {
        conn.close();
      }
    }
    catch (Exception e)
    {
    }
  }
  return response;
}

/**
* Проверка, успешно ли произошел Upload на сервер
* @param response Ответ сервера
* @return
*/
private boolean IsUploadOk(String response)
{
  return response.equals(UPLOAD_OK_MSG);
}

Сервер

Сервер, как и раньше, написан на .NET. Есть хэндлер Handler.ashx, и в нем написано следующее:
namespace MobileServer
{
  [WebService(Namespace = "http://tempuri.org/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  public class Handler : IHttpHandler
  {
    private const string UPLOAD_PATH = "...";
    
    /// <summary>
    /// Получение пути ко временному файлу с фрагментом картинки
    /// </summary>
    private static string GetChunkFileName(string fileName, int number)
    {
    return String.Format("{0}\{1}_{2}", UPLOAD_PATH, f.FileName, number);
    }

    /// <summary>
    /// Получение пути к файлу с картинкой
    /// </summary>
    private static string GetFileName(string fileName)
    {
    return String.Format("{0}\{1}.png", UPLOAD_PATH, f.FileName);
    }
    
    /// <summary>
    /// Сохранение фрагмента картинки в файл
    /// </summary>
    /// <param name="number">Номер фрагмента
    /// <param name="f">Файл, полученный по HTTP
    private static void SaveChunkToFile(int number, HttpPostedFile f)
    {
      byte[] buf = new byte[f.ContentLength];
      f.InputStream.Read(buf, 0, f.ContentLength);
      String content = String.Empty;

      for (int i = 0; i < buf.Length; ++i)
      {
        if (buf[i] == 0)
          break;

        content += ((char)buf[i]).ToString();
      }

      byte[] rez = Convert.FromBase64String(content);

      string fileName = GetChunkFileName(f.FileName, number);

      FileStream fs = new FileStream(fileName, FileMode.CreateNew);
      BinaryWriter bw = new BinaryWriter(fs);
      bw.Write(rez, 0, rez.Length);
      bw.Flush();
      bw.Close();
      fs.Close();
    }
    
    /// <summary>
    /// Проверка, все ли фрагменты данной картинки залились
    /// </summary>
    /// <param name="fileName">Имя файла с картинкой
    /// <param name="count">Общее количество фрагментов
    private static bool AreAllChunksUploaded(string fileName, int count)
    {
      for (int i = 1; i <= count; ++i)
      {
        if (!File.Exists(GetChunkFileName(fileName, i)))
        {
          return false;
        }
      }
      return true;
    }

    /// <summary>
    /// Объединение всех фрагментов файла, и сохранение в отдельный файл
    /// </summary>
    /// <param name="fileName">Название файла
    private static void MergeChunks(string fileName)
    {
      for (int i = 1; i <= imageInfo.ChunksCount; ++i)
      {
        FileStream fs = new FileStream(GetChunkFileName(fileName, i), FileMode.Open);
        BinaryReader br = new BinaryReader(fs);
        byte[] buf = br.ReadBytes(2000);
        br.Close();
        fs.Close();

        fs = new FileStream(GetFileName(fileName), FileMode.Append);
        BinaryWriter bw = new BinaryWriter(fs);
        bw.Write(buf);
        bw.Close();
        fs.Close();
      }
    }

    #region IHttpHandler members
    public void ProcessRequest(HttpContext context)
    {
      try
      {
        int number = Convert.ToInt32(context.Request["number"]);
        int count = Convert.ToInt32(context.Request["count"]);
        string sessionId = context.Request["sessionId"];

        HttpPostedFile f = (HttpPostedFile)context.Request.Files[0];

        SaveChunkToFile(number, f);

        if (AreAllChunksUploaded(f.FileName, count))
        {
          MergeChunks(f.FileName);
          //TODO: код для удаления временных файлов
        }
        context.Response.Write("Upload OK");
      }
      catch (Exception e)
      {
        context.Response.Write("Upload failed:\n" + e.Message);
      }
    }

    public bool IsReusable
    {
      get
      {
        return false;
      }
    }
    #endregion
  }
}
В общем, как-то так.

3 комментария:

Андрей комментирует...

Дарья, у мидлета нет ограничения на передачу данных. Это ограничение скорее самого сервера или прокси.

darja комментирует...

Хмм. Я читала, что это ограничение неподписанных мидлетов. Подписать мидлет мне так и не удалось, так что точно проверить не могу. Но фотографии больше 2 кб точно не передавались. Даже с эмулятора.

Анонимный комментирует...

Дело в том, что в J2ME реализация HTTP протокола такова, что размер буфера отправки данных ограничен 2 kb (в новых моделях это ограничение убрали). В случае если мидлет пытается послать более 2 kb, то J2ME API автоматически переключается в режим отправки порциями (Transfer-Encoding: chunked) и если сервер умеет такое принимать то всё нормально передастся, иначе ругнётся что-то типа отсутствует поле Content Length или ещё что-нибудь. Кстати к примеру ngnix не умеет принимать порциями.