├── .gitignore ├── README.md ├── example ├── main_client.cpp └── main_server.cpp ├── Serializer.hpp └── buttonrpc.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | build-vs2015/buttonrpc-demo/Release/ 2 | build-vs2015/buttonrpc-demo/buttonrpc-demo.VC.db 3 | *.ipdb 4 | *.pdb 5 | *.iobj 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # buttonrpc - modern rpc framework for C++ 3 | - ZeroMQ 作为网络层 4 | - 使用c++14开发 5 | 6 | ## Features 7 | - 轻量级,跨平台,简单易用 8 | - 服务端可以绑定自由函数,类成员函数,std::function对象 9 | - 服务端可以绑定参数是任意自定义类型的函数 10 | - 客户端与服务端自动重连机制 11 | - 客户端调用超时选项 12 | 13 | ## Example 14 | server: 15 | 16 | ```c++ 17 | #include "buttonrpc.hpp" 18 | 19 | int foo(int age, int mm){ 20 | return age + mm; 21 | } 22 | 23 | int main() 24 | { 25 | buttonrpc server; 26 | server.as_server(5555); 27 | 28 | server.bind("foo", foo); 29 | server.run(); 30 | 31 | return 0; 32 | } 33 | ``` 34 | 35 | client: 36 | 37 | ```c++ 38 | #include 39 | #include "buttonrpc.hpp" 40 | 41 | int main() 42 | { 43 | buttonrpc client; 44 | client.as_client("127.0.0.1", 5555); 45 | int a = client.call("foo", 2, 3).val(); 46 | std::cout << "call foo result: " << a << std::endl; 47 | system("pause"); 48 | return 0; 49 | } 50 | 51 | // output: call foo result: 5 52 | 53 | ``` 54 | 55 | ## Dependences 56 | - [ZeroMQ](http://zguide.zeromq.org/page:all) 57 | 58 | 59 | ## Building 60 | - windows vs2015 或者更高版本, linux 添加编译选项:-std=c++1z 61 | 62 | ## Usage 63 | 64 | - 1: 更多例子在目录 example/ 下 65 | 66 | -------------------------------------------------------------------------------- /example/main_client.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "buttonrpc.hpp" 5 | 6 | #ifdef _WIN32 7 | #include // use sleep 8 | #else 9 | #include 10 | #endif 11 | 12 | 13 | #define buttont_assert(exp) { \ 14 | if (!(exp)) {\ 15 | std::cout << "ERROR: "; \ 16 | std::cout << "function: " << __FUNCTION__ << ", line: " << __LINE__ << std::endl; \ 17 | system("pause"); \ 18 | }\ 19 | }\ 20 | 21 | 22 | struct PersonInfo 23 | { 24 | int age; 25 | std::string name; 26 | float height; 27 | 28 | // must implement 29 | friend Serializer& operator >> (Serializer& in, PersonInfo& d) { 30 | in >> d.age >> d.name >> d.height; 31 | return in; 32 | } 33 | friend Serializer& operator << (Serializer& out, PersonInfo d) { 34 | out << d.age << d.name << d.height; 35 | return out; 36 | } 37 | }; 38 | 39 | int main() 40 | { 41 | buttonrpc client; 42 | client.as_client("127.0.0.1", 5555); 43 | client.set_timeout(2000); 44 | 45 | int callcnt = 0; 46 | while (1){ 47 | std::cout << "current call count: " << ++callcnt << std::endl; 48 | 49 | client.call("foo_1"); 50 | 51 | client.call("foo_2", 10); 52 | 53 | int foo3r = client.call("foo_3", 10).val(); 54 | buttont_assert(foo3r == 100); 55 | 56 | int foo4r = client.call("foo_4", 10, "buttonrpc", 100, (float)10.8).val(); 57 | buttont_assert(foo4r == 1000); 58 | 59 | PersonInfo dd = { 10, "buttonrpc", 170 }; 60 | dd = client.call("foo_5", dd, 120).val(); 61 | buttont_assert(dd.age == 20); 62 | buttont_assert(dd.name == "buttonrpc is good"); 63 | buttont_assert(dd.height == 180); 64 | 65 | int foo6r = client.call("foo_6", 10, "buttonrpc", 100).val(); 66 | buttont_assert(foo6r == 1000); 67 | 68 | buttonrpc::value_t xx = client.call("foo_7", 666); 69 | buttont_assert(!xx.valid()); 70 | #ifdef _WIN32 71 | Sleep(1000); 72 | #else 73 | sleep(1); 74 | #endif 75 | } 76 | 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /example/main_server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "buttonrpc.hpp" 4 | 5 | 6 | #define buttont_assert(exp) { \ 7 | if (!(exp)) {\ 8 | std::cout << "ERROR: "; \ 9 | std::cout << "function: " << __FUNCTION__ << ", line: " << __LINE__ << std::endl; \ 10 | system("pause"); \ 11 | }\ 12 | }\ 13 | 14 | 15 | // 测试例子 16 | void foo_1() { 17 | 18 | } 19 | 20 | void foo_2(int arg1) { 21 | buttont_assert(arg1 == 10); 22 | } 23 | 24 | int foo_3(int arg1) { 25 | buttont_assert(arg1 == 10); 26 | return arg1 * arg1; 27 | } 28 | 29 | int foo_4(int arg1, std::string arg2, int arg3, float arg4) { 30 | buttont_assert(arg1 == 10); 31 | buttont_assert(arg2 == "buttonrpc"); 32 | buttont_assert(arg3 == 100); 33 | buttont_assert((arg4 > 10.0) && (arg4 < 11.0)); 34 | return arg1 * arg3; 35 | } 36 | 37 | class ClassMem 38 | { 39 | public: 40 | int bar(int arg1, std::string arg2, int arg3) { 41 | buttont_assert(arg1 == 10); 42 | buttont_assert(arg2 == "buttonrpc"); 43 | buttont_assert(arg3 == 100); 44 | return arg1 * arg3; 45 | } 46 | }; 47 | 48 | struct PersonInfo 49 | { 50 | int age; 51 | std::string name; 52 | float height; 53 | 54 | // must implement 55 | friend Serializer& operator >> (Serializer& in, PersonInfo& d) { 56 | in >> d.age >> d.name >> d.height; 57 | return in; 58 | } 59 | friend Serializer& operator << (Serializer& out, PersonInfo d) { 60 | out << d.age << d.name << d.height; 61 | return out; 62 | } 63 | }; 64 | 65 | PersonInfo foo_5(PersonInfo d, int weigth) 66 | { 67 | buttont_assert(d.age == 10); 68 | buttont_assert(d.name == "buttonrpc"); 69 | buttont_assert(d.height == 170); 70 | 71 | PersonInfo ret; 72 | ret.age = d.age + 10; 73 | ret.name = d.name + " is good"; 74 | ret.height = d.height + 10; 75 | return ret; 76 | } 77 | 78 | int main() 79 | { 80 | buttonrpc server; 81 | server.as_server(5555); 82 | 83 | server.bind("foo_1", foo_1); 84 | server.bind("foo_2", foo_2); 85 | server.bind("foo_3", std::function(foo_3)); 86 | server.bind("foo_4", foo_4); 87 | server.bind("foo_5", foo_5); 88 | 89 | ClassMem s; 90 | server.bind("foo_6", &ClassMem::bar, &s); 91 | 92 | std::cout << "run rpc server on: " << 5555 << std::endl; 93 | server.run(); 94 | 95 | return 0; 96 | } 97 | -------------------------------------------------------------------------------- /Serializer.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * buttonrpc library 4 | * Copyright 2018-04-28 Button 5 | * 6 | */ 7 | 8 | #pragma once 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | using namespace std; 16 | 17 | class StreamBuffer : public vector 18 | { 19 | public: 20 | StreamBuffer(){ m_curpos = 0; } 21 | StreamBuffer(const char* in, size_t len){ 22 | m_curpos = 0; 23 | insert(begin(), in, in+len); 24 | } 25 | ~StreamBuffer(){ } 26 | 27 | void reset(){ m_curpos = 0; } 28 | const char* data(){ return &(*this)[0]; } 29 | const char* current(){ return&(*this)[m_curpos]; } 30 | void offset(int k){ m_curpos += k; } 31 | bool is_eof(){ return (m_curpos >= size()); } 32 | void input( char* in, size_t len){ insert(end(), in, in+len); } 33 | int findc(char c){ 34 | iterator itr = find(begin()+m_curpos, end(), c); 35 | if (itr != end()) 36 | { 37 | return itr - (begin()+m_curpos); 38 | } 39 | return -1; 40 | } 41 | 42 | private: 43 | // 当前字节流位置 44 | unsigned int m_curpos; 45 | }; 46 | 47 | class Serializer 48 | { 49 | public: 50 | Serializer() { m_byteorder = LittleEndian; } 51 | ~Serializer(){ } 52 | 53 | Serializer(StreamBuffer dev, int byteorder=LittleEndian){ 54 | m_byteorder = byteorder; 55 | m_iodevice = dev; 56 | } 57 | 58 | public: 59 | enum ByteOrder { 60 | BigEndian, 61 | LittleEndian 62 | }; 63 | 64 | public: 65 | void reset(){ 66 | m_iodevice.reset(); 67 | } 68 | int size(){ 69 | return m_iodevice.size(); 70 | } 71 | void skip_raw_date(int k){ 72 | m_iodevice.offset(k); 73 | } 74 | const char* data(){ 75 | return m_iodevice.data(); 76 | } 77 | void byte_orser(char* in, int len){ 78 | if (m_byteorder == BigEndian){ 79 | reverse(in, in+len); 80 | } 81 | } 82 | void write_raw_data(char* in, int len){ 83 | m_iodevice.input(in, len); 84 | m_iodevice.offset(len); 85 | } 86 | const char* current(){ 87 | return m_iodevice.current(); 88 | } 89 | void clear(){ 90 | m_iodevice.clear(); 91 | reset(); 92 | } 93 | 94 | template 95 | void output_type(T& t); 96 | 97 | template 98 | void input_type(T t); 99 | 100 | // 直接给一个长度, 返回当前位置以后x个字节数据 101 | void get_length_mem(char* p, int len){ 102 | memcpy(p, m_iodevice.current(), len); 103 | m_iodevice.offset(len); 104 | } 105 | 106 | public: 107 | template 108 | void getv(Serializer& ds, Tuple& t) { 109 | ds >> std::get(t); 110 | } 111 | 112 | template 113 | Tuple get_tuple(std::index_sequence) { 114 | Tuple t; 115 | initializer_list{((getv(*this, t)), 0)...}; 116 | return t; 117 | } 118 | 119 | template 120 | Serializer &operator >> (T& i){ 121 | output_type(i); 122 | return *this; 123 | } 124 | 125 | template 126 | Serializer &operator << (T i){ 127 | input_type(i); 128 | return *this; 129 | } 130 | 131 | private: 132 | int m_byteorder; 133 | StreamBuffer m_iodevice; 134 | }; 135 | 136 | template 137 | inline void Serializer::output_type(T& t) 138 | { 139 | int len = sizeof(T); 140 | char* d = new char[len]; 141 | if (!m_iodevice.is_eof()){ 142 | memcpy(d, m_iodevice.current(), len); 143 | m_iodevice.offset(len); 144 | byte_orser(d, len); 145 | t = *reinterpret_cast(&d[0]); 146 | } 147 | delete [] d; 148 | } 149 | 150 | template<> 151 | inline void Serializer::output_type(std::string& in) 152 | { 153 | int marklen = sizeof(uint16_t); 154 | char* d = new char[marklen]; 155 | memcpy(d, m_iodevice.current(), marklen); 156 | byte_orser(d, marklen); 157 | int len = *reinterpret_cast(&d[0]); 158 | m_iodevice.offset(marklen); 159 | delete [] d; 160 | if (len == 0) return; 161 | in.insert(in.begin(), m_iodevice.current(), m_iodevice.current() + len); 162 | m_iodevice.offset(len); 163 | } 164 | 165 | template 166 | inline void Serializer::input_type(T t) 167 | { 168 | int len = sizeof(T); 169 | char* d = new char[len]; 170 | const char* p = reinterpret_cast(&t); 171 | memcpy(d, p, len); 172 | byte_orser(d, len); 173 | m_iodevice.input(d, len); 174 | delete [] d; 175 | } 176 | 177 | template<> 178 | inline void Serializer::input_type(std::string in) 179 | { 180 | // 先存入字符串长度 181 | uint16_t len = in.size(); 182 | char* p = reinterpret_cast< char*>(&len); 183 | byte_orser(p, sizeof(uint16_t)); 184 | m_iodevice.input(p, sizeof(uint16_t)); 185 | 186 | // 存入字符串 187 | if (len == 0) return; 188 | char* d = new char[len]; 189 | memcpy(d, in.c_str(), len); 190 | m_iodevice.input(d, len); 191 | delete [] d; 192 | } 193 | 194 | template<> 195 | inline void Serializer::input_type(const char* in) 196 | { 197 | input_type(std::string(in)); 198 | } 199 | -------------------------------------------------------------------------------- /buttonrpc.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * buttonrpc library 4 | * Copyright 2018-04-28 Button 5 | * 6 | */ 7 | 8 | #pragma once 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "Serializer.hpp" 17 | 18 | 19 | class Serializer; 20 | 21 | template 22 | struct type_xx{ typedef T type; }; 23 | 24 | template<> 25 | struct type_xx{ typedef int8_t type; }; 26 | 27 | 28 | // 打包帮助模板 29 | template 30 | void package_params_impl(Serializer& ds, const Tuple& t, std::index_sequence) 31 | { 32 | initializer_list{((ds << std::get(t)), 0)...}; 33 | } 34 | 35 | template 36 | void package_params(Serializer& ds, const std::tuple& t) 37 | { 38 | package_params_impl(ds, t, std::index_sequence_for{}); 39 | } 40 | 41 | // 用tuple做参数调用函数模板类 42 | template 43 | decltype(auto) invoke_impl(Function&& func, Tuple&& t, std::index_sequence) 44 | { 45 | return func(std::get(std::forward(t))...); 46 | } 47 | 48 | template 49 | decltype(auto) invoke(Function&& func, Tuple&& t) 50 | { 51 | constexpr auto size = std::tuple_size::type>::value; 52 | return invoke_impl(std::forward(func), std::forward(t), std::make_index_sequence{}); 53 | } 54 | 55 | // 调用帮助类,主要用于返回是否void的情况 56 | template 57 | typename std::enable_if::value, typename type_xx::type >::type 58 | call_helper(F f, ArgsTuple args) { 59 | invoke(f, args); 60 | return 0; 61 | } 62 | 63 | template 64 | typename std::enable_if::value, typename type_xx::type >::type 65 | call_helper(F f, ArgsTuple args) { 66 | return invoke(f, args); 67 | } 68 | 69 | // rpc 类定义 70 | class buttonrpc 71 | { 72 | public: 73 | enum rpc_role{ 74 | RPC_CLIENT, 75 | RPC_SERVER 76 | }; 77 | enum rpc_err_code { 78 | RPC_ERR_SUCCESS = 0, 79 | RPC_ERR_FUNCTIION_NOT_BIND, 80 | RPC_ERR_RECV_TIMEOUT 81 | }; 82 | 83 | // wrap return value 84 | template 85 | class value_t { 86 | public: 87 | typedef typename type_xx::type type; 88 | typedef std::string msg_type; 89 | typedef uint16_t code_type; 90 | 91 | value_t() { code_ = 0; msg_.clear(); } 92 | bool valid() { return (code_ == 0 ? true : false); } 93 | int error_code() { return code_; } 94 | std::string error_msg() { return msg_; } 95 | type val() { return val_; } 96 | 97 | void set_val(const type& val) { val_ = val; } 98 | void set_code(code_type code) { code_ = code; } 99 | void set_msg(msg_type msg) { msg_ = msg; } 100 | 101 | friend Serializer& operator >> (Serializer& in, value_t& d) { 102 | in >> d.code_ >> d.msg_; 103 | if (d.code_ == 0) { 104 | in >> d.val_; 105 | } 106 | return in; 107 | } 108 | friend Serializer& operator << (Serializer& out, value_t d) { 109 | out << d.code_ << d.msg_ << d.val_; 110 | return out; 111 | } 112 | private: 113 | code_type code_; 114 | msg_type msg_; 115 | type val_; 116 | }; 117 | 118 | buttonrpc(); 119 | ~buttonrpc(); 120 | 121 | // network 122 | void as_client(std::string ip, int port); 123 | void as_server(int port); 124 | void send(zmq::message_t& data); 125 | void recv(zmq::message_t& data); 126 | void set_timeout(uint32_t ms); 127 | void run(); 128 | 129 | public: 130 | // server 131 | template 132 | void bind(std::string name, F func); 133 | 134 | template 135 | void bind(std::string name, F func, S* s); 136 | 137 | // client 138 | template 139 | value_t call(std::string name, Params... ps) { 140 | using args_type = std::tuple::type...>; 141 | args_type args = std::make_tuple(ps...); 142 | 143 | Serializer ds; 144 | ds << name; 145 | package_params(ds, args); 146 | return net_call(ds); 147 | } 148 | 149 | template 150 | value_t call(std::string name) { 151 | Serializer ds; 152 | ds << name; 153 | return net_call(ds); 154 | } 155 | 156 | private: 157 | Serializer* call_(std::string name, const char* data, int len); 158 | 159 | template 160 | value_t net_call(Serializer& ds); 161 | 162 | template 163 | void callproxy(F fun, Serializer* pr, const char* data, int len); 164 | 165 | template 166 | void callproxy(F fun, S* s, Serializer* pr, const char* data, int len); 167 | 168 | // 函数指针 169 | template 170 | void callproxy_(R(*func)(Params...), Serializer* pr, const char* data, int len) { 171 | callproxy_(std::function(func), pr, data, len); 172 | } 173 | 174 | // 类成员函数指针 175 | template 176 | void callproxy_(R(C::* func)(Params...), S* s, Serializer* pr, const char* data, int len) { 177 | 178 | using args_type = std::tuple::type...>; 179 | 180 | Serializer ds(StreamBuffer(data, len)); 181 | constexpr auto N = std::tuple_size::type>::value; 182 | args_type args = ds.get_tuple < args_type >(std::make_index_sequence{}); 183 | 184 | auto ff = [=](Params... ps)->R { 185 | return (s->*func)(ps...); 186 | }; 187 | typename type_xx::type r = call_helper(ff, args); 188 | 189 | value_t val; 190 | val.set_code(RPC_ERR_SUCCESS); 191 | val.set_val(r); 192 | (*pr) << val; 193 | } 194 | 195 | // functional 196 | template 197 | void callproxy_(std::function func, Serializer* pr, const char* data, int len) { 198 | 199 | using args_type = std::tuple::type...>; 200 | 201 | Serializer ds(StreamBuffer(data, len)); 202 | constexpr auto N = std::tuple_size::type>::value; 203 | args_type args = ds.get_tuple < args_type > (std::make_index_sequence{}); 204 | 205 | typename type_xx::type r = call_helper(func, args); 206 | 207 | value_t val; 208 | val.set_code(RPC_ERR_SUCCESS); 209 | val.set_val(r); 210 | (*pr) << val; 211 | } 212 | 213 | private: 214 | std::map> m_handlers; 215 | 216 | zmq::context_t m_context; 217 | std::unique_ptr> m_socket; 218 | 219 | rpc_err_code m_error_code; 220 | 221 | int m_role; 222 | }; 223 | 224 | inline buttonrpc::buttonrpc() : m_context(1){ 225 | m_error_code = RPC_ERR_SUCCESS; 226 | } 227 | 228 | inline buttonrpc::~buttonrpc(){ 229 | m_context.close(); 230 | } 231 | 232 | // network 233 | inline void buttonrpc::as_client( std::string ip, int port ) 234 | { 235 | m_role = RPC_CLIENT; 236 | m_socket = std::unique_ptr>(new zmq::socket_t(m_context, ZMQ_REQ), [](zmq::socket_t* sock){ sock->close(); delete sock; sock =nullptr;}); 237 | ostringstream os; 238 | os << "tcp://" << ip << ":" << port; 239 | m_socket->connect (os.str()); 240 | } 241 | 242 | inline void buttonrpc::as_server( int port ) 243 | { 244 | m_role = RPC_SERVER; 245 | m_socket = std::unique_ptr>(new zmq::socket_t(m_context, ZMQ_REP), [](zmq::socket_t* sock){ sock->close(); delete sock; sock =nullptr;}); 246 | ostringstream os; 247 | os << "tcp://*:" << port; 248 | m_socket->bind (os.str()); 249 | } 250 | 251 | inline void buttonrpc::send( zmq::message_t& data ) 252 | { 253 | m_socket->send(data); 254 | } 255 | 256 | inline void buttonrpc::recv( zmq::message_t& data ) 257 | { 258 | m_socket->recv(&data); 259 | } 260 | 261 | inline void buttonrpc::set_timeout(uint32_t ms) 262 | { 263 | // only client can set 264 | if (m_role == RPC_CLIENT) { 265 | m_socket->setsockopt(ZMQ_RCVTIMEO, ms); 266 | } 267 | } 268 | 269 | inline void buttonrpc::run() 270 | { 271 | // only server can call 272 | if (m_role != RPC_SERVER) { 273 | return; 274 | } 275 | while (1){ 276 | zmq::message_t data; 277 | recv(data); 278 | StreamBuffer iodev((char*)data.data(), data.size()); 279 | Serializer ds(iodev); 280 | 281 | std::string funname; 282 | ds >> funname; 283 | Serializer* r = call_(funname, ds.current(), ds.size()- funname.size()); 284 | 285 | zmq::message_t retmsg (r->size()); 286 | memcpy (retmsg.data (), r->data(), r->size()); 287 | send(retmsg); 288 | delete r; 289 | } 290 | } 291 | 292 | // 处理函数相关 293 | 294 | inline Serializer* buttonrpc::call_(std::string name, const char* data, int len) 295 | { 296 | Serializer* ds = new Serializer(); 297 | if (m_handlers.find(name) == m_handlers.end()) { 298 | (*ds) << value_t::code_type(RPC_ERR_FUNCTIION_NOT_BIND); 299 | (*ds) << value_t::msg_type("function not bind: " + name); 300 | return ds; 301 | } 302 | auto fun = m_handlers[name]; 303 | fun(ds, data, len); 304 | ds->reset(); 305 | return ds; 306 | } 307 | 308 | template 309 | inline void buttonrpc::bind( std::string name, F func ) 310 | { 311 | m_handlers[name] = std::bind(&buttonrpc::callproxy, this, func, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); 312 | } 313 | 314 | template 315 | inline void buttonrpc::bind(std::string name, F func, S* s) 316 | { 317 | m_handlers[name] = std::bind(&buttonrpc::callproxy, this, func, s, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); 318 | } 319 | 320 | template 321 | inline void buttonrpc::callproxy( F fun, Serializer* pr, const char* data, int len ) 322 | { 323 | callproxy_(fun, pr, data, len); 324 | } 325 | 326 | template 327 | inline void buttonrpc::callproxy(F fun, S * s, Serializer * pr, const char * data, int len) 328 | { 329 | callproxy_(fun, s, pr, data, len); 330 | } 331 | 332 | template 333 | inline buttonrpc::value_t buttonrpc::net_call(Serializer& ds) 334 | { 335 | zmq::message_t request(ds.size() + 1); 336 | memcpy(request.data(), ds.data(), ds.size()); 337 | if (m_error_code != RPC_ERR_RECV_TIMEOUT) { 338 | send(request); 339 | } 340 | zmq::message_t reply; 341 | recv(reply); 342 | value_t val; 343 | if (reply.size() == 0) { 344 | // timeout 345 | m_error_code = RPC_ERR_RECV_TIMEOUT; 346 | val.set_code(RPC_ERR_RECV_TIMEOUT); 347 | val.set_msg("recv timeout"); 348 | return val; 349 | } 350 | m_error_code = RPC_ERR_SUCCESS; 351 | ds.clear(); 352 | ds.write_raw_data((char*)reply.data(), reply.size()); 353 | ds.reset(); 354 | 355 | ds >> val; 356 | return val; 357 | } 358 | --------------------------------------------------------------------------------