3.教程:使用async-std编写聊天服务(3.8 处理客户端断线连接)

3.8 处理客户端断线连接


       目前,我们只在broker中上添加新的客户端连接。
       这显然是错误的:如果一个对等方关闭了与聊天室的连接,我们不应该再尝试向它发送任何消息。
处理断开连接的一个微妙之处在于,我们可以在读操作的任务或写操作的任务中检测到客户端连接是否连接着。
在这两种情况下,最直接的解决方案就是从管理客户端连接的map中移除客户端连接,但这样做是错误的。
如果读和写操作都失败了,我们将移除客户端连接两次,但是客户端连接可能在两次失败之间又重新连接(读和写都发现客户端断开了,都执行移除是没有问题的,但是读先移除后,客户端又连接上来了,这时写再移除,就会把客户端的重新连接移除掉,这显然是一个问题)!
        为了解决这个问题,我们将只在写入端完成时移除断线客户端连接。如果读端完成,我们将通知写端它也应该停止。
也就是说,我们需要为writer任务添加一个关闭信号的能力。
        一种方法是使用通道接收端Receiver<()>收到关闭消息关闭。不过,还有一个更简单的解决方案,它巧妙地利用了RAII。关闭通道是一个同步事件,因此我们不需要发送关闭消息,只需删除发件人即可。这样,我们静态地保证我们只发布一次关闭,即使我们提前返回via?或者恐慌。
首先,将关闭通道添加到connection_loop:


#[derive(Debug)]
enum Void {} // 1

#[derive(Debug)]
enum Event {
    NewPeer {
        name: String,
        stream: Arc<TcpStream>,
        shutdown: Receiver<Void>, // 2
    },
    Message {
        from: String,
        to: Vec<String>,
        msg: String,
    },
}

async fn connection_loop(mut broker: Sender<Event>, stream: Arc<TcpStream>) -> Result<()> {
    // ...
    let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::<Void>(); // 3
    broker.send(Event::NewPeer {
        name: name.clone(),
        stream: Arc::clone(&stream),
        shutdown: shutdown_receiver,
    }).await.unwrap();
    // ...
}
  1. 为了强制关闭通道不发送消息,我们使用了定义了一个Void的类型。
  2. 我们将关闭通道传递给写操作的协程任务中。
  3. 在读操作中,我们创建一个_shutdown_sender,其唯一目的是被删除。

在connection_writer_loop函数中,我们现在需要在关闭和消息通道中选择接收信息。为此,我们使用select宏:

use futures::{select, FutureExt};

async fn connection_writer_loop(
    messages: &mut Receiver<String>,
    stream: Arc<TcpStream>,
    shutdown: Receiver<Void>, // 1
) -> Result<()> {
    let mut stream = &*stream;
    let mut messages = messages.fuse();
    let mut shutdown = shutdown.fuse();
    loop { // 2
        select! {
            msg = messages.next().fuse() => match msg {
                Some(msg) => stream.write_all(msg.as_bytes()).await?,
                None => break,
            },
            void = shutdown.next().fuse() => match void {
                Some(void) => match void {}, // 3
                None => break,
            }
        }
    }
    Ok(())
}
  1. 我们境加关闭通道作为参数。
  2. 因为使用select宏,我们不能使用while let循环,所以我们将其进一步分解为一个循环。
  3. 在关闭通道分支下,我们使用match void{}作为静态检查的unreachable!().

       另一个问题是,在connection_writer_loop中检测到断开连接的那一刻和实际从对等映射中移除对等映射的那一刻中间,可能会将新消息发送到客户端连接管理map的通道中。为了不完全丢失这些消息,我们将把消息通道返回给broker。这还允许我们建立一个可用不变的消息通道,而且客户端连接管理map要比客户端连接存活更长,并使broker是可靠的。

最终代码是这样的:

use async_std::{
    io::BufReader,
    net::{TcpListener, TcpStream, ToSocketAddrs},
    prelude::*,
    task,
};
use futures::channel::mpsc;
use futures::sink::SinkExt;
use futures::{select, FutureExt};
use std::{
    collections::hash_map::{Entry, HashMap},
    future::Future,
    sync::Arc,
};

type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
type Sender<T> = mpsc::UnboundedSender<T>;
type Receiver<T> = mpsc::UnboundedReceiver<T>;

#[derive(Debug)]
enum Void {}

// main
fn run() -> Result<()> {
    task::block_on(accept_loop("127.0.0.1:8080"))
}

async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {
    let listener = TcpListener::bind(addr).await?;
    let (broker_sender, broker_receiver) = mpsc::unbounded();
    let broker_handle = task::spawn(broker_loop(broker_receiver));
    let mut incoming = listener.incoming();
    while let Some(stream) = incoming.next().await {
        let stream = stream?;
        println!("Accepting from: {}", stream.peer_addr()?);
        spawn_and_log_error(connection_loop(broker_sender.clone(), stream));
    }
    drop(broker_sender);
    broker_handle.await;
    Ok(())
}

async fn connection_loop(mut broker: Sender<Event>, stream: TcpStream) -> Result<()> {
    let stream = Arc::new(stream);
    let reader = BufReader::new(&*stream);
    let mut lines = reader.lines();

    let name = match lines.next().await {
        None => Err("peer disconnected immediately")?,
        Some(line) => line?,
    };
    let (_shutdown_sender, shutdown_receiver) = mpsc::unbounded::<Void>();
    broker.send(Event::NewPeer {
        name: name.clone(),
        stream: Arc::clone(&stream),
        shutdown: shutdown_receiver,
    }).await.unwrap();

    while let Some(line) = lines.next().await {
        let line = line?;
        let (dest, msg) = match line.find(':') {
            None => continue,
            Some(idx) => (&line[..idx], line[idx + 1 ..].trim()),
        };
        let dest: Vec<String> = dest.split(',').map(|name| name.trim().to_string()).collect();
        let msg: String = msg.trim().to_string();

        broker.send(Event::Message {
            from: name.clone(),
            to: dest,
            msg,
        }).await.unwrap();
    }

    Ok(())
}

async fn connection_writer_loop(
    messages: &mut Receiver<String>,
    stream: Arc<TcpStream>,
    shutdown: Receiver<Void>,
) -> Result<()> {
    let mut stream = &*stream;
    let mut messages = messages.fuse();
    let mut shutdown = shutdown.fuse();
    loop {
        select! {
            msg = messages.next().fuse() => match msg {
                Some(msg) => stream.write_all(msg.as_bytes()).await?,
                None => break,
            },
            void = shutdown.next().fuse() => match void {
                Some(void) => match void {},
                None => break,
            }
        }
    }
    Ok(())
}

#[derive(Debug)]
enum Event {
    NewPeer {
        name: String,
        stream: Arc<TcpStream>,
        shutdown: Receiver<Void>,
    },
    Message {
        from: String,
        to: Vec<String>,
        msg: String,
    },
}

async fn broker_loop(events: Receiver<Event>) {
    let (disconnect_sender, mut disconnect_receiver) = // 1
        mpsc::unbounded::<(String, Receiver<String>)>();
    let mut peers: HashMap<String, Sender<String>> = HashMap::new();
    let mut events = events.fuse();
    loop {
        let event = select! {
            event = events.next().fuse() => match event {
                None => break, // 2
                Some(event) => event,
            },
            disconnect = disconnect_receiver.next().fuse() => {
                let (name, _pending_messages) = disconnect.unwrap(); // 3
                assert!(peers.remove(&name).is_some());
                continue;
            },
        };
        match event {
            Event::Message { from, to, msg } => {
                for addr in to {
                    if let Some(peer) = peers.get_mut(&addr) {
                        let msg = format!("from {}: {}\n", from, msg);
                        peer.send(msg).await
                            .unwrap() // 6
                    }
                }
            }
            Event::NewPeer { name, stream, shutdown } => {
                match peers.entry(name.clone()) {
                    Entry::Occupied(..) => (),
                    Entry::Vacant(entry) => {
                        let (client_sender, mut client_receiver) = mpsc::unbounded();
                        entry.insert(client_sender);
                        let mut disconnect_sender = disconnect_sender.clone();
                        spawn_and_log_error(async move {
                            let res = connection_writer_loop(&mut client_receiver, stream, shutdown).await;
                            disconnect_sender.send((name, client_receiver)).await // 4
                                .unwrap();
                            res
                        });
                    }
                }
            }
        }
    }
    drop(peers); // 5
    drop(disconnect_sender); // 6
    while let Some((_name, _pending_messages)) = disconnect_receiver.next().await {
    }
}

fn spawn_and_log_error<F>(fut: F) -> task::JoinHandle<()>
where
    F: Future<Output = Result<()>> + Send + 'static,
{
    task::spawn(async move {
        if let Err(e) = fut.await {
            eprintln!("{}", e)
        }
    })
}

在broker中,我们创建一个通道来获取断开连接的客户端连接及其未送达的消息。
当产生事件通道不再发送事件消息时(即当所有读操作退出时),broker's主循环也将退出。
因为broker本身持有一个disconnection_sender,我们知道disconnections通道不能在main loop中完全释放。
我们将客户端连接的名字和未发送出的消息发送给(happy和not so happy)路径中的disconnections通道。再说一次,我们可以安全地unwrap,因为broker比写操作活得长。

It is not strictly necessary in the current setup, where the broker waits for readers' shutdown anyway.
 However, if we add a server-initiated shutdown (for example, kbd:[ctrl+c] handling), this will be a way for the broker to shutdown the writers.
我们删除客户端连接管理map以关闭writer的消息通道,并确保关闭writer。
在当前的设置中,它并不是绝对必要的,在当前的设置中,代理仍然等待读卡器的关闭。
但是,如果我们添加服务器启动的关闭(例如,kbd:[ctrl+c]处理),这将是代理关闭编写器的一种方式。
最后,我们关闭并释放disconnect通道。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值