mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 08:51:08 +03:00
Framework to define services as RESTful HTTP API services. Serde implementations for some core types.
This commit is contained in:
parent
d900f0b934
commit
f64d20749f
17 changed files with 448 additions and 6 deletions
14
api/Cargo.toml
Normal file
14
api/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "grin_api"
|
||||
version = "0.1.0"
|
||||
authors = ["Ignotus Peverell <igno.peverell@protonmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
grin_chain = { path = "../chain" }
|
||||
|
||||
iron = "~0.5.1"
|
||||
log = "~0.3"
|
||||
router = "~0.5.1"
|
||||
serde = "~0.9.10"
|
||||
serde_derive = "~0.9.10"
|
||||
serde_json = "~0.9.8"
|
3
api/rustfmt.toml
Normal file
3
api/rustfmt.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
hard_tabs = true
|
||||
wrap_comments = true
|
||||
write_mode = "Overwrite"
|
65
api/src/endpoints.rs
Normal file
65
api/src/endpoints.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2016 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// pub struct HashID(pub [u8; 32]);
|
||||
//
|
||||
// impl FromStr for HashId {
|
||||
// type Err = ;
|
||||
//
|
||||
// fn from_str(s: &str) -> Result<HashId, > {
|
||||
// }
|
||||
// }
|
||||
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use iron::method::Method;
|
||||
|
||||
use chain::{self, Tip};
|
||||
use rest::*;
|
||||
|
||||
/// ApiEndpoint implementation for the blockchain. Exposes the current chain
|
||||
/// state as a simple JSON object.
|
||||
#[derive(Clone)]
|
||||
pub struct ChainApi {
|
||||
/// data store access
|
||||
chain_store: Arc<chain::ChainStore>,
|
||||
}
|
||||
|
||||
impl ApiEndpoint for ChainApi {
|
||||
type ID = String;
|
||||
type T = Tip;
|
||||
|
||||
fn methods(&self) -> Vec<Method> {
|
||||
vec![Method::Get]
|
||||
}
|
||||
|
||||
fn get(&self, id: String) -> ApiResult<Tip> {
|
||||
self.chain_store.head().map_err(|e| ApiError::Internal(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Start all server REST APIs. Just register all of them on a ApiServer
|
||||
/// instance and runs the corresponding HTTP server.
|
||||
pub fn start_rest_apis(addr: String, chain_store: Arc<chain::ChainStore>) {
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut apis = ApiServer::new("/v1".to_string());
|
||||
apis.register_endpoint("/chain".to_string(), ChainApi { chain_store: chain_store });
|
||||
apis.start(&addr[..]).unwrap_or_else(|e| {
|
||||
error!("Failed to start API HTTP server: {}.", e);
|
||||
});
|
||||
});
|
||||
}
|
29
api/src/lib.rs
Normal file
29
api/src/lib.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2016 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
extern crate grin_chain as chain;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate iron;
|
||||
extern crate router;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
|
||||
mod endpoints;
|
||||
mod rest;
|
||||
|
||||
pub use endpoints::start_rest_apis;
|
275
api/src/rest.rs
Normal file
275
api/src/rest.rs
Normal file
|
@ -0,0 +1,275 @@
|
|||
// Copyright 2016 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! RESTful API server to easily expose services as RESTful JSON/HTTP endpoints.
|
||||
//! Fairly constrained on what the service API must look like by design.
|
||||
//!
|
||||
//! To use it, just have your service(s) implement the ApiEndpoint trait and
|
||||
//! register them on a ApiServer.
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt::{self, Display, Debug, Formatter};
|
||||
use std::io::Read;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::ops::Index;
|
||||
use std::string::ToString;
|
||||
use std::str::FromStr;
|
||||
|
||||
use iron::{Iron, Request, Response, IronResult, IronError, status, headers};
|
||||
use iron::method::Method;
|
||||
use iron::modifiers::Header;
|
||||
use iron::middleware::Handler;
|
||||
use router::Router;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json;
|
||||
|
||||
/// Errors that can be returned by an ApiEndpoint implementation.
|
||||
#[derive(Debug)]
|
||||
pub enum ApiError {
|
||||
Internal(String),
|
||||
Argument(String),
|
||||
}
|
||||
|
||||
impl Display for ApiError {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ApiError::Argument(ref s) => write!(f, "Bad arguments: {}", s),
|
||||
ApiError::Internal(ref s) => write!(f, "Internal error: {}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ApiError {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
ApiError::Argument(_) => "Bad arguments.",
|
||||
ApiError::Internal(_) => "Internal error.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for IronError {
|
||||
fn from(e: ApiError) -> IronError {
|
||||
match e {
|
||||
ApiError::Argument(_) => IronError::new(e, status::Status::BadRequest),
|
||||
ApiError::Internal(_) => IronError::new(e, status::Status::InternalServerError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ApiResult<T> = ::std::result::Result<T, ApiError>;
|
||||
|
||||
/// Trait to implement to expose a service as a RESTful HTTP endpoint. Each
|
||||
/// method corresponds to a specific relative URL and HTTP method following
|
||||
/// basic REST principles:
|
||||
///
|
||||
/// * create: POST /
|
||||
/// * get: GET /:id
|
||||
/// * update: PUT /:id
|
||||
/// * delete: DELETE /:id
|
||||
///
|
||||
/// The methods method defines which operation the endpoint implements, they're
|
||||
/// all optional by default. It also allows the framework to automatically
|
||||
/// define the OPTIONS HTTP method.
|
||||
///
|
||||
/// The type accepted by create and update, and returned by get, must implement
|
||||
/// the serde Serialize and Deserialize traits. The identifier type returned by
|
||||
/// create and accepted by all other methods must have a string representation.
|
||||
pub trait ApiEndpoint: Clone + Send + Sync + 'static {
|
||||
type ID: ToString + FromStr;
|
||||
type T: Serialize + Deserialize;
|
||||
|
||||
fn methods(&self) -> Vec<Method>;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn create(&self, o: Self::T) -> ApiResult<Self::ID> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn delete(&self, id: Self::ID) -> ApiResult<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn update(&self, id: Self::ID, o: Self::T) -> ApiResult<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn get(&self, id: Self::ID) -> ApiResult<Self::T> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper required to define the implementation below, Rust doesn't let us
|
||||
// define the parametric implementation for trait from another crate.
|
||||
struct ApiWrapper<E>(E);
|
||||
|
||||
impl<E> Handler for ApiWrapper<E>
|
||||
where E: ApiEndpoint,
|
||||
<<E as ApiEndpoint>::ID as FromStr>::Err: Debug + Send + Error
|
||||
{
|
||||
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||
match req.method {
|
||||
Method::Get => {
|
||||
let res = self.0.get(extract_param(req, "id")?)?;
|
||||
let res_json = serde_json::to_string(&res)
|
||||
.map_err(|e| IronError::new(e, status::InternalServerError))?;
|
||||
Ok(Response::with((status::Ok, res_json)))
|
||||
}
|
||||
Method::Put => {
|
||||
let id = extract_param(req, "id")?;
|
||||
let t: E::T = serde_json::from_reader(req.body.by_ref())
|
||||
.map_err(|e| IronError::new(e, status::BadRequest))?;
|
||||
self.0.update(id, t)?;
|
||||
Ok(Response::with(status::NoContent))
|
||||
}
|
||||
Method::Delete => {
|
||||
let id = extract_param(req, "id")?;
|
||||
self.0.delete(id)?;
|
||||
Ok(Response::with(status::NoContent))
|
||||
}
|
||||
Method::Post => {
|
||||
let t: E::T = serde_json::from_reader(req.body.by_ref())
|
||||
.map_err(|e| IronError::new(e, status::BadRequest))?;
|
||||
let id = self.0.create(t)?;
|
||||
Ok(Response::with((status::Created, id.to_string())))
|
||||
}
|
||||
_ => Ok(Response::with(status::MethodNotAllowed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_param<ID>(req: &mut Request, param: &'static str) -> IronResult<ID>
|
||||
where ID: ToString + FromStr,
|
||||
<ID as FromStr>::Err: Debug + Send + Error + 'static
|
||||
{
|
||||
|
||||
let id = req.extensions.get::<Router>().unwrap().find(param).unwrap_or("");
|
||||
id.parse::<ID>().map_err(|e| IronError::new(e, status::BadRequest))
|
||||
}
|
||||
|
||||
/// HTTP server allowing the registration of ApiEndpoint implementations.
|
||||
pub struct ApiServer {
|
||||
root: String,
|
||||
router: Router,
|
||||
}
|
||||
|
||||
impl ApiServer {
|
||||
/// Creates a new ApiServer that will serve ApiEndpoint implementations
|
||||
/// under the root URL.
|
||||
pub fn new(root: String) -> ApiServer {
|
||||
ApiServer {
|
||||
root: root,
|
||||
router: Router::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the ApiServer at the provided address.
|
||||
pub fn start<A: ToSocketAddrs>(self, addr: A) -> Result<(), String> {
|
||||
Iron::new(self.router).http(addr).map(|_| ()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Register a new API endpoint, providing a relative URL for the new
|
||||
/// endpoint.
|
||||
pub fn register_endpoint<E>(&mut self, subpath: String, endpoint: E)
|
||||
where E: ApiEndpoint,
|
||||
<<E as ApiEndpoint>::ID as FromStr>::Err: Debug + Send + Error
|
||||
{
|
||||
|
||||
assert_eq!(subpath.chars().nth(0).unwrap(), '/');
|
||||
|
||||
// declare a route for each method actually implemented by the endpoint
|
||||
let route_postfix = &subpath[1..];
|
||||
let root = self.root.clone() + &subpath;
|
||||
for m in endpoint.methods() {
|
||||
let full_path = match m {
|
||||
Method::Get => root.clone() + "/:id",
|
||||
Method::Put => root.clone() + "/:id",
|
||||
Method::Delete => root.clone() + "/:id",
|
||||
Method::Post => root.clone(),
|
||||
_ => panic!(format!("Unsupported method: {}.", m)),
|
||||
};
|
||||
self.router.route(m.clone(),
|
||||
full_path,
|
||||
ApiWrapper(endpoint.clone()),
|
||||
m.to_string() + "_" + route_postfix);
|
||||
}
|
||||
|
||||
// support for the HTTP Options method by differentiating what's on the
|
||||
// root resource vs the id resource
|
||||
let (root_opts, sub_opts) = endpoint.methods().iter().fold((vec![], vec![]),
|
||||
|mut acc, m| {
|
||||
if *m == Method::Post {
|
||||
acc.0.push(m.clone());
|
||||
} else {
|
||||
acc.1.push(m.clone());
|
||||
}
|
||||
acc
|
||||
});
|
||||
self.router.options(root.clone(),
|
||||
move |_: &mut Request| {
|
||||
Ok(Response::with((status::Ok,
|
||||
Header(headers::Allow(root_opts.clone())))))
|
||||
},
|
||||
"option_".to_string() + route_postfix);
|
||||
self.router.options(root.clone() + "/:id",
|
||||
move |_: &mut Request| {
|
||||
Ok(Response::with((status::Ok,
|
||||
Header(headers::Allow(sub_opts.clone())))))
|
||||
},
|
||||
"option_id_".to_string() + route_postfix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use rest::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Animal {
|
||||
name: String,
|
||||
legs: u32,
|
||||
lethal: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestApi;
|
||||
|
||||
impl ApiEndpoint for TestApi {
|
||||
type ID = String;
|
||||
type T = Animal;
|
||||
|
||||
fn methods(&self) -> Vec<Method> {
|
||||
vec![Method::Get]
|
||||
}
|
||||
|
||||
fn get(&self, name: String) -> Result<Animal> {
|
||||
Ok(Animal {
|
||||
name: name,
|
||||
legs: 4,
|
||||
lethal: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn req_chain_json() {
|
||||
let mut apis = ApiServer::new("/v1".to_string());
|
||||
apis.register_endpoint("/animal".to_string(), TestApi);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ authors = ["Ignotus Peverell <igno.peverell@protonmail.com>"]
|
|||
bitflags = "^0.7.0"
|
||||
byteorder = "^0.5"
|
||||
log = "^0.3"
|
||||
serde = "~0.9.10"
|
||||
serde_derive = "~0.9.10"
|
||||
time = "^0.1"
|
||||
|
||||
grin_core = { path = "../core" }
|
||||
|
|
|
@ -25,6 +25,9 @@ extern crate bitflags;
|
|||
extern crate byteorder;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate time;
|
||||
|
||||
extern crate grin_core as core;
|
||||
|
|
|
@ -84,8 +84,8 @@ impl ChainStore for ChainKVStore {
|
|||
}
|
||||
|
||||
fn save_block_header(&self, bh: &BlockHeader) -> Result<(), Error> {
|
||||
self.db.put_ser(
|
||||
&to_key(BLOCK_HEADER_PREFIX, &mut bh.hash().to_vec())[..], bh)
|
||||
self.db.put_ser(&to_key(BLOCK_HEADER_PREFIX, &mut bh.hash().to_vec())[..],
|
||||
bh)
|
||||
}
|
||||
|
||||
fn get_header_by_height(&self, height: u64) -> Result<BlockHeader, Error> {
|
||||
|
|
|
@ -24,7 +24,7 @@ use core::ser;
|
|||
/// blockchain tree. References the max height and the latest and previous
|
||||
/// blocks
|
||||
/// for convenience and the total difficulty.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Tip {
|
||||
/// Height of the tip (max height of the fork)
|
||||
pub height: u64,
|
||||
|
|
|
@ -8,6 +8,8 @@ byteorder = "^0.5"
|
|||
num-bigint = "^0.1.35"
|
||||
rust-crypto = "^0.2"
|
||||
rand = "^0.3"
|
||||
serde = "~0.9.10"
|
||||
serde_derive = "~0.9.10"
|
||||
time = "^0.1"
|
||||
tiny-keccak = "1.1"
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ pub const ZERO_HASH: Hash = Hash([0; 32]);
|
|||
|
||||
/// A hash to uniquely (or close enough) identify one of the main blockchain
|
||||
/// constructs. Used pervasively for blocks, transactions and ouputs.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Hash(pub [u8; 32]);
|
||||
|
||||
impl fmt::Display for Hash {
|
||||
|
|
|
@ -17,9 +17,12 @@
|
|||
//! the related difficulty, defined as the maximum target divided by the hash.
|
||||
|
||||
use byteorder::{ByteOrder, BigEndian};
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use std::ops::Add;
|
||||
|
||||
use bigint::BigUint;
|
||||
use serde::{Serialize, Serializer, Deserialize, Deserializer, de};
|
||||
|
||||
use core::hash::Hash;
|
||||
use ser::{self, Reader, Writer, Writeable, Readable};
|
||||
|
@ -78,3 +81,38 @@ impl Readable<Difficulty> for Difficulty {
|
|||
Ok(Difficulty { num: BigUint::from_bytes_be(&data[..]) })
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Difficulty {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
serializer.serialize_str(self.num.to_str_radix(10).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Difficulty {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Difficulty, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
deserializer.deserialize_i32(DiffVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct DiffVisitor;
|
||||
|
||||
impl de::Visitor for DiffVisitor {
|
||||
type Value = Difficulty;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a difficulty")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where E: de::Error
|
||||
{
|
||||
let bigui = BigUint::parse_bytes(s.as_bytes(), 10).ok_or_else(|| {
|
||||
de::Error::invalid_value(de::Unexpected::Str(s), &"a value number")
|
||||
})?;
|
||||
Ok(Difficulty { num: bigui })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ extern crate crypto;
|
|||
extern crate num_bigint as bigint;
|
||||
extern crate rand;
|
||||
extern crate secp256k1zkp as secp;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate time;
|
||||
extern crate tiny_keccak;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||
authors = ["Ignotus Peverell <igno.peverell@protonmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
grin_api = { path = "../api" }
|
||||
grin_chain = { path = "../chain" }
|
||||
grin_core = { path = "../core" }
|
||||
grin_store = { path = "../store" }
|
||||
|
|
|
@ -32,6 +32,7 @@ extern crate time;
|
|||
extern crate tokio_core;
|
||||
extern crate tokio_timer;
|
||||
|
||||
extern crate grin_api as api;
|
||||
extern crate grin_chain as chain;
|
||||
#[macro_use]
|
||||
extern crate grin_core as core;
|
||||
|
|
|
@ -24,6 +24,7 @@ use futures::{future, Future};
|
|||
use tokio_core::reactor;
|
||||
|
||||
use adapters::{NetToChainAdapter, ChainToNetAdapter};
|
||||
use api;
|
||||
use chain;
|
||||
use chain::ChainStore;
|
||||
use core;
|
||||
|
@ -69,6 +70,9 @@ pub struct ServerConfig {
|
|||
/// Directory under which the rocksdb stores will be created
|
||||
pub db_root: String,
|
||||
|
||||
/// Network address for the Rest API HTTP server.
|
||||
pub api_http_addr: String,
|
||||
|
||||
/// Allows overriding the default cuckoo cycle size
|
||||
pub cuckoo_size: u8,
|
||||
|
||||
|
@ -86,6 +90,7 @@ impl Default for ServerConfig {
|
|||
fn default() -> ServerConfig {
|
||||
ServerConfig {
|
||||
db_root: ".grin".to_string(),
|
||||
api_http_addr: "127.0.0.1:13415".to_string(),
|
||||
cuckoo_size: 0,
|
||||
capabilities: p2p::FULL_NODE,
|
||||
seeding_type: Seeding::None,
|
||||
|
@ -150,6 +155,8 @@ impl Server {
|
|||
|
||||
evt_handle.spawn(server.start(evt_handle.clone()).map_err(|_| ()));
|
||||
|
||||
api::start_rest_apis(config.api_http_addr.clone(), chain_store.clone());
|
||||
|
||||
warn!("Grin server started.");
|
||||
Ok(Server {
|
||||
config: config,
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
//! Provides a connection wrapper that handles the lower level tasks in sending
|
||||
//! or
|
||||
//! receiving data from the TCP socket, as well as dealing with timeouts.
|
||||
//! or receiving data from the TCP socket, as well as dealing with timeouts.
|
||||
|
||||
use std::iter;
|
||||
use std::ops::Deref;
|
||||
|
|
Loading…
Reference in a new issue