使用Asio进行文件传输

近期实现了一个简单的文件同步工具,用boost::asio进行的网络通信,定义了tcp协议头,发送消息使用了队列机制,用fstream进行文件读写。练练手。

几个知识点

搭建基本框架时觉得有几个值得记录的地方。

协议头定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma  pack (push,1)
struct ProtocolHead
{
uint32_t pack_size; //包大小
//...其他自定义
};

struct ProtocolEx
{
ProtocolHead head;
uint32_t session;
//...其他自定义
};
#pragma pack(pop)

在网络协议编程中,经常会处理不同协议的数据报文。一种方法是通过指针偏移的方法来得到各种信息,但这样做不仅编程复杂,而且一旦协议有变化,程序修改起来也比较麻烦。在了解了编译器对结构空间的分配原则之后,我们完全可以利用这一特性定义自己的协议结构,通过访问结构的成员来获取各种信息。这样做,不仅简化了编程,而且即使协议发生变化,我们也只需修改协议结构的定义即可。

对于#pragma pack

这是给编译器用的参数设置,有关结构体字节对齐方式设置, #pragma pack是指定数据在内存中的对齐方式。

1
2
3
4
5
6
#pragma pack (n)       作用:C编译器将按照n个字节对齐。
#pragma pack () 作用:取消自定义字节对齐方式。


#pragma pack (push,1) 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
#pragma pack(pop) 作用:恢复对齐状态

因此可见,加入push和pop可以使对齐恢复到原来状态,而不是编译器默认,可以说后者更优,但是很多时候两者差别不大。

这里强制按照1字节进行对齐,可以理解成所有的内容都是按照1字节进行读取,其他所有的数据成员都是1字节的整数倍,所以也就不用进行内存对齐,各个成员在内存中就按照实际顺序进行排列。这样既节省空间,又利于控制。

读定长

定义好协议头,就可以严格按照约定进行定长读写,逻辑清晰,并且不需要像使用read_some一样处理复杂的粘包情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ProtocolHead head_;
void Session::do_read_head()
{
auto self(shared_from_this());
head_ = { };
boost::asio::async_read(socket_, boost::asio::buffer(&head_, sizeof(ProtocolHead)),
[this, self](const boost::system::error_code& ec, std::size_t /*bytes_transferred*/)
{
if (ec)
{
do_eof();
return;
}
//对头进行一些验证操作
//...
//头合格,读体
do_read_body();
});
}

void Session::do_read_body()
{
auto self(shared_from_this());
//读取包头内容
uint32_t size_ = head_.pack_size;
std::vector<char> buffer_;//使用vector代替字节数组,更灵活些
buffer_.resize(buffer_size);
//读定长
boost::asio::async_read(socket_, boost::asio::buffer(&buffer_[0], buffer_.size()),
[this, self](const boost::system::error_code& ec, std::size_t /*bytes_transferred*/)
{
if (ec)
{
do_eof();
return;
}
//读完后续工作
//...

});
}

发送队列

为了将读文件和发数据过程分割,两个过程进行节奏肯定是不一样的,多次同时调用async_write会出问题,所以维护一个队列来发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using Buffers = std::vector<char>;
boost::mutex send_list_mutex_;
std::list<Buffers> send_list_;
//发送队列
void EchoSession::send_data(Buffers&& buff)
{
if (buff.size() == 0) {
return;
}
//这里有个问题,只有这样折腾一次,才能够释放掉参数buff,还没搞懂怎么回事
auto temp_buff = std::move(buff);
send_list_mutex_.lock();
send_list_.push_back(std::move(temp_buff));
//这里是判断是否有写任务正在进行
if (send_list_.size() > 1) {
send_list_mutex_.unlock();
return;
}

send_list_mutex_.unlock();
do_write();
}

//写操作
void EchoSession::do_write()
{
auto self(shared_from_this());
//自动锁,可以自己定义一个原子锁放进去,这里直接用了boost的锁
std::lock_guard<boost::mutex>lock(send_list_mutex_);
if (send_list_.empty()) {
return;
}
//从队列头开始写
boost::asio::async_write(socket_, boost::asio::buffer(send_list_.front()),
[this, self](const boost::system::error_code& ec, std::size_t length) {
if (ec)
{
do_eof();
return;
}
//写完判断队列是否为空,不为空则继续
send_list_mutex_.lock();
send_list_.pop_front();
if (!send_list_.empty()) {
send_list_mutex_.unlock();
do_write();
}
else {
send_list_mutex_.unlock();
}
});
}

下一步

后面可以对功能进行扩展,比如客户端在固定时间段(空闲时间)进行同步任务、一次任务是否完成,重试多次、服务端对客户端连接进行管理等。其实这样功能的现成软件很多,积累些经验。

Powered by Hexo

Copyright © 2018 - 2022 Yshen's Blog All Rights Reserved.

UV : | PV :

Fork me on GitHub