我想发送两个文件给客户端,第一个文件是img.jpg,第二个文件是message.txt
第一个文件 img.jpg 被正确接收,但文件 message.txt 被接收,大小为零
客户端输出是: 图片.jpg: 49152 消息.txt:4096
read: End of file [asio.misc:2]
这是我的客户端和服务器代码:
服务器.cpp:
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <boost/asio.hpp>
int main(int argc, char* argv[])
{
try
{
boost::asio::io_context io;
std::cout << "Server Start\n";
boost::asio::ip::tcp::acceptor acc(io,
boost::asio::ip::tcp::endpoint(
boost::asio::ip::tcp::v4(), 6666));
for (;;) {
boost::asio::ip::tcp::socket sock(io);
acc.accept(sock);
std::vector<std::string> names{ "img.jpg" , "message.txt"};
std::vector<int> sizes{ 49152 , 4096 };
for (int i = 0; i < 2; ++i) {
//Send Header
boost::asio::streambuf reply;
std::ostream header(&reply);
header << names[i] << " ";
header << std::to_string(sizes[i]) << " ";
header << "\r\n";
boost::asio::write(sock, reply);
//Send Bytes
std::ifstream input(names[i], std::ifstream::binary);
std::vector<char> vec(sizes[i]);
input.read(&vec[i], sizes[i]);
boost::asio::write(sock, boost::asio::buffer(vec, sizes[i]));
}
sock.close();
}
acc.close();
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
客户端.cpp:
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <boost/asio.hpp>
int main(int argc, char* argv[])
{
try
{
boost::asio::io_context io;
boost::asio::ip::tcp::resolver resolv(io);
boost::asio::ip::tcp::resolver::query q("127.0.0.1", "6666");
boost::asio::ip::tcp::resolver::iterator ep = resolv.resolve(q);
boost::asio::ip::tcp::socket sock(io);
boost::asio::connect(sock, ep);
//Get Files
for (int i = 0; i < 2; ++i) {
//Read Header
boost::asio::streambuf reply;
boost::asio::read_until(sock, reply, "\r\n");
std::istream header(&reply);
std::string fileName;
int fileSize;
header >> fileName;
header >> fileSize;
std::cout << fileName << ": " << fileSize << '\n';
//Read File Data
std::ofstream output(fileName, std::ofstream::binary | std::ofstream::app);
std::vector<char> vec(fileSize);
boost::asio::read(sock, boost::asio::buffer(vec, vec.size()));
output.write(&vec[0], vec.size());
output.close();
}
sock.close();
}
catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
有很多问题。
&vec[i]
是服务器端的一个错误:
std::vector<char> vec(sizes[i]);
input.read(&vec[i], sizes[i]);
如果
i>0
(除了第一次运行之外总是如此),您将解决 vec
出界,因为尺寸不是 sizes[i]+i
你的标题格式很草率:带有空格的文件名会导致UB
os << to_string(n)
应该只是os << n
服务器使用客户端...完全忽略的分隔符
"\r\n"
。所有文件至少以未被消耗的 \r\n
开头,更糟糕的是,文件的最后两个字符将被读取为下一个文件头的一部分。这充其量会失败,但可能会导致 UB
事实上,它总是会导致UB,因为客户端完全缺乏错误处理
我现在注意到,您几乎通过对标题(
streambuf header;
)和内容(直接进入vec
)使用单独的缓冲区来解决这个问题。然而,read_until
记录了它可能会读到超过分隔符的情况。因此,您应该写入 streambuf
中的所有剩余数据,并从仍要读取的数量中减去长度。
简而言之,建议每个流使用单独的、精确大小的缓冲区 OR 一个
DynamicBuffer
(如 streambuf
)。
同样的问题是客户端在循环的每次迭代中使用新的缓冲区:
for (int i = 0; i < 2; ++i) {
// Read Header
asio::streambuf reply;
它至少应该在循环之外,以便接收到的任何多余数据都将在下一次迭代中正确使用
您通常希望处理读取的部分成功(即接受与 EOF 条件一起接收的数据)。这里它不应该影响正确性,因为你精确地限制了正文读取到预期的大小,但这仍然是一个好习惯
您在
asio::buffer(vec, vec.size())
中指定了冗余缓冲区大小,这只会引发错误。让它们远离以获得相同的行为,而不会有获得错误尺寸的风险:asio::buffer(vec)
(例如,它将避免前面提到的 UB)
具有中途修复的组合服务器/客户端:https://coliru.stacked-crooked.com/a/03bee101ff6e8a7a
客户端增加了很多错误处理
asio::streambuf buf;
for (;;) {
asio::read_until(sock, buf, "\r\n");
std::string name;
int size;
if (std::istream header(&buf); (header >> std::quoted(name) >> size).ignore(1024, '\n')) {
std::cout << name << ": " << size << '\n';
std::vector<char> vec(size);
boost::system::error_code ec;
auto n = read(sock, asio::buffer(vec), ec);
if (n != vec.size()) {
std::cerr << "Read completed: " << ec.message() << std::endl;
std::cerr << "Incomplete data (" << n << " of " << vec.size() << ")" << std::endl;
std::cerr << "Streambuf still had " << buf.size() << " bytes (total: " << (n + buf.size()) << ")" << std::endl;
break;
}
std::ofstream(name, std::ios::binary /*| std::ios::app*/).write(&vec[0], n);
} else {
std::cerr << "Error receiving header, header invalid?" << std::endl;
break;
}
}
这使我们能够通过
streambuf
读数超出分隔符来演示问题:
main.cpp: 2712
main.cpp: 2712
Read completed: End of file
Incomplete data (2217 of 2712)
Streambuf still had 495 bytes (total: 2712)
或者我本地的测试:)
test.gif: 400557
message.txt: 4096
Incomplete data (3604 of 4096)
Streambuf still had 492 (total: 4096)
笨拙/幼稚的修复方法可能看起来像这样:
if (std::istream header(&buf); (header >> std::quoted(name) >> size).ignore(1024, '\n')) {
std::cout << name << ": " << size << '\n';
std::cerr << "Streambuf still had " << buf.size() << " bytes" << std::endl;
size -= buf.size();
std::ofstream ofs(name, std::ios::binary /*| std::ios::app*/);
ofs << &buf;
std::cerr << "Adjusted size to read: " << size << std::endl;
std::vector<char> vec(size);
boost::system::error_code ec;
auto n = read(sock, asio::buffer(vec), ec);
if (n != vec.size()) {
std::cerr << "Read completed: " << ec.message() << std::endl;
std::cerr << "Incomplete data (" << n << " of " << vec.size() << ")" << std::endl;
break;
}
ofs.write(&vec[0], n);
} else {
std::cerr << "Error receiving header, header invalid?" << std::endl;
break;
}
虽然它可能看起来工作正常:
它只会引发小文件的新问题,其中整个后续文件被“意外”用作当前文件的内容。相反,只需 SayWhatYouMean(TM):
if ((std::istream(&buf) >> name >> size).ignore(1024, '\n')) {
std::cout << name << ": " << size << '\n';
read(sock, buf, asio::transfer_exactly(size), ec);
if (buf.size() < size) {
std::cerr << "Incomplete data" << std::endl;
break;
}
std::ofstream(output_dir / name, std::ios::binary /*| std::ios::app*/)
.write(buffer_cast<char const*>(buf.data()), size);
buf.consume(size);
} else {
还从命令行获取文件列表,而不是硬编码文件/大小,并写入输出目录以确保安全:
#include <boost/asio.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <ranges>
namespace asio = boost::asio;
using asio::ip::tcp;
using std::ranges::contains;
using std::filesystem::path;
constexpr uint16_t PORT = 6666;
int main(int argc, char* argv[]) try {
asio::io_context io;
std::vector<std::string_view> const
args(argv + 1, argv + argc),
opts{"--client", "-c", "--server", "-s"};
bool const server = contains(args, "--server") || contains(args, "-s");
bool const client = contains(args, "--client") || contains(args, "-c");
if (server) {
std::cout << "Server Start" << std::endl;
for (tcp::acceptor acc(io, {{}, PORT});;) {
tcp::socket sock = acc.accept();
for (path name : args) {
if (contains(opts, name))
continue;
auto size = file_size(name);
// Send Header
asio::streambuf buf;
std::ostream(&buf) << name << " " << size << "\r\n";
write(sock, buf);
// Send bytes
std::vector<char> vec(size);
std::ifstream(name, std::ios::binary).read(vec.data(), size);
write(sock, asio::buffer(vec));
}
}
}
if (client) {
path output_dir = "./output/";
create_directories(output_dir);
tcp::socket sock(io);
// connect(sock, tcp::resolver(io).resolve("127.0.0.1", std::to_string(PORT)));
sock.connect({{}, PORT});
asio::streambuf buf;
for (boost::system::error_code ec;;) {
read_until(sock, buf, "\r\n", ec);
path name;
size_t size;
if ((std::istream(&buf) >> name >> size).ignore(1024, '\n')) {
std::cout << name << ": " << size << '\n';
read(sock, buf, asio::transfer_exactly(size), ec);
if (buf.size() < size) {
std::cerr << "Incomplete data" << std::endl;
break;
}
std::ofstream(output_dir / name, std::ios::binary /*| std::ios::app*/)
.write(buffer_cast<char const*>(buf.data()), size);
buf.consume(size);
} else {
std::cerr << "Error receiving header, header invalid?" << std::endl;
break;
}
}
}
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
return 1;
}
通过现场测试
for a in {1..10}; do dd if=/dev/urandom bs=1 count=10 of=small-$a.txt; done 2>/dev/null
g++ -std=c++2b -O2 -Wall -pedantic -pthread main.cpp
./a.out --server *.* &
sleep 1; ./a.out --client
kill %1
md5sum {.,output}/*.* | sort
印刷:
Server Start
"a.out": 187496
"main.cpp": 2546
"small-1.txt": 10
"small-10.txt": 10
"small-2.txt": 10
"small-3.txt": 10
"small-4.txt": 10
"small-5.txt": 10
"small-6.txt": 10
"small-7.txt": 10
"small-8.txt": 10
"small-9.txt": 10
Error receiving header, header invalid?
024c40ee2e93ee2e6338567336094ba2 ./small-8.txt
024c40ee2e93ee2e6338567336094ba2 output/small-8.txt
164f873a00178eca1354b1a4a398bf0f ./small-10.txt
164f873a00178eca1354b1a4a398bf0f output/small-10.txt
2ff416d02ca7ea8db2b5cb489a63852d ./small-6.txt
2ff416d02ca7ea8db2b5cb489a63852d output/small-6.txt
4559b8844afe7d5090948e97a8cef8d8 ./small-7.txt
4559b8844afe7d5090948e97a8cef8d8 output/small-7.txt
6fd6eac47427bfda3fc5456afed99602 ./small-4.txt
6fd6eac47427bfda3fc5456afed99602 output/small-4.txt
76fa51d5d6f06b9c8483f5539cd5611b ./a.out
76fa51d5d6f06b9c8483f5539cd5611b output/a.out
8a114a62f0ad5e087d7b338eeebcadf1 ./small-1.txt
8a114a62f0ad5e087d7b338eeebcadf1 output/small-1.txt
b4f11b6ed8870d431c5ec579d12991c0 ./small-5.txt
b4f11b6ed8870d431c5ec579d12991c0 output/small-5.txt
e1f0f06f1226ff7c82f942684d22e100 ./small-3.txt
e1f0f06f1226ff7c82f942684d22e100 output/small-3.txt
ec3fabd7edd0870bcfa5bbcfc7f2c7ec ./small-2.txt
ec3fabd7edd0870bcfa5bbcfc7f2c7ec output/small-2.txt
f80c5bbe46af5f46e4d4bcb2b939bf38 ./main.cpp
f80c5bbe46af5f46e4d4bcb2b939bf38 output/main.cpp
ff225cbc0f536f8af6946261c4a6b3ec ./small-9.txt
ff225cbc0f536f8af6946261c4a6b3ec output/small-9.txt
不要进行基于文本的 IO,而是考虑发送二进制文件大小和名称信息。请参阅示例:https://stackoverflow.com/search?tab=newest&q=user%3a85371%20endian%20file&searchOn=3
¹ 因为 TCP 数据包传输的工作原理;这就是所有图书馆的行为方式