停等协议是运输层中用于保证数据可靠传输所使用的一个协议,它的核心原理是:发送方向接收方发送完一个分组时,会立即停止下来,等待接收方的确认报文段,收到了接收方的确认报文段就发送下一个报文段,如果超过一定时间还没有收到接受方的确认报文段,就会重新发送之间发送过的所有报文段。
之前我们用Boost.asio搭建了一个简单的异步通信的服务器和客户端,我就在这个服务器和客户端的基础上进行改进实现一个简单的停等协议。
服务器端代码
main函数是整个服务器启动的入口,服务器监听12345端口号。
int main()
{
try
{
boost::asio::io_context ioc;
Server s(ioc, 12345);
ioc.run();
}
catch (const boost::system::system_error &e)
{
std::cout << "Error: " << e.what() << std::endl;
}
return 0;
}
Server类
class Server {
public:
Server(boost::asio::io_context& ioc, int port) : server(ioc, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), port)){
std::cout << "服务器启动" << std::endl;
//数据双向通信功能
memset(recdata, '\0', max_len);
server.async_receive_from(boost::asio::buffer(recdata), clientep, std::bind(&Server::handle_receive, this, std::placeholders::_1, std::placeholders::_2));
}
void handle_receive(const boost::system::error_code& error, std::size_t transformed) {
if (!error&&transformed>0) {
//数据双向通信功能
std::cout << "成功接收到了客户端的数据: " << recdata << std::endl;
std::string str(recdata);
std::vector<std::string> res; //必须用vector类型,才能在split函数里面传入字符串
boost::split(res, str, boost::is_any_of(":"));
cnt = std::stoi(*res.begin());
memset(arkdata, '\0', max_len);
strcpy_s(arkdata,(std::to_string(cnt) + " ACK").c_str());
//模拟数据丢失功能,测试计时器超时
std::cout << "服务器睡眠15s" << std::endl;
std::chrono::seconds durations(15);
std::this_thread::sleep_for(durations);//暂停程序15s
server.async_send_to(boost::asio::buffer(arkdata,strlen(arkdata)), clientep, std::bind(&Server::handle_send, this, std::placeholders::_1));
}
else {
std::cout << "接收数据出错了: " << error.message() << std::endl;
}
}
void handle_send(const boost::system::error_code& error) {
if (!error) {
std::cout << "已经向客户端发送了确认分组: " << arkdata << std::endl;
memset(recdata, '\0', max_len);
server.async_receive_from(boost::asio::buffer(recdata), clientep, std::bind(&Server::handle_receive, this, std::placeholders::_1, std::placeholders::_2));
}
else {
std::cout << "发送数据出错了: " << error.message() << std::endl;
}
}
private:
boost::asio::ip::udp::socket server;
boost::asio::ip::udp::endpoint clientep;
/*std::ofstream *file;*/
int cnt = 0;
int max_len = 1024;
char recdata[1024] = ""; // 格式:序号:数据内容
char arkdata[1024] = "";
};
Server类中定义了一个socket成员对象server,配合clientep可以用于侦听指定的端口并处理客户端发送的数据包,和Tcp通信不同的是,UDP通信不需要建立连接,因此服务器端的套接字并不会监听客户端的连接,只是起到了一个侦听端口的作用,而clientep则用于处理客户端发送而来的请求。
当服务器接收到客户端发送来的报文段时,它会识别这一报文段的序号并发送一段确认报文段,该确认报文段的确认号与该报文段的序列号保持一致,并且服务器还通过多线程的休眠函数模拟了确认迟到的情况,服务器收到数据后会等待15s之后再发送确认报文段给客户端。
客户端代码
main函数,客户端程序启动的入口。
int main()
{
try
{
std::cout << "客户端启动" << std::endl;
boost::asio::io_context ioc;
Client c(ioc,12345);
ioc.run();
// 只会运行 IO 上下文的事件循环,也就是异步读和写函数(async_recive_from和async_send_to),不会导致程序从头开始执行
}
catch (const boost::system::system_error &e)
{
std::cout << "出错了: " << e.what()<< "错误代码: " << e.code() << std::endl;
}
}
Client类
class Client
{
public:
Client(boost::asio::io_context& ioc, int port) : client(ioc, boost::asio::ip::udp::v4()), serverep(boost::asio::ip::address::from_string("127.0.0.1"), port),timer(ioc)
{
//普通数据传输功能
memset(sendata, '\0', max_len);
strcpy(sendata, (std::to_string(cnt) + ":").c_str());
std::cout << "请输入要发送的消息: ";
std::string str;
std::cin >> str;
strcat(sendata, str.c_str());
std::cout << "设置客户端的计时器为10s" << std::endl;
timer.expires_after(std::chrono::seconds(10));//设置超时时间10s
timer.async_wait(std::bind(&Client::handle_timeout, this, std::placeholders::_1));
/*设置超时回调函数
调用`timer.async_wait()`函数会在定时器到期时异步触发回调函数`handle_wait`。因此,回调函数不会立即被调用,而是在等待10秒后才会被调用*/
client.async_send_to(boost::asio::buffer(sendata,strlen(sendata)), serverep, std::bind(&Client::handle_send, this, std::placeholders::_1));
}
void handle_recive(const boost::system::error_code& e, std::size_t recived)//不能少了const
{
if (!e)
{
//file->close(); // 在发送完成后关闭文件
/*delete file;
file = nullptr;*/
std::cout << "收到了数据:" << recdata << std::endl;
timer.cancel();//取消计时器
std::vector<std::string>str;
boost::split(str, recdata, boost::is_any_of(":"));
if (cnt == std::stoi(*str.begin()))
{
std::cout << "确认分组和数据分组匹配,可以发送新的数据" << std::endl;
if (!cnt)cnt++;
else cnt--;
memset(sendata, '\0', max_len);
strcpy(sendata, (std::to_string(cnt) + ":").c_str());
std::cout << "请输入要发送的内容: ";
std::string str2;
std::cin >> str2;
strcat(sendata, str2.c_str());
/* file->write(sendata, strlen(sendata));*/
client.close();
client.open(boost::asio::ip::udp::v4());
timer.expires_after(std::chrono::seconds(10));//重新设置超时时间10s
timer.async_wait(std::bind(&Client::handle_timeout, this, std::placeholders::_1));//设置超时回调函数
client.async_send_to(boost::asio::buffer(sendata, strlen(sendata)), serverep, std::bind(&Client::handle_send, this, std::placeholders::_1));
}
else
{
std::cout << "确认分组和数据分组不匹配,将收到的数据丢弃" << std::endl;
}
}
else
{
std::cout << "接收数据出错了: " << e.what() << std::endl;
}
}
void handle_send(const boost::system::error_code& e)//不能少了const
{
if (!e)
{
std:: cout << "发送了数据: " << sendata << std::endl;
memset(recdata, '\0', max_len);
client.async_receive_from(boost::asio::buffer(recdata), serverep, std::bind(&Client::handle_recive, this, std::placeholders::_1, std::placeholders::_2));
}
else
{
std::cout << "发送数据出错了: " << e.what() << std::endl;
}
}
void handle_timeout(const boost::system::error_code& e)
{
if (!e)
{
std::cout << "计时器超时,重新发送数据" << std::endl;
timer.cancel();
timer.expires_after(std::chrono::seconds(10));//重新设置超时时间10s
timer.async_wait(std::bind(&Client::handle_timeout, this, std::placeholders::_1));//设置超时回调函数
client.async_send_to(boost::asio::buffer(sendata,strlen(sendata)), serverep,std::bind(&Client::handle_send, this, std::placeholders::_1));
}
else if (e == boost::asio::error::operation_aborted)
{
std::cout << "计时器取消了"<< std::endl;
/*如果在计时器超时之前调用了 `cancel()` 函数取消计时器,那么回调函数将被调用,
并且传递的 `error_code` 参数将为 `boost::asio::error::operation_aborted`。
这表示计时器被取消*/
}
else
{
std::cout << "计时器出错了: " << e.message() << std::endl;
/*如果在计时器超时之前发生了其他错误,例如计时器对象被销毁或发生了与底层操作系统相关的错误,
那么回调函数将被调用,并且传递的 `error_code` 参数将包含相应的错误信息。*/
}
}
private:
boost::asio::ip::udp::socket client;
boost::asio::ip::udp::endpoint serverep;
/*std::ofstream *file;*/
boost::asio::steady_timer timer;//定时器
int max_len = 1024;
char recdata[1024]="";
char sendata[1024]="";
int cnt = 0;
};
Client类中封装了一个socket,用来侦听服务器发送过来的数据,并配合端点serverep处理发送过来的数据,在程序开始,客户端会向服务器发送一段报文段,并为每一个报文段都分配一个序号,然后等待接收服务器发送而来的确认报文段,并且客户端设置了一个定时器timer,如果超过10s钟,客户端没有收到确认报文段,那么客户端就会重新发送上一个已经发送的报文段,由于我们服务器设置了确认报文段的发送时间是15s,因此,客户端在10s内将收不到服务器发送而来的确认分组,因此,客户端会重新发送上一个已经发送的分组。