Отправка файлов через сокет poco stream

Первое, что я должен сказать, это то, что этот пост сильно вдохновлен ответом [Remy Lebeau][1]https://stackoverflow.com/users/65863/remy-lebeau на вопрос здесь:

Отправить двоичный файл через соединение TCP/IP.

Я использую библиотеки poco и C++.

Заголовок сообщения

Цель состоит в том, чтобы отправить какой-то большой двоичный файл через Интернет. Поэтому для этого я использую следующий заголовок сообщения:

class FileDesc
{
  size_t file_size = 0;
  size_t chunk_size = 0;
  size_t file_name_len = 0;
  char file_name[0];

public:

  FileDesc(size_t file_sz, size_t buf_sz) 
    : file_size(ByteOrder::toNetwork(file_sz)), 
      chunk_size(ByteOrder::toNetwork(buf_sz)) {}

  size_t get_file_size() const { return ByteOrder::fromNetwork(file_size); }

  size_t get_chunk_size() const { return ByteOrder::fromNetwork(chunk_size); }

  string get_file_name() const 
  {
    const auto len = get_file_name_len();
    string ret; ret.reserve(len + 1);
    for (size_t i = 0; i < len; ++i)
      ret.push_back(file_name[i]);
    return ret;
  }

  size_t get_file_name_len() const 
  {
    return ByteOrder::fromNetwork(file_name_len); 
  }

  size_t size() const 
  {
    return sizeof(*this) + ByteOrder::fromNetwork(file_name_len);
  }

  static shared_ptr<FileDesc> allocate(size_t file_sz, size_t buf_sz, 
                       const string & name)
  {
    const auto n = name.size();
    const size_t sz = sizeof(FileDesc) + n;
    shared_ptr<FileDesc> ret((FileDesc*) malloc(sz), free);
    new (ret.get()) FileDesc(file_sz, buf_sz);
    memcpy(ret->file_name, name.data(), n);
    ret->file_name_len = ByteOrder::toNetwork(n);
    return ret;
  }
};

Файл будет отправляться кусками фиксированной длины, за исключением, возможно, последнего. Этот заголовок отправляется в начале передачи. Часть-отправитель уведомляет часть-получатель о том, что будет отправлен файл размером file_size байт фрагментами размером chunk_size и что имя файла состоит из file_name_len символов. В конце этого сообщения прокручивается имя файла через массив символов file_name, длина которого является переменной и зависит от длины имени файла. Статический метод allocate отвечает за выделение этому заголовку достаточного размера для хранения полного имени файла.

Процедура отправителя

Вот код для части отправителя:

bool send_file(StreamSocket & sock, const string & file_name, long buf_size)
{
  stringstream s;

  ifstream file(file_name, ios::binary | ios::ate);
  if (not file.is_open() or not file.good()) // verify file reading
    {
      s << "cannot open file " << file_name;
      throw domain_error(s.str());
    }
  long file_size = file.tellg();
  if (file_size == 0)
    {
      s << "file " << file_name << " has zero bytes";
      throw domain_error(s.str());
    }
  file.seekg(0);

  unique_ptr<char[]> chunk(new char[buf_size]);

  auto desc_ptr = FileDesc::allocate(file_size, buf_size, file_name);

    // send the file description
  int status = sock.sendBytes(desc_ptr.get(), desc_ptr->size());
  if (status != desc_ptr->size())
    return false;

  do // now send the file
    {
      long num_bytes = min(file_size, buf_size);
      file.read(chunk.get(), num_bytes);

      char * pbuf = chunk.get();
      auto n = num_bytes;
      while (n > 0)
      {
        status = sock.sendBytes(pbuf, n);
        pbuf += status;
        n -= status;
      }

      file_size -= num_bytes;
    } 
  while (file_size > 0);

  char ok;  // now I wait for receiving part notifies reception
  status = sock.receiveBytes(&ok, sizeof(ok)); 

  return ok == 0;
}

send_file(sock, file_name, buf_size) открывает файл file_name и отправляет его содержимое через poco-сокет sock кусками размером buf_len. Таким образом, он выполнит file_size/buf_len передач плюс начальную передачу файлового дескриптора.

Приемная часть

Сторона получателя создает экземпляр poco ServerSocket следующим образом:

  ServerSocket server_socket(addr);
  auto sock = server_socket.acceptConnection();
  auto p = receive_file(sock, 256);

receive_file(sock, 256) означает, что на сокете sock будет получен файл, максимальная длина которого составляет 256 байт (достаточно для моего приложения). Эта подпрограмма, являющаяся аналогом send_file(), сохраняет файл и реализована следующим образом:

pair<bool, string> 
receive_file(StreamSocket & sock, size_t max_file_name_len)
{
  stringstream s;
  size_t buf_sz = sizeof(FileDesc) + max_file_name_len;
  unique_ptr<char[]> buffer(new char[buf_sz]);
  int num = sock.receiveBytes(buffer.get(), buf_sz);
  FileDesc * msg = reinterpret_cast<FileDesc*>(buffer.get());
  if (msg->size() > num)
    return make_pair(false, "");

  long chunk_size = msg->get_chunk_size();
  if (chunk_size == 0)
    throw domain_error("chunk size is zero");

  const string file_name = msg->get_file_name();

  ofstream file(file_name, ios::binary);
  if (not file.is_open() or not file.good())
    {
      s << "cannot create file" << file_name;
      throw domain_error(s.str());
    }

  long file_size = msg->get_file_size();
  unique_ptr<char[]> chunk(new char[chunk_size]);

  do
    {   // num_bytes is the quantity that I must receive for this chunk
      auto num_bytes = min(file_size, chunk_size); 
      char * pbuf = chunk.get();
      long n = num_bytes; 
      while (n > 0)
       {
         num = sock.receiveBytes(pbuf, n, 0); // <-- here is potentially the problem
         if (num == 0) // connection closed?
           return make_pair(file_size == 0, file_name);

         pbuf += num;
         n -= num;
       }

      file.write(chunk.get(), num_bytes);
      file_size -= num_bytes;
    }
  while (file_size > 0);

  char ok;
  num = sock.sendBytes(&ok, sizeof(ok)); // notify reception to sender

  return make_pair(true, file_name);
}

receive_file() возвращает pair<bool,string>, где first указывает, что прием был выполнен правильно, а second имя файла.

Эта проблема

По причине, которую я не могу понять, иногда, когда num = sock.receiveBytes(pbuf, n, 0) получает меньше байтов, чем ожидаемое n, и цикл выполняет чтение остатка, только последний прием, соответствующий последним блокам отправки.

Любопытно, что до сих пор моя проблема возникала только тогда, когда я использую петлю (127.0.0.1), что довольно удобно для тестирования, потому что я тестирую все на своей машине. Наоборот, когда передача идет между двумя разными пирами, все работает нормально, что, конечно, не говорит о том, что все правильно.

Итак, я был бы очень признателен, если бы кто-нибудь помог мне покритиковать мою реализацию и в конечном итоге указать, в чем проблема.


person lrleon    schedule 13.10.2015    source источник


Ответы (1)


Если вы не хотите, чтобы ввод-вывод блокировался, вы должны установить для сокета неблокирующий режим.

Но похоже, что простая вещь слишком сложна; вот быстрый пример:

$ fallocate -l 1G test.in
$ ls -al test.in
-rw-r--r-- 1 alex alex 1073741824 Oct 15 23:31 test.in
$ ls -al test.out
ls: cannot access test.out: No such file or directory
$./download
$ ls -al test.out 
-rw-rw-r-- 1 alex alex 1073741824 Oct 15 23:32 test.out
$ diff test.out test.in 
$

Итак, у нас все хорошо - файлы 1G и идентичны (хотя и скучно).

Код клиента:

#include "Poco/StreamCopier.h"
#include "Poco/FileStream.h"
#include "Poco/Net/SocketAddress.h"
#include "Poco/Net/ServerSocket.h"
#include "Poco/Net/StreamSocket.h"
#include "Poco/Net/SocketStream.h"

using namespace Poco;
using namespace Poco::Net;

int main()
{
        StreamSocket ss;
        ss.connect(SocketAddress("localhost", 9999));
        SocketStream istr(ss);

        std::string file("test.out");
        Poco::FileOutputStream ostr(file, std::ios::binary);
        StreamCopier::copyStream(istr, ostr);

        return 0;
}

А вот сервер:

#include "Poco/Net/SocketAddress.h"
#include "Poco/Net/Socket.h"
#include "Poco/Net/StreamSocket.h"
#include "Poco/Net/ServerSocket.h"
#include "Poco/Net/SocketStream.h"
#include "Poco/StreamCopier.h"
#include "Poco/Timespan.h"
#include "Poco/FileStream.h"

using namespace Poco;
using namespace Poco::Net;

int main()
{
        ServerSocket srvs(SocketAddress("localhost", 9999));
        Timespan span(250000);
        while (true)
        {
                if (srvs.poll(span, Socket::SELECT_READ))
                {
                        StreamSocket strs = srvs.acceptConnection();
                        SocketStream ostr(strs);
                        std::string file("test.in");
                        Poco::FileInputStream istr(file, std::ios::binary);
                        StreamCopier::copyStream(istr, ostr);
                }
        }
        return 0;
}

Наслаждайтесь POCO!

person Alex    schedule 16.10.2015
comment
OMG, очень впечатляюще и лаконично! Я начинаю думать, что poco действительно очень хорошая библиотека. Я немного изменил ваш фрагмент; в основном я поставил FileInputStream в клиентской части и FileOutputStream в серверной части. Может я чего-то не понимаю, но работает нормально. Пожалуйста, поправьте меня, если я ошибаюсь. В любом случае, я отмечу ваш ответ как принятый и большое спасибо! - person lrleon; 16.10.2015