使用Rust搭建一個簡單的聊天室(一) | “不得不學的編程語言——Rust”

Rust聊天室

  這篇文章,我將帶大家使用Rust來搭建一個簡單的聊天室。這個聊天室我們分兩部分來實現:服務端和客戶端;

一、服務端

  在服務端,我們要實現監聽端口、接收消息和轉發消息的功能。

1.監聽端口

  在該聊天室項目中,我們採用TCP來完成通信功能。在Rust中我們可以使用標準庫std下面的net模塊中的TcpListener結構來監聽指定端口。

TcpListener的解釋如下:
    一個Tcp Socket服務器,監聽到該端口的任何連接。
TcpListener的使用示例如下:
                
use std::net::TcpListener;//導入TcpListener結構
const LOCAL_HOST : &str = "127.0.0.1:8080";//設置監聽的地址和端口
//創建TcpListener監聽器
let listener = TcpListener.bind(LOCAL_HOST).expect("Failed to create TcpListener");
//將監聽器設置爲非阻塞模式
listener.set_nonblocking(true).expect("Cannot set non-blocking")

  在上面代碼中,我們使用到了TcpListener的bind()方法和set_nonblocking方法,這兩個方法的解釋如下:

pub fn bind<A:ToSocketAddrs>(addr:A) -> Result<TcpListener>: 創建一個連接到傳入的具體地址的TCP監聽器,該方法將返回一個可以接收連接的TCP監聽器。

pub fn set_nonblocking(&self,nonblocking:bool) -> Result<()>: 將TCP流設置爲阻塞或者非阻塞模式。

關於阻塞和非阻塞的概念可以參考這篇博客

  在實現端口監聽對象的創建後,我們需要在服務端使用一個數組來保存所有連接到服務端的客戶端,方便後續的信息轉發。並且在listener監聽到連接後,將客戶端的socket保存到該數組。

let mut clients = vec![];//創建數組
//接受連接
if let Ok((mut socket,address))  = listener.accept(){
	clients.push(socket.try_clone().expect("Failed to clone client"));//向數組插入客戶端對象
}

  在這裏我們用到了TcpListener的accept方法,這個方法的解釋如下:

pub fn accept(&self) -> Result<(TcpStream,SocketAddr)> : 該方法會接收一個新的到服務端TCP監聽器的連接,並返回一個TCP流對象和對應的Socket地址。

  因此,在上述的接收到的socket,即客戶端到服務端的TCP流,插入到數組中,方便後續向多個客戶端轉發消息。

  在完成端口監聽和連接接收後,接下來我們要完成的是服務端的消息接收和消息轉發功能。由於我們的服務端一般都是需要實現同時收發消息的,所以消息的接收和消息的轉發是需要分別在不同的線程內去完成的。例如,我們可以在線程A監聽客戶端發送的消息C,在線程B向其他客戶端轉發這條消息C,那我們在Rust中如何保證消息C能在線程A和B之間傳遞呢?

  Rust的標準庫std中,在同步sync模塊中的高級對象mpsc中一個叫做channel的mpsc隊列。所謂的mpsc就是指multi-producer&single consumer。所以,我們可以在服務端使用channel來作爲消息隊列,對應的負責接收客戶端消息的多個子線程就相當於消息的producer,而負責轉發消息的子線程就相當於consumer。

use std::sync::mpsc;
//創建消息隊列
let (sender,receiver) = mpsc::<String>();
2.接收消息

  在接收到客戶端的連接後,我們除了要將客戶端的流插入到數組外,還需要單獨創建一個子線程,用來監聽這個客戶端發送的消息。

use std::thread;
use std::io::{ErrorKind,Read,Write};
let sd = sender.clone();//複製一個消息隊列的生產者
//創建子線程
thread::spawn(move || loop{
	//創建一個指定大小的信息緩存區
	let mut buffer = vec![0;32];
	//socket是指TCPListener的accept獲取到的連接的客戶端TCP流
	match socket.read_exact(&mut buffer){//讀取TCP流中的消息
		Ok(_) =>{//獲取成功
			let message = buffer.into_iter().take_while(|&x| x!=0).collect::<Vec<_>>();//從緩衝區中讀取信息
			let message = String::from_utf8(message).expect("Invalid utf8 message");//將信息轉換爲utf8格式
			sd.send(message).expect("Failed to send message to receiver");;//將消息發送到消息隊列
		},
		Err(ref err) if err.kind() == ErrorKind::WouldBlock => (),//阻塞錯誤
		Err(_) => {//發生錯誤
			//處理錯誤
			break;//結束線程
		}
	}
	//線程休眠
	thread::sleep(::std::time::Duration::from_millis(100));
});

  首先,我們創建了一個大小爲32的數組來作爲信息的緩衝區,然後使用accept得到的socket來讀取流中的信息。這個socket的類型是TcpStream。

TcpStream : 標準庫std中的net模塊的一種結構;

  通過查看標準庫文檔,可以看到TcpStream這個結構實現了Read這個Trait。在這裏,我們定義了緩衝區的大小爲32,所以我們希望每次都讀滿整個緩衝區,因爲使用了Read這個Trait中的read_exact方法。

fn read_exact(&mut self,buf : &mut [u8]) -> Result<()>:
	該方法從流中讀取了特定的字節數的數據來填滿傳入的參數buf

在這裏創建了一個大小爲32的緩衝區,且由於read_exact這個方法的特性,造成本文實現的程序有一個缺陷:不能傳輸超過32字節數的信息(這將在後續的文章進行改進)

  從上面接收信息的具體程序可以看出,當獲取信息成功時,先對信息進行轉碼,然後使用信道生產者sender將該信息傳送到消息隊列中。

3.轉發消息

  轉發消息功能主要做的工作就是:從消息隊列獲取消息,然後轉發給每一個客戶端。

if let Ok(message) = receiver.try_recv(){//從隊列獲取信息
	let msg = message.clone();
	println!("Message [{}]  is received.",msg);
	//轉發給每一個客戶端
	clients = clients.into_iter().filter_map(|mut client|{
		let mut buffer = message.clone().into_bytes();//將消息放入緩衝區
		buffer.resize(MESSAGE_SIZE,0);
		client.write_all(&buffer).map(|_| client).ok()
	}).collect::<Vec<_>>();
}

  在這裏使用filter_mapcollect這兩個方法,來輔助消息的轉發過程,在這個過程中,將轉發失敗(即下線的客戶端)刪除,避免下次繼續轉發這個客戶端。
同樣地,在這裏還有個小Bug,就是在轉發的過程中,沒有仔細地去判斷信息的來源,從而導致發送該信息的客戶端,同樣地也會接收到這條信息,這將會在後續的文章進行改進

二、客戶端

  在該聊天室的客戶端實現中,客戶端需要完成以下三個功能:連接服務端、發送消息和接收消息。

1.連接服務端

  在服務端建立監聽指定地址的對象時,使用的結構TcpListener,而我們在客戶端中去連接該客戶端時,要使用TcpStream結構來幫助實現。

 TcpStream : 本地與遠程服務端之間的socket對象,可通過connect連接方法或者accept方法生成。

  連接的代碼如下所示:

use std::net::TcpStream;
use std::sync::mpsc::{self};

const LOCAL_HOST : &str = "127.0.0.1:8080";//服務端地址
const MESSAGE_SIZE : usize = 32;//緩衝區大小

let mut client = TcpStream::connect(LOCAL_HOST).expect("Failed to connect");//連接服務端

client.set_nonblocking(true).expect("Failed to intiate non-blocking");//設置爲非阻塞模式

let (sender,receiver) = mpsc::channel::<String>();

  這裏創建了一個消息隊列,該消息隊列主要用於發送信息的功能。

2.發送消息

  發送消息的完成步驟可以分爲兩步:(1)讀取用戶在命令行發送的消息;(2)將該消息發送到服務端
  爲了不影響主線程從命令終端讀取用戶的輸入,消息的發送時放在子線程中,因此前面創建的消息隊列就起到了連接消息發送功能的步驟(1)和步驟(2)的作用。
  讀取用戶輸入的代碼如下:

use std::io::{self};

	loop{//不斷等待從終端讀取信息
		let mut buffer = String::new();
		io::stdin().read_line(&mut buf).expect("Failed to read from stdin");
		let message = buffer.trim().to_string();
		if message == "exit" || sender.send(message).is_err{
			break;
		}
	}

  該代碼還添加了判斷,如果用戶輸入的是“exit”,則表示退出客戶端,或者信息往消息隊列發送消息失敗時退出程序。
  在讀取到了用戶輸入的內容後,並將其輸入到消息隊列,我們就將在子線程中向服務端發送用戶輸入的信息:

use std::sync::mpsc::{self,TryRecvError};

	//從消息隊列接收消息
	match receiver.try_recv(){
		Ok(message) => {//接收成功
			let mut buffer = message.clone().into_bytes();
			buffer.resize(MESSAGE,0);
			client.write_all(&buffer).expect("Failed to write to socket");
		},
		Err(TryRecvError::Empty) => (),
		Err(TryRecvError::Disconnected) => {
			break;
		}
	}
3.接收消息

  客戶端接收服務端轉發的消息的代碼也是運行在子線程,該代碼比較簡單。

        match client.read_exact(&mut buffer){
            Ok(_) =>{
                let message = buffer.into_iter().take_while(|&x| x!=0).collect::<Vec<_>>();
                let message = str::from_utf8(&message).unwrap();
                println!("Message: {:?}",message);
            },
            Err(ref err) if err.kind() == ErrorKind::WouldBlock => (),
            Err(_) =>{
                println!("Connection with server was served");
                break;
            }
        }
三、總結

  本文的實現工程可以訪問該倉庫。接下來,針對上述提出的bug,我將通過一系列文章來解決並完整這個Rust聊天室項目。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章