rust TcpStream 为什么设计读写一体 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
bli22ard
V2EX    Rust

rust TcpStream 为什么设计读写一体

  •  
  •   bli22ard 2024-09-22 22:0550 +08:00 2582 次点击
    这是一个创建于 453 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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  
       2024-09-22 22:18:33 +08:00 via Android
    可以用 BufReader<Arc<TcpStream>>
    SingeeKing
        2
    SingeeKing  
    PRO
       2024-09-23 00:38:16 +08:00 via iPhone
    BufReader/BufWriter+ cloned TcpStream 有什么问题吗?

    毕竟底层是一个 fd ,就算提供你想要的 get_reader 和 get_writer 底层肯定还是一个 clone ,和自己 clone 相比也没啥提升
    PTLin
        3
    PTLin  
       2024-09-23 10:43:41 +08:00
    你的想法也没什么问题。标准库没提供,只能手动 clone ,但是 tokio 提供了你想要的功能。
    https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html#method.split
    bli22ard
        5
    bli22ard  
    OP
       2024-09-23 21:33:30 +08:00
    @nagisaushio BufReader 是没问题,但是 BufWriter 我试了下不行。因为 BufWriter<W: ?Sized + Write> 而 Arc<TcpStream> 没有实现 Write 。ac 套 buf 看起来只能实现 Reader 不能实现 Writer 。
    bli22ard
        6
    bli22ard  
    OP
       2024-09-23 21:43:57 +08:00
    @SingeeKing 一些资料上说,TcpStream try_clone 采用底层操作系统的 dup (在 Unix 上)或 DuplicateHandle (在 Windows 上)来创建一个新的文件描述符,该描述符指向同一个底层套接字,这样会占用一个文件描述符。另外这样调用 try_clone 不够直观。
    bli22ard
        7
    bli22ard  
    OP
       2024-09-23 21:50:19 +08:00
    @PTLin
    @capric tokio 现在还没用, 不过这个 split 够优雅。 看来这个阻塞 io 下没有一个好的方式来处理这个分离的问题。不知道 tokio split 底层具体实现的原理的是什么
    SingeeKing
        8
    SingeeKing  
    PRO
       2024-09-24 03:41:53 +08:00 via iPhone
    @bli22ard 对,它是要 dup 的不然 drop 会出问题;如果真的很在意文件描述符可以自己用 Rc/Arc 包一层
    PTLin
        9
    PTLin  
       2024-09-24 10:55:18 +08:00
    别钻牛角尖了,本来 os 上的 socket 就没有可以设定只能只读/只写的接口,介于这个原因标准库才没搞什么像是 tokio 里 split 那种只读只写的结构,和你上一个问的为什么&File 可以读写数据一个理由,就是更贴近 os 端的设计导致的。
    所有什么只能读或者只能写的接口全都是上层语言或者库的抽象,你要想搞什么只读只写自己包一下就完事了,try_clone 在 Linux 就是 dup 系 syscall ,让多个不同 fd 指向同一个 fdtable 里的 file ,操作 clone 出来的新 TcpStream 和你操作原先的没有任何区别,两个指向的都是一个 socket file 。
    PTLin
        10
    PTLin  
       2024-09-24 11:00:55 +08:00
    @PTLin 最后有口误,是让不同的 fd 对于的 fdtable 里的条目指向同一个 file 。
    capric
        11
    capric  
       2024-09-24 15:21:56 +08:00
    @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
       2024-09-24 23:10:56 +08:00
    @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
       2024-09-24 23:40:46 +08:00
    多次尝试,通过 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  
       2024-09-25 07:21:12 +08:00
    @bli22ard 我不是让你用 dup 实现 split ,我是指 dup 之后两个 fd 也是指向的一个 vfs 这个概念,再结合你上个问的&File 问的问题你应该理解为什么有 impl Write for &TcpStream 了吧。
    bli22ard
        15
    bli22ard  
    OP
       2024-09-25 15:28:22 +08:00
    @PTLin 有点理解了, 这里如果不是 impl Write for &TcpStream ,那 arc 就没法实现共享 tcpstream 进行写入,&File 可能也是基于这个和 drop 关闭资源考虑吧
    PTLin
        16
    PTLin  
       2024-09-25 19:45:38 +08:00
    @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
       2024-09-25 22:05:59 +08:00
    @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  
       2024-09-26 16:28:59 +08:00
    讨论设计问题不要从你当前的需求出发,而是要把其他需求也考虑进去。比如你认为标准库应该提供 split ,那么它应该如何实现呢?
    bli22ard
        19
    bli22ard  
    OP
       2024-09-27 09:29:41 +08:00
    @fakeshadow 读写在不同线程处理在很多场景都需要啊, 比如代理软件,p2p 共享软件。实现的话, 我能想到的是,ReadStream 和 WriteStream 共同持有底层 fd , 这个 fd 使用 arc 记录引用次数。当 arc drop 时候,close 掉 fd 。ReadStream 和 WriteStream drop 时候,shutdown 掉对应的 read write
    fakeshadow
        20
    fakeshadow  
       2024-09-27 12:56:20 +08:00
    @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
       2024-09-27 23:35:34 +08:00
    @fakeshadow 就算嵌入式,不支持 arc , 那同样也可以屏蔽 split ,嵌入式基本不支持 thread ,那标准库还不是 thread 也提供了,我认为嵌入式场景不是设计成读写一体的主要原因
    关于     帮助文档     自助推广系统         API     FAQ     Solana     2765 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 37ms UTC 02:42 PVG 10:42 LAX 18:42 JFK 21:42
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86