-
Notifications
You must be signed in to change notification settings - Fork 7
/
async.cpp
298 lines (262 loc) · 10.5 KB
/
async.cpp
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/*
* See "traditional.cpp" first.
*
* This file illustrates how C++ experts write asynchronous code today.
*
* The code is highly efficient, and scales way better than the code shown in
* "traditional.cpp". It is however harder to write, harder to debug and much
* harder to understand.
*
* Using async IO, we don't dedicate one thread to a single connection. We
* can handle tens of thousands of connections from one thread. Very often,
* servers that use this approach will have only one thread per CPU-core,
* no matter how many connections they deal with.
*
* What I show here is a simple async state-machine that download the page,
* using the same naive algorithm as in "traditional.cpp"
*
* Copyright 2014 by Jarle (jgaa) Aase <jarle@jgaa.com>
* I put this code in the public domain.
*/
#include <iostream>
#include <string>
#include <sstream>
#include <future>
#include <thread>
#include <memory>
#include <boost/asio.hpp>
// Convenience...
using boost::asio::ip::tcp;
/*! HTTP Client object. */
class Request
{
/* We can't use the stack for data this time, so we put what we need
* as private properties in the object.
*/
const std::string host_;
boost::asio::io_service io_service_;
tcp::resolver resolver_;
std::promise<std::string> result_;
std::unique_ptr<tcp::socket> sck_;
char io_buffer_[1024] = {};
std::string result_buffer_;
public:
/*! Constructor
*
* Since we use properties to hold the data we need, it makes sense to
* initialize it in the constructor.
*/
Request(const std::string& host)
: host_(host), resolver_(io_service_)
{}
/*! Async fetch a single HTTP page, at the root-level "/".
*
* Since we gave the host parameter to the constructor, this method
* is now without arguments.
*
* @returns A future that will, at some later time, be able to provide
* the content of the page, or throw an exception if the async
* fetch failed. We need to return a future, in stead of a string, since
* this is an Async method that is expected to return immediately.
*
* @Note This is not production-grade code, as we don't look at the
* HTTP headers from the server, and don't validate the length of
* the returned data. We also don't deal with redirects or authentication.
* This is intentionally, as this code is meant to illustrate how we do
* network IO, and not how we deal with an increasingly bloated HTTP
* standard.
*/
std::future<std::string> Fetch() {
// Start resolving the address in another thread.
std::thread([=]() {
/* This is the scope of a C++11 lambda expression.
* It executes in the newly started thread, and when the scope
* is left, the thread will exit.
*/
/* Resolve the host-name, and ask asio to call OnResolved()
* when done. async_resolve() itself will return immediately.
*/
resolver_.async_resolve( {host_, "80"},
std::bind(&Request::OnResolved, this,
std::placeholders::_1,
std::placeholders::_2));
/* Run the event-loop for asio now. This function will return
* when we have no more requests pending - in our case, when
* we fail or have received the page from the server.
*/
io_service_.run();
}
/* Back in the main-thread, we need to detach the thread we just
* started from it's local variable - or the program, will die.
*/
).detach();
/* Return the future.
*
* At this point, the thread we just started will deal with
* resolving the host-name and fetching the page on it's own.
*/
return result_.get_future();
}
private:
/*! Callback that is called for an IP belonging to the host
*
* The first time, it is initiated from async_resolve. If we fail
* to connect to that IP, it may be called again, initiated
* from OnConnected(), as we iterate over the IP numbers returned
* by the DNS system.
*
* If we have to debug this, there are no easy way to know if we were
* called from async_resolve or OnConnected, just by looking at the
* properties and stack-trace. This complicates debugging quite a bit.
*/
void OnResolved(const boost::system::error_code& error,
tcp::resolver::iterator iterator) {
try {
if (error || (iterator == tcp::resolver::iterator())) {
// We failed. The exception is picked up below
throw std::runtime_error("Failed to resolve host");
}
// Connect
sck_ = std::make_unique<tcp::socket>(io_service_);
/* Initiate an async Connect operation.
*
* Ask asio to call OnConnected() when we have a connection
* or a connection-failed result.
*
* async_connect returns immediately
*/
sck_->async_connect(*iterator,
std::bind(&Request::OnConnected, this,
iterator, std::placeholders::_1));
} catch(...) {
/* We pick up the exception, and pass it to the result_
* property. At this moment, the future that the main-thread
* holds will unblock, and the exception will be re-thrown
* there when result.get() is called.
*
* Since we don't start another async operation,
* io_service_.run() will return, and our thread will exit.
*/
result_.set_exception(std::current_exception());
}
}
/*! Callback that is called by asio when we are connected,
* or failed to connect.
*
* If we failed, this callback will participate in iterating
* over the IP's returned by the DNS system.
*/
void OnConnected(tcp::resolver::iterator iterator,
const boost::system::error_code& error) {
if (error) {
/* Simulate a fragment of the loop in "traditional.cpp"
* for(; address_it != addr_end; ++address_it)
*/
++iterator;
/* Ask asio to call OnResolved().
*
* We post it as a task in stead of calling it directly to
* avoid stack-buildup and potential side-effects (when the
* code becomes more complex and we may introduce mutexes etc.)
*/
io_service_.post(std::bind(&Request::OnResolved, this,
boost::system::error_code(),
iterator));
return;
}
/* Async send the HTTP request
*
* Ask asio to call OnSentRequest() when done, or if it failed.
*/
boost::asio::async_write(*sck_, boost::asio::buffer(GetRequest(host_)),
std::bind(&Request::OnSentRequest, this,
std::placeholders::_1));
}
/* Callback when a request have been sent (or failed). */
void OnSentRequest(const boost::system::error_code& error) {
try {
if (error) {
// Failure. Same work-flow as in OnResolved()
throw std::runtime_error("Failed to send request");
}
// Initiate fetching of the reply
FetchMoreData();
} catch(...) {
// Same work-flow as in OnResolved()
result_.set_exception(std::current_exception());
}
}
/* Initiate a async read operation to get a reply or part of it.
*i
* This function returns immediately.
*/
void FetchMoreData() {
/* Ask asio to start a async read, and to call OnDataRead when done */
sck_->async_read_some(boost::asio::mutable_buffers_1(io_buffer_,
sizeof(io_buffer_)),
std::bind(&Request::OnDataRead,
this,
std::placeholders::_1,
std::placeholders::_2));
}
/*! Callback that is called when we have read (or failed to read) data
* from the remote HTTP server.
*/
void OnDataRead(const boost::system::error_code& error,
std::size_t bytes_transferred) {
if (error) {
/* Just assume that we are done
*
* We set a value to the result_, and immediately the value
* will be available to the main-thread that can get it from
* result.get(). In this case, there is unlikely to be any
* exceptions.
*
* Since we don't start another async operation,
* io_service_.run() will return, and our thread will exit.
*/
result_.set_value(std::move(result_buffer_));
return;
}
/* Append the data read to our private buffer that we will later\
* hand over to the main thread.
*/
result_buffer_.append(io_buffer_, bytes_transferred);
// Initiate another async read from the server.
FetchMoreData();
}
// Construct a simple HTTP request to the host
std::string GetRequest(const std::string& host) const {
std::ostringstream req;
req << "GET / HTTP/1.1\r\nHost: " << host << " \r\n"
<< "Connection: close\r\n\r\n";
return req.str();
}
};
int main(int argc, char *argv[])
{
// Check that we have one and only one argument (the host-name)
assert(argc == 2 && *argv[1]);
// Construct our HTTP Client object
Request req(argv[1]);
// Initiate the fetch and get the future
auto result = req.Fetch();
// Wait for the other thread to do it's job
result.wait();
try {
// Get the page or an exception
std::cout << result.get();
} catch(const std::exception& ex) {
// Explain to the user that there was a problem
std::cerr << "Caught exception " << ex.what() << std::endl;
// Error exit
return -1;
} catch(...) {
// Explain to the user that there was an ever bigger problem
std::cerr << "Caught exception!" << std::endl;
// Error exit
return -2;
}
// Successful exit
return 0;
}