V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
bli22ard
V2EX  ›  Rust

rust TcpStream 为什么设计读写一体

  •  
  •   bli22ard · 85 天前 · 1758 次点击
    这是一个创建于 85 天前的主题,其中的信息可能已经有所发展或是发生改变。
    fn main(){
        let mut ts1=TcpStream::connect(("127.0.0.1", 6666)).unwrap();
        //读写全都在一起
        ts1.write("hello".as_bytes()).unwrap();
        let mut buf=[0;1024];
        ts1.read(&mut buf).unwrap();
        //这样设计,在一些情况不方便
    
        //1 两个 TcpStream 需要全双工拷贝
        let mut ts2=TcpStream::connect(("127.0.0.1", 6667)).unwrap();
        //这里不得不进行 clone
        let mut ts11=ts1.try_clone().unwrap();
        let mut ts22=ts2.try_clone().unwrap();
        thread::spawn(move||{
           std::io::copy(&mut ts1,&mut ts2).unwrap();
        });
    
        thread::spawn(move||{
            std::io::copy(&mut ts22,&mut ts11).unwrap();
        });
        // 以下两种情况以开发一个 http server 为场景
        //2 当需要将 TcpStream 使用 BufReader  和  BufWriter 封装构造一个结构体给上层使用
        
        struct Req{
            br:BufReader<TcpStream>,
            bw:BufWriter<TcpStream>,
        }
        //3 如果不进行 buf 封装,底层处理也不能使用 buf 进行读取,因为 buf 读取可能会读取超过底层处理的数据的长度,这样底层
        // 只能使用非 buf 方式进行读取,效率就比较低下
        struct Req1{
            ts:TcpStream
        }
        
        
    }
    
    

    目前我能想到的 TcpStream 读写一体,是为了 drop 时候自动关闭 tcp 连接,但是这样确实带来了诸多不便。 同样 BufReader 、BufWriter 在 new 的时候传&TcpStream ,不能生成一个具有所有权的 br 、bw ,会依赖&TcpStream 。TcpStream 为什么不提供一个 getWriter 和 getReader 两个分离的函数呢?

    21 条回复    2024-09-27 23:35:34 +08:00
    nagisaushio
        1
    nagisaushio  
       85 天前 via Android
    可以用 BufReader<Arc<TcpStream>>
    SingeeKing
        2
    SingeeKing  
       85 天前 via iPhone
    BufReader/BufWriter+ cloned TcpStream 有什么问题吗?

    毕竟底层是一个 fd ,就算提供你想要的 get_reader 和 get_writer 底层肯定还是一个 clone ,和自己 clone 相比也没啥提升
    PTLin
        3
    PTLin  
       85 天前
    你的想法也没什么问题。标准库没提供,只能手动 clone ,但是 tokio 提供了你想要的功能。
    https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html#method.split
    bli22ard
        5
    bli22ard  
    OP
       84 天前
    @nagisaushio BufReader 是没问题,但是 BufWriter 我试了下不行。因为 BufWriter<W: ?Sized + Write> 而 Arc<TcpStream> 没有实现 Write 。arc 套 buf 看起来只能实现 Reader 不能实现 Writer 。
    bli22ard
        6
    bli22ard  
    OP
       84 天前
    @SingeeKing 一些资料上说,TcpStream try_clone 采用底层操作系统的 dup (在 Unix 上)或 DuplicateHandle (在 Windows 上)来创建一个新的文件描述符,该描述符指向同一个底层套接字,这样会占用一个文件描述符。另外这样调用 try_clone 不够直观。
    bli22ard
        7
    bli22ard  
    OP
       84 天前
    @PTLin
    @capric tokio 现在还没用, 不过这个 split 够优雅。 看来这个阻塞 io 下没有一个好的方式来处理这个分离的问题。不知道 tokio split 底层具体实现的原理的是什么
    SingeeKing
        8
    SingeeKing  
       84 天前 via iPhone
    @bli22ard 对,它是要 dup 的不然 drop 会出问题;如果真的很在意文件描述符可以自己用 Rc/Arc 包一层
    PTLin
        9
    PTLin  
       84 天前
    别钻牛角尖了,本来 os 上的 socket 就没有可以设定只能只读/只写的接口,介于这个原因标准库才没搞什么像是 tokio 里 split 那种只读只写的结构,和你上一个问的为什么&File 可以读写数据一个理由,就是更贴近 os 端的设计导致的。
    所有什么只能读或者只能写的接口全都是上层语言或者库的抽象,你要想搞什么只读只写自己包一下就完事了,try_clone 在 Linux 就是 dup 系 syscall ,让多个不同 fd 指向同一个 fdtable 里的 file ,操作 clone 出来的新 TcpStream 和你操作原先的没有任何区别,两个指向的都是一个 socket file 。
    PTLin
        10
    PTLin  
       84 天前
    @PTLin 最后有口误,是让不同的 fd 对于的 fdtable 里的条目指向同一个 file 。
    capric
        11
    capric  
       84 天前
    @bli22ard 实现在这里,就是很简单的 Arc 和 clone
    ```rust
    /// Owned read half of a [`TcpStream`], created by [`into_split`].
    ///
    /// Reading from an `OwnedReadHalf` is usually done using the convenience methods found
    /// on the [`AsyncReadExt`] trait.
    ///
    /// [`TcpStream`]: TcpStream
    /// [`into_split`]: TcpStream::into_split()
    /// [`AsyncReadExt`]: trait@crate::io::AsyncReadExt
    #[derive(Debug)]
    pub struct OwnedReadHalf {
    inner: Arc<TcpStream>,
    }

    /// Owned write half of a [`TcpStream`], created by [`into_split`].
    ///
    /// Note that in the [`AsyncWrite`] implementation of this type, [`poll_shutdown`] will
    /// shut down the TCP stream in the write direction. Dropping the write half
    /// will also shut down the write half of the TCP stream.
    ///
    /// Writing to an `OwnedWriteHalf` is usually done using the convenience methods found
    /// on the [`AsyncWriteExt`] trait.
    ///
    /// [`TcpStream`]: TcpStream
    /// [`into_split`]: TcpStream::into_split()
    /// [`AsyncWrite`]: trait@crate::io::AsyncWrite
    /// [`poll_shutdown`]: fn@crate::io::AsyncWrite::poll_shutdown
    /// [`AsyncWriteExt`]: trait@crate::io::AsyncWriteExt
    #[derive(Debug)]
    pub struct OwnedWriteHalf {
    inner: Arc<TcpStream>,
    shutdown_on_drop: bool,
    }

    pub(crate) fn split_owned(stream: TcpStream) -> (OwnedReadHalf, OwnedWriteHalf) {
    let arc = Arc::new(stream);
    let read = OwnedReadHalf {
    inner: Arc::clone(&arc),
    };
    let write = OwnedWriteHalf {
    inner: arc,
    shutdown_on_drop: true,
    };
    (read, write)
    }
    ```
    bli22ard
        12
    bli22ard  
    OP
       83 天前
    @PTLin 本来读写分开不应该占用两个文件描述符,也不需要 dup syscall 。感觉 safe 代码没法实现读写分开有所有权。参考 @capric 尝试用 Arc<TcpStream> ,但是 Write Trait 的 fn write(&mut self, buf: &[u8]) -> Result<usize>; 需要一个 &mut self ,inner 的 Arc 提供不了可变借用。实现这个分离感觉只能 unsafe 代码
    bli22ard
        13
    bli22ard  
    OP
       83 天前
    多次尝试,通过 arc 确实可以实现,因为 impl Write for &TcpStream 有一个这样的实现,它就只需要 &TcpStream 的&mut &TcpStream 就可以了,arc 可以提供&TcpStream 的所有权 所以就可以调用到&TcpStream 实现的 write 方法。

    实现代码如下

    use std::io::{Read, Write};
    use std::net::TcpStream;
    use std::sync::Arc;

    pub struct OwnerWriteTcpStream {
    inner:Arc<TcpStream>
    }

    impl Write for OwnerWriteTcpStream {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
    (&*self.inner).write(buf)
    }

    fn flush(&mut self) -> std::io::Result<()> {
    (&*self.inner).flush()

    }
    }

    pub struct OwnerReadTcpStream {
    inner:Arc<TcpStream>
    }

    impl Read for OwnerReadTcpStream {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
    (&*self.inner).read(buf)
    }
    }

    pub struct OwnerTcpStream(TcpStream);

    impl OwnerTcpStream {
    pub fn new(stream:TcpStream) -> Self {
    OwnerTcpStream(stream)
    }
    pub fn split(self) -> (OwnerReadTcpStream, OwnerWriteTcpStream) {
    let arc_read=Arc::new(self.0);
    let arc_write=arc_read.clone();
    let read=OwnerReadTcpStream{inner: arc_read};
    let write=OwnerWriteTcpStream{inner: arc_write};
    (read,write)
    }
    }

    这个回复 markdown 用不了,不知道为什么
    PTLin
        14
    PTLin  
       83 天前
    @bli22ard 我不是让你用 dup 实现 split ,我是指 dup 之后两个 fd 也是指向的一个 vfs 这个概念,再结合你上个问的&File 问的问题你应该理解为什么有 impl Write for &TcpStream 了吧。
    bli22ard
        15
    bli22ard  
    OP
       83 天前
    @PTLin 有点理解了, 这里如果不是 impl Write for &TcpStream ,那 arc 就没法实现共享 tcpstream 进行写入,&File 可能也是基于这个和 drop 关闭资源考虑吧
    PTLin
        16
    PTLin  
       82 天前
    @bli22ard 关键是你要明白,为什么你 Arc TcpStream 配合对&TcpStream 实现的 Write trait 可以实现 split 以用来实现一个线程读一个线程写抽象。
    是因为对 Linux 来讲,fd 对应的 socket file 或者普通 file 本身就是可以多个线程/进程并发读写的,因为这个能力所以才有了 Rust 可以抽象出来的可 Send TcpStream 以及&TcpStream Write Read ,进而可以通过 Arc TcpStream 实现 split 。
    bli22ard
        17
    bli22ard  
    OP
       82 天前
    @PTLin 在 c 里面,fd 就是个 int , 放到 write 和 read 函数都可以, 不管这个 fd 在哪个线程被并发调用,我之前奇怪的是,很多语言,java 、golang 、c 、都可以将读写分开进行处理,rust 这不能分开太难受了,我要实现一个 http proxy ,需要两个 tcpstream 的 读写在两个线程互相 copy ,所以就有了此贴。有了 OwnerTcpStream 方便了很多。
    不过个人感觉,标准库,不应该去实现 impl Write for &TcpStream ,而是应该给 tcpstream 提供函数可以获取 ReadStream 和 WriteStream 他们两个持有相同值类型 fd ,这样 api 更为友好。最后感谢各位的回复,谢谢大家🙏
    fakeshadow
        18
    fakeshadow  
       81 天前
    讨论设计问题不要从你当前的需求出发,而是要把其他需求也考虑进去。比如你认为标准库应该提供 split ,那么它应该如何实现呢?
    bli22ard
        19
    bli22ard  
    OP
       81 天前
    @fakeshadow 读写在不同线程处理在很多场景都需要啊, 比如代理软件,p2p 共享软件。实现的话, 我能想到的是,ReadStream 和 WriteStream 共同持有底层 fd , 这个 fd 使用 arc 记录引用次数。当 arc drop 时候,close 掉 fd 。ReadStream 和 WriteStream drop 时候,shutdown 掉对应的 read write
    fakeshadow
        20
    fakeshadow  
       81 天前
    @bli22ard Rust 是系统级的语言,除了你说的应用场景还有其他的情况,例如:
    1.没有原子变量的平台,例如某些嵌入式,他们没法使用 Arc
    2.没有堆分配器的平台,这些平台和 1 类有些重合,他们不仅没法使用 Arc ,还无法使用任何依赖堆分配的智能指针。
    3.不希望支付 Arc 开销的应用场景,比如单线程并发读写

    如果标准库只是简单的套 Arc ,那么其 split API 对上面两个应用场景就是毫无价值的,他们还是要自己实现其常见的 split 方法,例如:
    ```
    fn split(stream: &TcpStream) -> (ReadHalf<'_>, WriteHalf<'_> {
    // 常用于栈上协程
    }

    fn split(stream: &Rc<TcpStream>) -> (ReadHalf, WriteHalf) {
    // 常用于单线程
    }
    ```

    你说的应用场景是重要的,但 Rust 标准库的设计不能仅仅关注在某些重要领域而忽视其他的需求。这时候你反观标注库的实现,就会发现对内部可变的文件实现 Read, Write 是一个折中的方案,以上情况都可以简单的利用其满足自己的需求。你说它完美吗?那肯定不是,我相信也会有更好的实现方式。但在更好的设计被提出之前,我觉得标准库的实现是正确的。
    bli22ard
        21
    bli22ard  
    OP
       80 天前
    @fakeshadow 就算嵌入式,不支持 arc , 那同样也可以屏蔽 split ,嵌入式基本不支持 thread ,那标准库还不是 thread 也提供了,我认为嵌入式场景不是设计成读写一体的主要原因
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5514 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 07:51 · PVG 15:51 · LAX 23:51 · JFK 02:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.