blob: 4210a00e8574a12f1b8c835879b0351e6395ed2e [file] [log] [blame]
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -07001/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
Alexander Afanasyev6dfeffe2017-01-30 22:40:32 -08002/*
Eric Newberrye345baa2018-05-23 18:17:07 -07003 * Copyright (c) 2013-2018, Regents of the University of California,
4 * Colorado State University,
5 * University Pierre & Marie Curie, Sorbonne University.
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -07006 *
7 * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
8 *
9 * ndn-cxx library is free software: you can redistribute it and/or modify it under the
10 * terms of the GNU Lesser General Public License as published by the Free Software
11 * Foundation, either version 3 of the License, or (at your option) any later version.
12 *
13 * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
14 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
15 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
16 *
17 * You should have received copies of the GNU General Public License and GNU Lesser
18 * General Public License along with ndn-cxx, e.g., in COPYING.md file. If not, see
19 * <http://www.gnu.org/licenses/>.
20 *
21 * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
Eric Newberrye345baa2018-05-23 18:17:07 -070022 *
23 * @author Shuo Yang
24 * @author Weiwei Liu
25 * @author Chavoosh Ghasemi
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -070026 */
27
28#include "segment-fetcher.hpp"
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -050029#include "../name-component.hpp"
Eric Newberrye345baa2018-05-23 18:17:07 -070030#include "../encoding/buffer-stream.hpp"
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -050031#include "../lp/nack.hpp"
32#include "../lp/nack-header.hpp"
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -070033
Alexander Afanasyev6dfeffe2017-01-30 22:40:32 -080034#include <boost/lexical_cast.hpp>
Davide Pesavento5afbb0b2018-01-01 17:24:18 -050035#include <cmath>
Alexander Afanasyev6dfeffe2017-01-30 22:40:32 -080036
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -070037namespace ndn {
38namespace util {
39
Eric Newberrye345baa2018-05-23 18:17:07 -070040constexpr double SegmentFetcher::MIN_SSTHRESH;
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -050041
Eric Newberrye345baa2018-05-23 18:17:07 -070042void
43SegmentFetcher::Options::validate()
44{
45 if (maxTimeout < 1_ms) {
46 BOOST_THROW_EXCEPTION(std::invalid_argument("maxTimeout must be greater than or equal to 1 millisecond"));
47 }
48
49 if (initCwnd < 1.0) {
50 BOOST_THROW_EXCEPTION(std::invalid_argument("initCwnd must be greater than or equal to 1"));
51 }
52
53 if (aiStep < 0.0) {
54 BOOST_THROW_EXCEPTION(std::invalid_argument("aiStep must be greater than or equal to 0"));
55 }
56
57 if (mdCoef < 0.0 || mdCoef > 1.0) {
58 BOOST_THROW_EXCEPTION(std::invalid_argument("mdCoef must be in range [0, 1]"));
59 }
60}
61
62SegmentFetcher::SegmentFetcher(Face& face,
63 security::v2::Validator& validator,
64 const SegmentFetcher::Options& options)
65 : m_options(options)
66 , m_face(face)
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -050067 , m_scheduler(m_face.getIoService())
68 , m_validator(validator)
Eric Newberrye345baa2018-05-23 18:17:07 -070069 , m_rttEstimator(options.rttOptions)
70 , m_timeLastSegmentReceived(time::steady_clock::now())
71 , m_nextSegmentNum(0)
72 , m_cwnd(options.initCwnd)
73 , m_ssthresh(options.initSsthresh)
74 , m_nSegmentsInFlight(0)
75 , m_nSegments(0)
76 , m_highInterest(0)
77 , m_highData(0)
78 , m_recPoint(0)
79 , m_nReceived(0)
80 , m_nBytesReceived(0)
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -070081{
Eric Newberrye345baa2018-05-23 18:17:07 -070082 m_options.validate();
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -070083}
84
Muktadir Chowdhury1c109b42018-01-10 08:36:00 +000085shared_ptr<SegmentFetcher>
Eric Newberrycc910cd2018-05-06 17:01:40 -070086SegmentFetcher::start(Face& face,
87 const Interest& baseInterest,
Eric Newberrye345baa2018-05-23 18:17:07 -070088 security::v2::Validator& validator,
89 const SegmentFetcher::Options& options)
Eric Newberrycc910cd2018-05-06 17:01:40 -070090{
Eric Newberrye345baa2018-05-23 18:17:07 -070091 shared_ptr<SegmentFetcher> fetcher(new SegmentFetcher(face, validator, options));
92 fetcher->fetchFirstSegment(baseInterest, false, fetcher);
Eric Newberrycc910cd2018-05-06 17:01:40 -070093 return fetcher;
94}
95
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -070096void
97SegmentFetcher::fetchFirstSegment(const Interest& baseInterest,
Eric Newberrye345baa2018-05-23 18:17:07 -070098 bool isRetransmission,
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -050099 shared_ptr<SegmentFetcher> self)
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700100{
101 Interest interest(baseInterest);
Eric Newberry2b765f82018-06-25 14:51:13 -0700102 interest.setCanBePrefix(true);
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700103 interest.setMustBeFresh(true);
Eric Newberrye345baa2018-05-23 18:17:07 -0700104 interest.setInterestLifetime(m_options.interestLifetime);
105 if (isRetransmission) {
106 interest.refreshNonce();
107 }
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700108
Eric Newberrye345baa2018-05-23 18:17:07 -0700109 m_nSegmentsInFlight++;
110 auto pendingInterest = m_face.expressInterest(interest,
111 bind(&SegmentFetcher::afterSegmentReceivedCb,
112 this, _1, _2, self),
113 bind(&SegmentFetcher::afterNackReceivedCb,
114 this, _1, _2, self),
115 nullptr);
116 auto timeoutEvent =
117 m_scheduler.scheduleEvent(m_options.useConstantInterestTimeout ? m_options.maxTimeout : getEstimatedRto(),
118 bind(&SegmentFetcher::afterTimeoutCb, this, interest, self));
119 if (isRetransmission) {
120 updateRetransmittedSegment(0, pendingInterest, timeoutEvent);
121 }
122 else {
123 BOOST_ASSERT(m_pendingSegments.count(0) == 0);
124 m_pendingSegments.emplace(0, PendingSegment{SegmentState::FirstInterest, time::steady_clock::now(),
125 pendingInterest, timeoutEvent});
126 }
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700127}
128
129void
Eric Newberrye345baa2018-05-23 18:17:07 -0700130SegmentFetcher::fetchSegmentsInWindow(const Interest& origInterest, shared_ptr<SegmentFetcher> self)
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700131{
Eric Newberrye345baa2018-05-23 18:17:07 -0700132 if (checkAllSegmentsReceived()) {
133 // All segments have been retrieved
134 finalizeFetch(self);
135 }
136
137 int64_t availableWindowSize = static_cast<int64_t>(m_cwnd) - m_nSegmentsInFlight;
138
139 std::vector<std::pair<uint64_t, bool>> segmentsToRequest; // The boolean indicates whether a retx or not
140
141 while (availableWindowSize > 0) {
142 if (!m_retxQueue.empty()) {
143 auto pendingSegmentIt = m_pendingSegments.find(m_retxQueue.front());
144 m_retxQueue.pop();
145 if (pendingSegmentIt == m_pendingSegments.end()) {
146 // Skip re-requesting this segment, since it was received after RTO timeout
147 continue;
148 }
149 BOOST_ASSERT(pendingSegmentIt->second.state == SegmentState::InRetxQueue);
150 segmentsToRequest.emplace_back(pendingSegmentIt->first, true);
151 }
152 else if (m_nSegments == 0 || m_nextSegmentNum < static_cast<uint64_t>(m_nSegments)) {
153 if (m_receivedSegments.count(m_nextSegmentNum) > 0) {
154 // Don't request a segment a second time if received in response to first "discovery" Interest
155 m_nextSegmentNum++;
156 continue;
157 }
158 segmentsToRequest.emplace_back(m_nextSegmentNum++, false);
159 }
160 else {
161 break;
162 }
163 availableWindowSize--;
164 }
165
166 for (const auto& segment : segmentsToRequest) {
167 Interest interest(origInterest); // to preserve Interest elements
168 interest.refreshNonce();
Eric Newberry2b765f82018-06-25 14:51:13 -0700169 interest.setCanBePrefix(false);
Eric Newberrye345baa2018-05-23 18:17:07 -0700170 interest.setMustBeFresh(false);
171
172 Name interestName(m_versionedDataName);
173 interestName.appendSegment(segment.first);
174 interest.setName(interestName);
175 interest.setInterestLifetime(m_options.interestLifetime);
176 m_nSegmentsInFlight++;
177 auto pendingInterest = m_face.expressInterest(interest,
178 bind(&SegmentFetcher::afterSegmentReceivedCb,
179 this, _1, _2, self),
180 bind(&SegmentFetcher::afterNackReceivedCb,
181 this, _1, _2, self),
182 nullptr);
183 auto timeoutEvent =
184 m_scheduler.scheduleEvent(m_options.useConstantInterestTimeout ? m_options.maxTimeout : getEstimatedRto(),
185 bind(&SegmentFetcher::afterTimeoutCb, this, interest, self));
186 if (segment.second) { // Retransmission
187 updateRetransmittedSegment(segment.first, pendingInterest, timeoutEvent);
188 }
189 else { // First request for segment
190 BOOST_ASSERT(m_pendingSegments.count(segment.first) == 0);
191 m_pendingSegments.emplace(segment.first, PendingSegment{SegmentState::FirstInterest,
192 time::steady_clock::now(),
193 pendingInterest, timeoutEvent});
194 m_highInterest = segment.first;
195 }
196 }
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700197}
198
199void
Muktadir Chowdhury1c109b42018-01-10 08:36:00 +0000200SegmentFetcher::afterSegmentReceivedCb(const Interest& origInterest,
Eric Newberrye345baa2018-05-23 18:17:07 -0700201 const Data& data,
Muktadir Chowdhury1c109b42018-01-10 08:36:00 +0000202 shared_ptr<SegmentFetcher> self)
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700203{
Muktadir Chowdhury1c109b42018-01-10 08:36:00 +0000204 afterSegmentReceived(data);
Eric Newberrye345baa2018-05-23 18:17:07 -0700205 BOOST_ASSERT(m_nSegmentsInFlight > 0);
206 m_nSegmentsInFlight--;
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700207
Eric Newberrye345baa2018-05-23 18:17:07 -0700208 name::Component currentSegmentComponent = data.getName().get(-1);
209 if (!currentSegmentComponent.isSegment()) {
210 return signalError(DATA_HAS_NO_SEGMENT, "Data Name has no segment number");
211 }
212
213 uint64_t currentSegment = currentSegmentComponent.toSegment();
214
215 // The first received Interest could have any segment ID
216 std::map<uint64_t, PendingSegment>::iterator pendingSegmentIt;
217 if (m_receivedSegments.size() > 0) {
218 pendingSegmentIt = m_pendingSegments.find(currentSegment);
219 }
220 else {
221 pendingSegmentIt = m_pendingSegments.begin();
222 }
223
224 // Cancel timeout event
225 m_scheduler.cancelEvent(pendingSegmentIt->second.timeoutEvent);
226 pendingSegmentIt->second.timeoutEvent = nullptr;
227
228 m_validator.validate(data,
229 bind(&SegmentFetcher::afterValidationSuccess, this, _1, origInterest,
230 pendingSegmentIt, self),
231 bind(&SegmentFetcher::afterValidationFailure, this, _1, _2, self));
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500232}
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700233
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500234void
Alexander Afanasyev6dfeffe2017-01-30 22:40:32 -0800235SegmentFetcher::afterValidationSuccess(const Data& data,
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500236 const Interest& origInterest,
Eric Newberrye345baa2018-05-23 18:17:07 -0700237 std::map<uint64_t, PendingSegment>::iterator pendingSegmentIt,
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500238 shared_ptr<SegmentFetcher> self)
239{
Eric Newberrye345baa2018-05-23 18:17:07 -0700240 // We update the last receive time here instead of in the segment received callback so that the
241 // transfer will not fail to terminate if we only received invalid Data packets.
242 m_timeLastSegmentReceived = time::steady_clock::now();
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500243
Eric Newberrye345baa2018-05-23 18:17:07 -0700244 m_nReceived++;
245
246 // It was verified in afterSegmentReceivedCb that the last Data name component is a segment number
247 uint64_t currentSegment = data.getName().get(-1).toSegment();
248 // Add measurement to RTO estimator (if not retransmission)
249 if (pendingSegmentIt->second.state == SegmentState::FirstInterest) {
250 m_rttEstimator.addMeasurement(m_timeLastSegmentReceived - pendingSegmentIt->second.sendTime,
251 std::max<int64_t>(m_nSegmentsInFlight + 1, 1));
252 }
253
254 // Remove from pending segments map
255 m_pendingSegments.erase(pendingSegmentIt);
256
257 // Copy data in segment to temporary buffer
258 auto receivedSegmentIt = m_receivedSegments.emplace(std::piecewise_construct,
259 std::forward_as_tuple(currentSegment),
260 std::forward_as_tuple(data.getContent().value_size()));
261 std::copy(data.getContent().value_begin(), data.getContent().value_end(),
262 receivedSegmentIt.first->second.begin());
263 m_nBytesReceived += data.getContent().value_size();
264 afterSegmentValidated(data);
265
266 if (data.getFinalBlock()) {
267 if (!data.getFinalBlock()->isSegment()) {
268 return signalError(FINALBLOCKID_NOT_SEGMENT,
269 "Received FinalBlockId did not contain a segment component");
270 }
271
272 if (data.getFinalBlock()->toSegment() + 1 != static_cast<uint64_t>(m_nSegments)) {
273 m_nSegments = data.getFinalBlock()->toSegment() + 1;
274 cancelExcessInFlightSegments();
275 }
276 }
277
278 if (m_receivedSegments.size() == 1) {
279 m_versionedDataName = data.getName().getPrefix(-1);
280 if (currentSegment == 0) {
281 // We received the first segment in response, so we can increment the next segment number
282 m_nextSegmentNum++;
283 }
284 }
285
286 if (m_highData < currentSegment) {
287 m_highData = currentSegment;
288 }
289
290 if (data.getCongestionMark() > 0 && !m_options.ignoreCongMarks) {
291 windowDecrease();
292 }
293 else {
294 windowIncrease();
295 }
296
297 fetchSegmentsInWindow(origInterest, self);
298}
299
300void
301SegmentFetcher::afterValidationFailure(const Data& data,
302 const security::v2::ValidationError& error,
303 shared_ptr<SegmentFetcher> self)
304{
305 signalError(SEGMENT_VALIDATION_FAIL, "Segment validation failed: " +
306 boost::lexical_cast<std::string>(error));
307}
308
309
310void
311SegmentFetcher::afterNackReceivedCb(const Interest& origInterest,
312 const lp::Nack& nack,
313 shared_ptr<SegmentFetcher> self)
314{
315 afterSegmentNacked();
316 BOOST_ASSERT(m_nSegmentsInFlight > 0);
317 m_nSegmentsInFlight--;
318
319 switch (nack.getReason()) {
320 case lp::NackReason::DUPLICATE:
321 case lp::NackReason::CONGESTION:
322 afterNackOrTimeout(origInterest, self);
323 break;
324 default:
325 signalError(NACK_ERROR, "Nack Error");
326 break;
327 }
328}
329
330void
331SegmentFetcher::afterTimeoutCb(const Interest& origInterest,
332 shared_ptr<SegmentFetcher> self)
333{
334 afterSegmentTimedOut();
335 BOOST_ASSERT(m_nSegmentsInFlight > 0);
336 m_nSegmentsInFlight--;
337 afterNackOrTimeout(origInterest, self);
338}
339
340void
341SegmentFetcher::afterNackOrTimeout(const Interest& origInterest, shared_ptr<SegmentFetcher> self)
342{
343 if (time::steady_clock::now() >= m_timeLastSegmentReceived + m_options.maxTimeout) {
344 // Fail transfer due to exceeding the maximum timeout between the succesful receipt of segments
345 return signalError(INTEREST_TIMEOUT, "Timeout exceeded");
346 }
347
348 name::Component lastNameComponent = origInterest.getName().get(-1);
349 std::map<uint64_t, PendingSegment>::iterator pendingSegmentIt;
350 BOOST_ASSERT(m_pendingSegments.size() > 0);
351 if (lastNameComponent.isSegment()) {
352 BOOST_ASSERT(m_pendingSegments.count(lastNameComponent.toSegment()) > 0);
353 pendingSegmentIt = m_pendingSegments.find(lastNameComponent.toSegment());
354 }
355 else { // First Interest
356 BOOST_ASSERT(m_pendingSegments.size() > 0);
357 pendingSegmentIt = m_pendingSegments.begin();
358 }
359
360 // Cancel timeout event and set status to InRetxQueue
361 m_scheduler.cancelEvent(pendingSegmentIt->second.timeoutEvent);
362 pendingSegmentIt->second.timeoutEvent = nullptr;
363 pendingSegmentIt->second.state = SegmentState::InRetxQueue;
364
365 m_rttEstimator.backoffRto();
366
367 if (m_receivedSegments.size() == 0) {
368 // Resend first Interest (until maximum receive timeout exceeded)
369 fetchFirstSegment(origInterest, true, self);
370 }
371 else {
372 windowDecrease();
373 m_retxQueue.push(pendingSegmentIt->first);
374 fetchSegmentsInWindow(origInterest, self);
375 }
376}
377
378void
379SegmentFetcher::finalizeFetch(shared_ptr<SegmentFetcher> self)
380{
381 // Combine segments into final buffer
382 OBufferStream buf;
383 // We may have received more segments than exist in the object.
384 BOOST_ASSERT(m_receivedSegments.size() >= static_cast<uint64_t>(m_nSegments));
385
386 for (int64_t i = 0; i < m_nSegments; i++) {
387 buf.write(m_receivedSegments[i].get<const char>(), m_receivedSegments[i].size());
388 }
389
390 onComplete(buf.buf());
391}
392
393void
394SegmentFetcher::windowIncrease()
395{
396 if (m_options.useConstantCwnd) {
397 BOOST_ASSERT(m_cwnd == m_options.initCwnd);
398 return;
399 }
400
401 if (m_cwnd < m_ssthresh) {
402 m_cwnd += m_options.aiStep; // additive increase
403 }
404 else {
405 m_cwnd += m_options.aiStep / std::floor(m_cwnd); // congestion avoidance
406 }
407}
408
409void
410SegmentFetcher::windowDecrease()
411{
412 if (m_options.disableCwa || m_highData > m_recPoint) {
413 m_recPoint = m_highInterest;
414
415 if (m_options.useConstantCwnd) {
416 BOOST_ASSERT(m_cwnd == m_options.initCwnd);
417 return;
418 }
419
420 // Refer to RFC 5681, Section 3.1 for the rationale behind the code below
421 m_ssthresh = std::max(MIN_SSTHRESH, m_cwnd * m_options.mdCoef); // multiplicative decrease
422 m_cwnd = m_options.resetCwndToInit ? m_options.initCwnd : m_ssthresh;
423 }
424}
425
426void
427SegmentFetcher::signalError(uint32_t code, const std::string& msg)
428{
429 // Cancel all pending Interests before signaling error
430 for (const auto& pendingSegment : m_pendingSegments) {
431 m_face.removePendingInterest(pendingSegment.second.id);
432 if (pendingSegment.second.timeoutEvent) {
433 m_scheduler.cancelEvent(pendingSegment.second.timeoutEvent);
434 }
435 }
436 onError(code, msg);
437}
438
439void
440SegmentFetcher::updateRetransmittedSegment(uint64_t segmentNum,
441 const PendingInterestId* pendingInterest,
442 scheduler::EventId timeoutEvent)
443{
444 auto pendingSegmentIt = m_pendingSegments.find(segmentNum);
445 BOOST_ASSERT(pendingSegmentIt != m_pendingSegments.end());
446 BOOST_ASSERT(pendingSegmentIt->second.state == SegmentState::InRetxQueue);
447 pendingSegmentIt->second.state = SegmentState::Retransmitted;
448 pendingSegmentIt->second.id = pendingInterest;
449 pendingSegmentIt->second.timeoutEvent = timeoutEvent;
450}
451
452void
453SegmentFetcher::cancelExcessInFlightSegments()
454{
455 for (auto it = m_pendingSegments.begin(); it != m_pendingSegments.end();) {
456 if (it->first >= static_cast<uint64_t>(m_nSegments)) {
457 m_face.removePendingInterest(it->second.id);
458 if (it->second.timeoutEvent) {
459 m_scheduler.cancelEvent(it->second.timeoutEvent);
460 }
461 it = m_pendingSegments.erase(it);
462 BOOST_ASSERT(m_nSegmentsInFlight > 0);
463 m_nSegmentsInFlight--;
Alexander Afanasyevf3cfab52014-08-17 22:15:25 -0700464 }
465 else {
Eric Newberrye345baa2018-05-23 18:17:07 -0700466 ++it;
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500467 }
468 }
469}
470
Eric Newberrye345baa2018-05-23 18:17:07 -0700471bool
472SegmentFetcher::checkAllSegmentsReceived()
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500473{
Eric Newberrye345baa2018-05-23 18:17:07 -0700474 bool haveReceivedAllSegments = false;
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500475
Eric Newberrye345baa2018-05-23 18:17:07 -0700476 if (m_nSegments != 0 && m_nReceived >= m_nSegments) {
477 haveReceivedAllSegments = true;
478 // Verify that all segments in window have been received. If not, send Interests for missing segments.
479 for (uint64_t i = 0; i < static_cast<uint64_t>(m_nSegments); i++) {
480 if (m_receivedSegments.count(i) == 0) {
481 m_retxQueue.push(i);
482 haveReceivedAllSegments = false;
483 }
484 }
Muktadir R Chowdhury2bc2df02016-04-05 16:55:41 -0500485 }
486
Eric Newberrye345baa2018-05-23 18:17:07 -0700487 return haveReceivedAllSegments;
488}
489
490time::milliseconds
491SegmentFetcher::getEstimatedRto()
492{
493 // We don't want an Interest timeout greater than the maximum allowed timeout between the
494 // succesful receipt of segments
495 return std::min(m_options.maxTimeout,
496 time::duration_cast<time::milliseconds>(m_rttEstimator.getEstimatedRto()));
Muktadir R Chowdhuryf58f8f42015-09-02 11:56:49 -0500497}
498
499} // namespace util
500} // namespace ndn