Reputation: 2648
The first thing that I must say is that this post is strongly inspired in the answer of [Remy Lebeau][1]https://stackoverflow.com/users/65863/remy-lebeau at the question here:
Send binary file over TCP/IP connection.
I'm using poco libraries and C++.
The goal is to send a some big binary file through internet. So in order to accomplish that I use the following message header:
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;
}
};
The file will be sent by chunks of fixed length, except probably the last. This header is sent at the beginning of transmission. The sender part notifies to the receiver part that an file of file_size
bytes will be sent, by chunks of size chunk_size
and that the name of file has file_name_len
characters. At the end of this message is paddled the file name, through the char array file_name
, whose length is variable and depends on the file name length. The static method allocate
is responsible for allocating this header with enough size for storing the full file name.
Here is the code for the sender part:
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)
open the file file_name
and sends its content through the poco socket sock
by chunks of size buf_len
. So it will perform file_size/buf_len
transmissions plus the initial transmission of the file descriptor.
The receiver side instantiates a poco ServerSocket
as follows:
ServerSocket server_socket(addr);
auto sock = server_socket.acceptConnection();
auto p = receive_file(sock, 256);
receive_file(sock, 256)
expresses that at the socket sock
it will be received a file whose maximum file length is 256 bytes (enough for my app). This routine, which is the counter part of send_file()
, saves the file and it is implemented as follows:
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()
returns a pair<bool,string>
where first
indicates i the reception was correctly done and second
the file name.
For a reason that I do not manage to understand, sometimes, when num = sock.receiveBytes(pbuf, n, 0)
receives less bytes than the expected n
and the loop accomplishes to read the remainder, just the last reception corresponding to the last send blocks.
Curiously, until now, my problem only happens when I use the loopback (127.0.0.1), which is pretty practical for testing because I test all on my machine. At the contrary, when the transmission is between two different peers, all works fine, what of course does not indicate that all is correct.
So, I would be very grateful if someone could help me criticizing my implementation and eventually indicating where is the problem
Upvotes: 0
Views: 4972
Reputation: 5330
If you don't want I/O to block, you should set socket to non-blocking mode.
But, it looks like a simple thing is overly complicated; here's a quick example:
$ 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
$
So, we're good - files are 1G and identical (albeit boring).
Client code:
#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;
}
And here's the server:
#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;
}
Enjoy POCO!
Upvotes: 6