러스트로 만드는 채팅앱

Hyeseong·2023년 5월 11일
0

참고 : https://book.async.rs/tutorial/specification.html

Specification & Getting Started

Specification

채팅은 TCP를 통한 간단한 텍스트 프로토콜을 사용합니다. 프로토콜은 \n으로 구분된 utf-8 메시지로 구성됩니다.

client는 server에 연결하고 login을 첫 번째 라인으로 보냅니다. 이 후 client는 다음 구문을 사용하여 다른 client에 메시지를 보낼 수 있습니다.

login1, login2, ... loginN: message

각 클라이언트는 from login: message 메시지를 수신합니다.

터미널 화면에서 아래와 같이 비슷하게 보일겁니다.

On Alice's computer:   |   On Bob's computer:

> alice                |   > bob
> bob: hello               < from alice: hello
                       |   > alice, bob: hi!
                           < from bob: hi!
< from bob: hi!        |

채팅 서버가 해야하는 중요한 일들 중 하나는 많은 동시 연결을 추적하는 것입니다. 채팅 client의 주요 과제는 동시 발신 메시지, 수신 메시지사용자 입력관리하는 것입니다.

Getting Started

카고를 이용하여 프로젝트를 생성합니다.

$ cargo new a-chat
$ cd a-chat

Cargo.toml파일에 아래 라인을 추가합니다.

[dependencies]
futures = "0.3.0"
async-std = "1"

Writing an Accept Loop

서버 구현을 위한 작업을 하겠습니다.

  • TCP 소켓을 address에 bind고 연결 수락을 시작하는 loop.
#![allow(unused)]

use async_std::{
    io::{BufReader, BufWriter},
    net::{TcpListener, TcpStream, ToSocketAddrs},
    prelude::*,
    task,
};

type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
  • async_std 라이브러리에서 비동기 작업을 수행하기 위한 일부 모듈과 네트워킹 관련 모듈을 가져오는 코드입니다. 이를 통해 러스트에서 비동기 소켓 프로그래밍을 할 수 있습니다.
  1. #![allow(unused)] 는 러스트의 컴파일러에게 컴파일러가 사용하지 않은 변수, 함수 또는 모듈 등이 있어도 경고를 표시하지 않도록 지시하는 디렉티브입니다. 이를 사용하면 코드에 특정 부분에 대해 경고를 표시하지 않아도 되기 때문에, 개발자는 불필요한 경고 메시지를 제거하고 더 깨끗한 코드를 유지할 수 있습니다.

    0.1. use 키워드를 사용하여 다른 crate들의 모듈을 가져올 수 있습니다.

    • extern은 Rust에서 다른 crate에 있는 함수나 데이터를 가져와서 사용하거나, 라이브러리를 링크하는 데 사용되는 예약어입니다. extern crate 문은 이전에 사용되었으며, Rust 2018 Edition 이후로는 더 이상 필요하지 않습니다. Rust 2018 Edition부터는 crate들이 라이브러리로서 불러오지 않고 모듈로서 불러와져서, extern crate 대신
  2. prelude: 표준 라이브러리의 prelude는 자주 사용되는 트레잇(trait)들을 쉽게 사용할 수 있게 해주는 모듈입니다. async_std의 prelude는 주로 비동기 I/O 작업에 필요한 트레잇들을 제공합니다.

    1.1. 자주 사용되는 타입 & 트레잇을 쉽게 사용할 수 있도록 가져오는 모듈.
    Rust 표준 라이브러리에는 std::prelude라는 모듈이 있으며, 이 모듈은 Vec, Option, Result 등과 같은 기본적인 타입들과, ToString, Into, From 등과 같은트레잇들을 내부적으로 가져옵니다. 이렇게 prelude 모듈을 사용하면, 매번 긴 경로를 사용하여 타입이나 트레잇을 가져올 필요 없이, 간단하게 use crate::prelude::*; 구문을 사용하여 필요한 타입이나 트레잇들을 가져올 수 있습니다. 이는 코드의 가독성을 높이고, 개발자의 생산성을 향상시킵니다.

  1. task: 이 모듈은 비동기 작업을 생성하고 실행하는 데 필요한 함수와 트레잇을 제공합니다.

  2. net: 이 모듈은 네트워킹에 필요한 기능들을 제공합니다. 여기서는 TcpListenerToSocketAddrs를 가져왔는데, 이들은 각각 TCP 연결을 수신하고 주소를 소켓 주소로 변환하는 데 사용됩니다.

  3. BufReaderBufWriter: 입력과 출력을 위한 버퍼링 기능을 제공합니다.

  4. type Result<T> = ... 코드를 통해서 type aliasing이 사용되고 있어요.

    5.1. Result: 이것은 러스트에서 에러 처리를 위해 사용되는 enum 타입입니다. Ok(T) 또는 Err(E) 두 가지 값을 가질 수 있으며, 여기서 T는 연산의 성공 결과 타입, E는 실패했을 때의 에러 타입을 나타냅니다. 이 코드에서는 Result 타입에 대한 별칭을 설정하고 있으며, 에러 타입Box<dyn std::error::Error + Send + Sync>로 설정되어 있습니다.

    5.2. Box<dyn std::error::Error + Send + Sync>: 이것은 에 할당된 동적 타입의 에러를 의미합니다. 여기서 SendSync멀티스레딩 환경에서 이 에러 타입이 안전하게 전송(Send)되거나 공유(Sync)될 수 있음을 보장합니다.

    5.3. 이러한 타입 별칭은 코드의 가독성을 높이고, 반복적으로 긴 타입 선언을 작성하는 것을 줄이는데 도움이 됩니다. 이렇게 설정한 Result 타입은 이후 비동기 네트워킹 코드에서 에러를 반환할 때 사용될 것입니다.

이제 서버의 루프를 작성합니다.

async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {
    let listener: TcpListener = TcpListener::bind(addr).await?;
    let mut incoming: async_std::net::Incoming = listener.incoming();
    while let Some(stream) = incoming.next().await {
        let stream: TcpStream = stream?;
        println!("Accepting from: {}", stream.peer_addr()?);
        let _handle: task::JoinHandle<()> = spawn_and_log_error(connection_loop(stream));
    }
    Ok(())
}
  • 이 코드는 TCP 소켓 서버의 메인 루프를 설정하는 함수입니다. 이 함수는 주어진 주소에서 들어오는 TCP 연결을 수신하고, 각 연결에 대해 처리를 수행합니다.
  1. async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()>: accept_loop 함수는 비동기 함수(async fn)로 선언되어 있습니다. 이 함수는 소켓 주소로 변환될 수 있는 어떤 타입의 addr 인자를 받습니다(impl ToSocketAddrs). 이 함수는 Result<()> 타입을 반환하며, 이는 함수가 성공적으로 수행되면 Ok(())를 반환하고, 에러가 발생하면 Err(E)를 반환함을 의미합니다. 여기서 E는 이전에 정의한 Box<dyn std::error::Error + Send + Sync> 타입입니다.

  2. impl ToSocketAddrs : 이 문법은 함수의 매개변수로 특정 트레잇(Trait)을 구현하는 어떤 타입이든 받을 수 있음을 나타냅니다. ToSocketAddrs는 러스트 표준 라이브러리의 트레잇으로, 소켓 주소로 변환될 수 있는 여러 가지 타입들에 대해 구현되어 있습니다.

    즉, accept_loop(addr: impl ToSocketAddrs) 함수는 ToSocketAddrs 트레잇을 구현하는 어떤 타입의 addr도 인자로 받을 수 있습니다. 이는 String, &str, SocketAddr, (IpAddr, u16), (Ipv4Addr, u16), (Ipv6Addr, u16), (str, u16), (String, u16), [SocketAddr; N], &[SocketAddr] 등과 같은 다양한 타입을 인자로 받을 수 있음을 의미합니다.

    2.1. impl 키워드
    2.1.1. 특정 타입에 대해 trait을 구현 할 때

    • impl 키워드는 특정 타입에 대해 트레잇(trait)를 구현하는 데 사용. 예를 들어, 아래 코드는 Display 트레잇을 MyStruct 타입에 대해 구현하고 있습니다.
    struct MyStruct {
      value: i32,
    		}
    
    impl std::fmt::Display for MyStruct {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(f, "MyStruct {{ value: {} }}", self.value)
        }
    }
    

    2.1.2. 특정 타입에 메소드를 추가할 때:
    impl 키워드는 특정 타입에 메소드를 추가하는 데도 사용됩니다. 예를 들어, 아래 코드는 MyStruct 타입에 new라는 메소드를 추가하고 있습니다

    struct MyStruct {
      value: i32,
    }
    
    impl MyStruct {
        fn new(value: i32) -> Self {
            Self { value }
        }
    }
    
  3. let listener = TcpListener::bind(addr).await?;: 이 줄은 주어진 주소에 TCP 소켓을 바인딩합니다. bind 함수는 비동기 함수이므로, .await 키워드를 사용하여 함수의 완료를 기다립니다. 만약 bind 함수가 에러를 반환하면 ? 연산자를 통해 에러를 바로 반환합니다.

    3.1. Q. 만약 await을 사용하지 않으면 Future를 반환함. Future는 Rust에서 비동기 연산을 나타내는 타입입니다. 이 Future는 결과가 준비될 때까지 대기하고, 준비된 결과가져오는 기능을 제공합니다.

    .await을 사용하면, Future의 결과가 준비될 때까지 현재의 비동기 작업을 일시 중지하고 다른 비동기 작업이 실행될 수 있도록 합니다. 그리고 Future의 결과가 준비되면, .await을 사용한 비동기 작업다시 실행되어 결과를 가져옵니다.

    따라서 .await을 사용하지 않고 Future를 그대로 반환하거나 저장하는 것도 가능합니다. 그러나 이 Future의 결과를 가져오려면 어떤 방식으로든 .await을 사용해야 합니다.

    3.2. TcpListener::bind메서드는 비동기 소켓 서버를 작성할 때 사용됩니다. 이 메서드는 소켓 주소를 매개변수로 받아 TcpListener 인스턴스를 바인딩합니다. 이것은 서버가 클라이언트의 연결 요청을 수신할 수 있도록 합니다.

    3.2.1. 매개변수 타입:

    addr: impl ToSocketAddrs - ToSocketAddrs 트레잇을 구현하는 타입. 이것은 소켓 주소를 나타내는 값으로, TcpListener를 바인딩할 위치를 지정합니다. 예를 들어, 문자열 "127.0.0.1:8080" 또는 튜플 ("127.0.0.1", 8080) 등이 될 수 있습니다.

    3.2.2. 반환 타입:
    Result<TcpListener> - 이 메서드는 Result를 반환하는데, 이것은 함수가 성공적으로 수행되면 Ok(TcpListener)를 반환하고, 에러가 발생하면 Err를 반환합니다. TcpListener는 TCP 소켓 서버를 나타내며, 클라이언트의 연결 요청을 수신하는 역할을 합니다.

    따라서, 아래와 같이 bind 메서드를 호출하면, 주어진 주소에 TCP 소켓 서버가 바인딩되고, 이 서버를 나타내는 TcpListener 인스턴스가 반환됩니다.

    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    
  4. ? : 연산자는 에러 처리를 간결하게 표현하는 데 사용됩니다. 이 연산자는 Result 타입의 값을 처리하며, Ok인 경우에는 내부 값을 언랩하여 반환하고, Err인 경우에는 현재 함수에서 바로 에러를 반환합니다.

    4.1. 예시

    fn some_function() -> Result<(), SomeError> {
        let result = could_fail()?;
        // Do something with result
        Ok(())
    }
    

    여기서 could_fail 함수는 Result<T, SomeError>를 반환합니다. ? 연산자는 이 Result 값을 처리하며, could_fail가 성공(Ok)하면 내부 값을 언랩하여 result저장하고, 실패(Err)하면 some_function에서 바로 에러를 반환합니다.

    이렇게 ? 연산자를 사용하면, matchif let을 사용하여 Result를 수동으로 처리하는 번거로움 없이 에러를 간결하게 처리할 수 있습니다. 이는 Rust에서 에러 처리를 간단하면서도 안전하게 만드는 중요한 기능 중 하나입니다.

  5. let mut incoming:async_std::net::Incoming = listener.incoming(); 소켓 서버에서 들어오는 TCP 연결 요청을 처리하는 스트림을 생성합니다.

    listener.incoming()는 들어오는 클라이언트 연결의 무한 스트림을 반환하는데, 이 스트림의 각 항목은 Result<TcpStream> 타입입니다. Result는 성공 시에 Ok(TcpStream)을 반환하고, 연결 시도 중에 오류가 발생하면 Err를 반환합니다. TcpStream은 클라이언트와의 TCP 연결을 나타냅니다.

  6. while let Some(stream) = incoming.next().await { // 3 // TODO } :
    6.1. incoming 스트림에서 새로운 항목(여기서는 TCP 연결)을 비동기적으로 가져오는 작업을 반복적으로 수행합니다. 이 작업은 incoming.next().await를 통해 수행되며, 이는 incoming 스트림다음 항목을 비동기적으로 가져옵니다.

    while let Some(stream) = incoming.next().await 구문은 incoming.next().await 호출이 Some 값을 반환하는 한 계속해서 루프를 수행합니다. Some 값은 incoming 스트림에서 새로운 TCP 연결을 성공적으로 가져왔음을 의미합니다. stream 변수에는 이 TCP 연결이 할당됩니다.

    만약 incoming.next().await 호출이 None반환하면, 이는 incoming 스트림에 더 이상 처리할 TCP 연결이 없음을 의미하고, while 루프는 종료됩니다.

    6.2. await 키워드 오른쪽에 코드 블록을 사용하는 경우, async fn에서 Future의 구현체를 직접 만들 때입니다.

    예를 들어, 다음과 같이 async fn에서 Future의 구현체를 만들어서 await 키워드로 실행할 수 있습니다.

    async fn foo() -> i32 {
      let x = async {
          // 비동기적으로 실행되는 코드 블록
          42
      };
    
      x.await
    }
    #[tokio::main]
    async fn main() {
      	let result = foo().await;
      	println!("{}", result);
    	}

    여기서 x는 async 블록이며, 이는 Future의 구현체입니다. x.await는 이 Future가 완료될 때까지 현재 태스크를 블록하고, Future의 결과를 반환합니다.

    이러한 코드 블록은 async fn에서만 사용할 수 있습니다. async fn을 호출한 코드에서 await 키워드를 사용하는 경우에는 코드 블록 대신 Future의 인스턴스를 반환하는 것이 일반적입니다.

  7. let stream: TcpStream = stream?;
    stream 변수에 TcpStream 값을 할당하되, 만약 값이 Err일 경우에는 해당 Err을 리턴한다는 뜻입니다.

  8. println!("Accepting from: {}", stream.peer_addr()?);
    stream.peer_addr() 메서드는 해당 TCP 연결의 소켓 주소를 반환합니다. 이 소켓 주소는 std::net::SocketAddr 형식입니다. println! 매크로에서 {}를 사용하여 해당 소켓 주소를 문자열로 포맷팅하여 출력하고 있습니다. ? 연산자는 이 표현식의 결과를 Result 타입으로 반환하고, 이 표현식에서 오류가 발생한 경우에는 Ok(()) 대신 Err 값을 반환하여 accept_loop 함수에서 예외 처리를 수행하게 됩니다.

  9. let _handle: task::JoinHandle<()>는 비동기적으로 실행될 connection_loop 함수를 호출하는 spawn_and_log_error 함수의 반환값입니다.

    spawn_and_log_error 함수는 제네릭 타입 F를 입력으로 받으며, 이는 Future 트레이트를 구현한 반환값을 가져야 합니다. spawn_and_log_error 함수는 async move 블록으로 구현되어 있으며, 반환값의 타입task::JoinHandle<()> 입니다.

    따라서, let _handle: task::JoinHandle<()>spawn_and_log_error(connection_loop(stream))의 반환값을 할당받는 변수입니다. connection_loop(stream)은 비동기적으로 실행되는 함수이며, 이를 수행하는 핸들러를 _handle 변수에 할당합니다. _handle 변수는 반환값이 없으며, 핸들러를 정상적으로 수행하기 위해 사용됩니다.

  10. Ok(()) : Rust에서 성공적으로 끝난 함수의 반환값

    Rust의 Result 타입은 함수가 성공적으로 완료되거나 (Ok) 또는 오류(Err)로 종료될 수 있음을 나타냅니다. OkErrResult 타입의 두 가지 variant입니다.

    Ok(())에서 괄호 안의 ()는 unit type을 의미하며, 특별한 값이 없음을 나타냅니다. 이것은 함수가 특정 값을 반환하지 않지만 성공적으로 완료되었음을 나타내는 일반적인 방법입니다.

    따라서, Ok(())는 이 함수가 성공적으로 종료되었고 반환할 특별한 값이 없음을 나타냅니다.

// main
fn run() -> Result<()> {
    let fut = accept_loop("127.0.0.1:8080");
    task::block_on(fut)
}
  • Rust에서는 비동기 함수를 호출해도 코드가 실행되지 않는다는 것입니다. 비동기가 아닌 함수에서 퓨처를 실행하는 방법은 퓨처를 executor에게 전달하는 것입니다. 이 경우 task::block_on을 사용하여 현재 스레드에서 future실행하고 완료될 때까지 차단합니다.

Receiving message

프로토콜의 메시지 수신부를 구현해봅시다.

  1. 수신을 TcpStream split하고 \n바이트를 utf-8로 디코딩
  2. 첫 번째 줄을 로그인으로 해석
  3. 나머지 줄을 다음과 같이 구문 분석합니다. login: messa
    use async_std::{
        io::BufReader,
        net::TcpStream,
    };

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)
        }
    })
}

async fn accept_loop(addr: impl ToSocketAddrs) -> Result<()> {
    let listener: TcpListener = TcpListener::bind(addr).await?;
    let mut incoming: async_std::net::Incoming = listener.incoming();
    while let Some(stream) = incoming.next().await {
        let stream: TcpStream = stream?;
        println!("Accepting from: {}", stream.peer_addr()?);
        let _handle: task::JoinHandle<()> = spawn_and_log_error(connection_loop(stream));
    }
    Ok(())
}

async fn connection_loop(mut stream: TcpStream) -> Result<()> {
    let reader: BufReader<&TcpStream> = BufReader::new(&stream);
    let mut writer: BufWriter<&TcpStream> = BufWriter::new(&stream);

    let name: String = match reader.lines().next().await {
        None => Err("peer disconnected immediately")?,
        Some(line) => line?,
    };
    println!("name = {}", name);

    // write response to client
    let response: &str = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
    writer.write_all(response.as_bytes()).await?;
    writer.flush().await?;

    Ok(())
}
  1. spawn_and_log_error 함수는 제네릭 함수로, F라는 타입 파라미터를 가지고 있습니다. 이 함수는 F가 Future trait를 구현하고, 결과값이 Result<()> 타입인 것을 요구합니다. 함수는 task::spawn 함수를 호출하여, async move 블록에서 fut을 실행시킵니다. 만약 fut 실행 중 오류가 발생하면 eprintln!("{}", e) 코드를 실행하여 오류 메시지를 출력합니다. task::spawn 함수는 실행된 태스크의 핸들을 반환합니다. 반환된 핸들은 현재 사용하지 않는 것으로 _handle 변수에 할당됩니다.

  2. task::JoinHandle<()>은 async_std 런타임에서 생성된 future가 실행을 완료한 후 반환하는 결과값이 없는 스레드 핸들을 나타내는 타입입니다.

    좀 더 자세하게 설명하면, task::JoinHandle실행 중인 future의 실행을 추적하고 제어하는 스레드 핸들러입니다. 이 핸들러는 async_std::task::spawn 함수에 의해 반환됩니다.

    반환 타입으로 ()을 사용하면 해당 future의 결과값이 없음을 나타내며, 이는 Result 타입을 사용하여 오류 처리를 수행할 필요가 없음을 의미합니다. 따라서 반환값이 없는 future를 실행하는 경우에는 task::JoinHandle<()>반환하는 것이 일반적입니다

  3. where 키워드제네릭 타입 매개변수제약 조건을 지정하는 데 사용됩니다.

    이 코드에서 FFuture 트레이트를 구현해야 하고, 그 출력은 Result<()> 타입이어야 하며, Send 트레이트를 구현하고, 'static 수명을 가지도록 제한됩니다.

    즉, spawn_and_log_error 함수의 매개변수 F는 Future 트레이트를 구현하며, Result<()> 타입을 출력하는 것이 보장되어야 하며, 다른 스레드로 전송할 수 있도록해야 하며, 수명은 'static이어야 합니다.

    3.1.좀 더 알아보기

    • where 제네릭 타입 매개변수에 대한 제약 조건(constraint)을 명시하는 키워드

    • 제네릭 함수나 제네릭 구조체에서 타입 매개변수제약 조건을 명시할 때 사용

    • where를 사용하여 특정 트레잇을 구현한 타입만 타입 매개변수로 받도록 제약을 걸거나, 타입 매개변수 간의 관계를 명시

      3.2. 다른 예시

      fn foo<T, U>(x: T, y: U) -> T
      where
       T: std::fmt::Debug,
       U: std::fmt::Debug,
      {
       println!("x = {:?}", x);
       println!("y = {:?}", y);
       x
      }
      
      fn main() {
       let s = String::from("hello");
       let n = 42;
       let result = foo(s, n);
       println!("result = {:?}", result);
      }
      
    • foo 함수는 제네릭 타입 매개변수 T와 U를 가지고 있습니다. 하지만 이 함수는 반환값으로 T 타입만을 가집니다.
    • 따라서 T 타입은 std::fmt::Debug trait을 구현해야 합니다. 이 제약조건을 where 키워드를 사용하여 명시했습니다.
    • 함수를 호출할 때 foo(s, n)로 호출하면 T 타입은 String 타입으로 추론됩니다. Stringstd::fmt::Debug trait을 구현하므로 제약조건을 만족합니다. U 타입은 i32로 추론됩니다.3.3. where
      F: Future<Output = Result<()>> + Send + 'static,
      • F: Future<Output = Result<()>>: 이 부분은 제네릭 타입 매개변수 F가 Future 트레이트를 구현해야 한다는 것을 나타냅니다. Future 트레이트는 어떤 비동기 계산을 나타내며, 결국에는 값을 반환합니다. 여기서는 Result<()> 타입을 반환하는 것으로 지정되어 있어요. 이는 성공 시 Ok(())를 반환하거나 에러를 나타내는 Result 타입을 반환함을 의미합니다.
    • + Send: 이 부분은 제네릭 타입 F에 대한 추가적인 트레이트 제약을 나타냅니다. Send 트레이트는 해당 타입이 스레드 간에 안전하게 공유될 수 있음을 의미합니다. 즉, 다른 스레드로 이동시킬 수 있고 동시에 접근할 수 있는 타입을 나타냅니다.

    • 'static: 이 부분은 라이프타임 'static을 의미합니다. 'static프로그램 전체 동안 유지되는 수명을 나타내는데, 여기서는 F 타입이 'static 수명을 갖는다는 것을 명시합니다. 이는 F 타입이 프로그램 전체에서 유효하고 제한 없이 사용될 수 있음을 의미합니다.

    • 따라서, F 타입Future 트레이트를 구현하고 Result<()>반환하며, Send 트레이트만족하고 'static 수명을 갖는 타입이어야 합니다.

    • 질문 : F:Future<Output = Result<()>> + Send 구문에서 Send는 F와 결합하는거야? 'Future<Output = Result<()>> '과 결합하는 거야?

    • 답변 :
      F: Future<Output = Result<()>> + Send 구문에서 SendF결합하는 것입니다. 이는 FSend 트레이트를 구현해야 한다는 것을 의미합니다. F 타입은 Future<Output = Result<()>>과는 별개로 결합하는 것이며, 두 가지 조건을 동시에 충족해야 합니다.

  4. {
          task::spawn(async move {
              if let Err(e) = fut.await {
                  eprintln!("{}", e)
              }
          })
      }
  • 해당 코드는 task::spawn 함수를 사용하여 비동기 작업실행하는 부분입니다.

  • task::spawn 함수는 비동기 클로저비동기 함수를 인자로 받아서 백그라운드에서 실행합니다.
    async move { ... }은 비동기 클로저를 정의하는 부분입니다. move 키워드클로저외부변수소유할 수 있도록 합니다.
    if let Err(e) = fut.await { ... }fut.await 표현식의 결과가 Err일 경우 실행됩니다. 이 부분은 fut가 완료될 때까지 대기하고, Err 값을 가지면 해당 에러를 출력합니다.
    eprintln!("{}", e)은 에러를 표준 오류 출력에 출력하는 부분입니다.
    즉, 해당 코드는 비동기 클로저task::spawn 함수로 실행하여 비동기 작업을 백그라운드에서 실행하고, 작업이 완료되면 에러가 발생했을 경우 해당 에러를 출력하는 역할을 합니다.

톺아보기-1

러스트 함수

  1. 일반함수(일반 함수(Non-Generic Function)

    • 인자와 반환값을 명시하며 호출 시 타입 파라미터가 사용되지 않음
    • 예시: fn add(a: i32, b: i32) -> i32 { a + b }
  2. 제네릭 함수(Generic Function)

    • 인자나 반환값이 타입 파라미터를 사용하는 함수
    • 예시: fn get_first<T>(list: &[T]) -> Option<&T> { list.first() }
    • 만약 함수의 타입을 정하지 않고 매개변수에만 제네릭 타입을 이용한다면 컴파일에러가 발생함. 이유는 컴파일러는 해당 타입을 알수 없기 때문임.
  3. 클로저(Closure)
    3.1. non-generic closure

    let add_one = |x| x + 1;

    3.2. generic closure

    let map = |arr: &[i32], op: fn(i32) -> i32| -> Vec<i32> {
      arr.iter().map(|&x| op(x)).collect()
    };
    
  4. while let Some(line) = lines.next().await {

    4.1. while let은 루프에서 패턴 매칭을 수행하며, 주어진 패턴에 일치하는 값으로 반복을 진행하거나 종료하는 제어 구조
    4.1.1. 문법

    	while let 패턴 ={
      	// 패턴과 일치하는 경우 실행되는 코드
    }

    4.2. Some(line) = lines.next().awaitlines라는 이터레이터에서 다음 값을 가져오고, 해당 값이 Some이면 패턴 매칭을 통해 값을 분해하여 line 변수에 할당하는 구문입니다.
    4.2.1. Some() : Rust의 Option 열거형의 하나인 Some 변형

  5. let mut reader: BufReader<&TcpStream> = BufReader::new(&stream);
    let mut writer: BufWriter<&TcpStream> = BufWriter::new(&stream);
    :
    5.1. TCP 스트림을 읽고 쓰기 위해 BufReader와 BufWriter를 사용하는 부분입니다.
    let mut reader: BufReader<&TcpStream> = BufReader::new(&stream);
    5.1.1. BufReader버퍼링된 읽기를 지원하는 타입
    5.1.2. BufReader는 주어진 reader(&TcpStream)를 감싸고 버퍼링된 읽기 기능을 제공합니다.
    5.1.3. &streamTcpStream에 대한 불변 참조를 의미합니다. BufReaderTcpStream을 읽기 위해 참조를 사용합니다.
    5.1.4. reader 변수는 BufReader<&TcpStream> 타입으로 선언되어 있습니다. 이는 TcpStream에서 읽기 작업을 수행하기 위한 버퍼링된 리더 객체를 나타냅니다.

톺아보기-2

버퍼링: 데이터를 임시로 저장하는 메모리 영역

특징
1. 입출력 작업을 효율적으로 처리하는 기술
2. 데이터를 읽거나 쓸 때에는 한 번에 작은 블록 단위로 처리하는 것보다 큰 블록 단위로 처리하는 것이 효율적
3. 사용시 데이터를 작은 블록 단위로 입출력 장치와 직접 통신하는 대신, `메모리에 임시로 저장한 후 더 큰 블록 단위로 입출력 장치와 통신할 수 있습니다. 이렇게 함으로써 입출력 장치와의 통신 횟수 줄이고, 메모리와 입출력 장치 간의 속도 차이를 완화할 수 있습니다.
4. 버퍼링은 입출력 작업의 성능을 향상시키고 응용 프로그램의 처리량을 개선하는 데 도움을 줍니다. 또한, 버퍼링은 데이터의 흐름을 관리하여 원활한 입출력 처리를 가능하게 합니다.

  • 예를 들어 파일을 읽을 때 버퍼링을 사용하면 한 번에 더 많은 데이터를 읽어 메모리에 저장한 후 응용 프로그램에서 필요한 만큼의 데이터를 처리할 수 있습니다. 마찬가지로 데이터를 쓸 때에도 버퍼에 쌓아둔 후 한 번에 출력하므로 출력 작업의 효율성을 높일 수 있습니다.

  1. let name: String = match reader.lines().next().await { None => Err("peer disconnected immediately")?, Some(line) => line?, };

    • connection_loop 함수 내부에서 클라이언트로부터 받은 첫 번째 줄을 읽어서 변수 name저장하는 부분입니다.
      6.1. reader.lines().next().awaitreader에서 비동기적으로 한 줄씩 읽어오는 작업입니다. lines()는 BufReader의 메서드로, 스트림을 줄 단위로 읽을 수 있게 해줍니다.
      6.2. next().await는 비동기적으로 다음 줄을 읽어오는 작업입니다. next()lines()에서 반환되는 Stream의 다음 아이템을 가져오는 메서드입니다.
      6.3. match 표현식을 사용하여 결과를 처리합니다. reader.lines().next().await의 결과가 None이면 클라이언트가 즉시 연결을 끊었다는 의미이므로 에러를 반환합니다.
      6.4. Some(line) => line?은 성공적으로 줄을 읽었을 때 해당 줄을 line 변수에 바인딩하고, line?을 사용하여 Result 타입의 값을 얻습니다. 만약 line의 값이 Err일 경우 에러를 반환합니다.
  2. let response: &str = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
    7.1. &str 타입을 사용한 이유.

    • 효율성: &str은 불변한 문자열 슬라이스를 나타내므로 메모리를 효율적으로 사용할 수 있습니다. 응답 본문과 같은 작은 크기의 문자열 데이터에 적합합니다.

    • 문자열 리터럴과의 호환성: &str은 Rust에서 문자열 리터럴의 타입입니다. 따라서 문자열 리터럴을 &str으로 사용하면 쉽게 응답 본문과 같은 문자열을 나타낼 수 있습니다.

    • 스트링 슬라이스의 편의성: &str은 문자열 슬라이스를 나타내므로 다양한 문자열 조작 및 처리 작업에 유용합니다. 문자열의 일부분에 대한 참조를 쉽게 가져올 수 있습니다.

    • 자동 변환: &str은 String과 상호 변환될 수 있습니다. 필요한 경우 String을 &str으로 변환하거나, &str을 String으로 변환할 수 있습니다.

    • 위의 이유들로 인해 &str은 보편적인 문자열 표현 방식으로 많이 사용됩니다.

  3. writer.write_all(response.as_bytes()).await?;

    • 응답을 클라이언트에게 전송하기 위해 writer를 사용하는 부분입니다.

    8.1. writerBufWriter로 생성된 쓰기 인터페이스입니다. BufWriter는 내부적으로 버퍼링을 수행하여 효율적인 쓰기 작업을 도와줍니다.

    8.2. write_all은 비동기적으로 주어진 바이트 슬라이스를 전송하는 메서드입니다. response.as_bytes()response 문자열바이트 슬라이스로 변환하여 전송할 준비를 합니다.

    8.3. await는 비동기 작업이 완료될 때까지 기다리는 키워드입니다. write_all 메서드는 전송 작업이 완료Result 타입을 반환합니다.

    8.4. ?Result 타입의 값을 처리하는 문법으로, 반환된 Result 값을 확인하고, 에러가 발생한 경우 함수에서 바로 에러를 반환합니다.

    8.5. 즉, writer.write_all(response.as_bytes()).await?; 코드는 비동기적으로 응답클라이언트에게 전송하는 작업을 수행합니다. 전송이 성공적으로 완료되면 계속 진행하고, 전송 중에 에러가 발생하면 함수에서 에러를 반환합니다.

  4. writer.flush().await?;: writer를 사용하여 버퍼에 남아있는 데이터비동기적으로 클라이언트에게 전송하는 작업을 수행합니다.

  5. 8, 9의 차이
    writer.write_all(response.as_bytes()).await?;:
    이 코드는 response 문자열을 바이트 슬라이스로 변환한 후, 해당 데이터를 비동기적으로 클라이언트에게 전송합니다.

    write_all 메서드는 버퍼에 데이터를 쓰는 작업이므로, 데이터는 버퍼에 임시로 저장됩니다.
    전송 작업이 완료되기 전까지는 실제로 클라이언트에게 전송되지 않습니다.

    await는 비동기 작업이 완료될 때까지 기다리는 키워드입니다.

    ?는 Result 타입의 값을 처리하는 문법으로, 반환된 Result 값을 확인하고, 에러가 발생한 경우 함수에서 바로 에러를 반환합니다.

    writer.flush().await?;:

    이 코드는 버퍼에 저장된 데이터를 목적지로 비우는 작업을 비동기적으로 수행합니다.

    flush 메서드는 버퍼에 남아있는 데이터를 목적지로 전송합니다. 전송 작업이 완료되고 버퍼가 비워질 때까지 기다립니다.

    따라서, writer.write_all(response.as_bytes()).await?;버퍼에 데이터를 쓰고 전송 작업을 시작하는 반면, writer.flush().await?버퍼에 저장된 데이터를 목적지로 비우는 작업을 수행합니다. 실제 전송flush 메서드를 호출할 때 이루어지며, 버퍼가 비워질 때까지 기다립니다.

Sending Message

Sending을 구현하는 가장 확실한 방법은 각 클라이언트의 TcpStream 쓰기에 대한 각 connection_loop 액세스 권한을 부여하는 것입니다. 그런 식으로 클라이언트는 수신자에게 직접 .write_all()로 메시지를 보낼 수 있습니다.

하지만 Alice가 bob: foo를 보내고 Charley가 bob: bar를 보내면 Bob은 어쩌면 fobaor를 받을 수 있습니다. 소켓을 통해 메시지를 보내려면 여러 시스템 호출이 필요하며이는 두 개의 동시 .write_all이 서로 간섭할 수도 있습니다!

일반적으로 단일 작업만 각 TcpStream에 써야 합니다. 이제 채널을 통해 메시지를 수신하고 소켓에 쓰는 connection_writer_loop 작업을 만들어 보겠습니다. 이 작업은 메시지 직렬화 합니다. Alice와 Charley가 동시에 두 개의 메시지를 Bob에게 보내면 Bob은 메시지가 채널에 도착한 순서대로 메시지를 보게 됩니다.

use futures::channel::mpsc; // 1
use futures::sink::SinkExt;
use std::sync::Arc;

type Sender<T> = mpsc::UnboundedSender<T>; // 2
type Receiver<T> = mpsc::UnboundedReceiver<T>;

async fn connection_writer_loop(
    mut messages: Receiver<String>,
    stream: Arc<TcpStream>, // 3
) -> Result<()> {
    let mut stream = &*stream;
    while let Some(msg) = messages.next().await {
        stream.write_all(msg.as_bytes()).await?;
    }
    Ok(())
}

설명

  1. futures::channel::mpsc : 다중 생산자 단일 소비자(multi-producer, single-consumer) 비동기 채널을 제공합니다.

  2. SinkExt trait는 Sink trait에 대한 확장을 제공합니다. Sink trait는 비동기적으로 요소를 소비하는 타입을 정의하는데 사용됩니다. SinkExt는 Sink를 확장하여 추가적인 유용한 메서드들을 제공합니다.

    SinkExt trait는 다양한 비동기적인 메서드들을 제공하여 요소를 소비하고 처리하는 데 도움을 줍니다. 예를 들면, SinkExt를 통해 요소를 비동기적으로 전송하고 버퍼링하는 기능, 요소를 변환하고 필터링하는 기능, 여러 소비자를 조합하는 기능 등을 사용할 수 있습니다.

    이 경우, SinkExt trait를 사용하여 Sink를 확장하고 비동기적인 작업을 수행하는 메서드들을 사용할 수 있게 됩니다. 따라서 futures::sink::SinkExt를 가져오면 비동기적인 소비자와 관련된 다양한 기능을 활용할 수 있게 됩니다.

  3. use std::sync::Arc;: Arc"Atomic Reference Counting"의 약자로, 다중 스레드 간에 공유되는 데이터를 안전하게 소유하고 참조하는 데 사용되는 스마트 포인터입니다. Arc는 여러 스레드에서 안전하게 공유될 수 있는 Rc (Reference Counting)의 상호 배타적인 버전입니다.

    ArcArc<T> 형태로 사용되며, T는 Arc로 공유되는 타입입니다. Arc데이터의 소유권을 나타내며, 참조자의 수를 추적하여 데이터의 소유권이 필요 없을 때 데이터를 자동으로 정리합니다.

    Arc는 주로 다중 스레드 환경에서 데이터를 안전하게 공유해야 하는 경우 사용됩니다. 다중 스레드 환경에서 Arc로 감싼 데이터를 여러 스레드에서 동시에 접근하고 참조할 수 있으며, 소유권 규칙을 준수하여 안전하게 데이터를 사용할 수 있게 됩니다.

    따라서, use std::sync::Arc;를 통해 std::sync 모듈의 Arc 타입을 가져오면 다중 스레드 환경에서 안전하게 데이터를 공유하기 위한 Arc 스마트 포인터를 사용할 수 있습니다.

스마트 포인터란?

  • 모리 관리를 돕는 타입
  • 포인터와 유사한 동작을 제공하면서도 추가적인 기능보안을 제공하는 래퍼 타입
    • 메모리 소유, 참조 카운팅, 빌림 규칙 등과 관련

특징

  1. 소유권 관리 : 스마트 포인터는 자체적으로 데이터의 소유를 관리합니다. 데이터를 생성하거나 소멸할 때 적절한 시점에 소유권을 이전하거나 해제함으로써 메모리 안전성을 유지합니다.
  2. 빌림 규칙 : 스마트 포인터는 Rust의 빌림 규칙을 적용하여 데이터에 대한 동시 접근을 제한합니다. 빌림 규칙은 컴파일러가 런타임 오류를 방지하기 위해 데이터에 대한 가변 참조자의 수와 라이프타임을 추적합니다.
  3. 추가적인 기능 : 스마트 포인터는 일반적인 포인터보다 많은 기능을 제공합니다. 예를 들어, 메모리 할당 및 해제, 소유권 전달, 참조 카운팅, 스레드 안전성 등을 처리할 수 있습니다.

스마트 포인터 요약 :

  1. Rust에서는 다양한 종류의 스마트 포인터가 제공됩니다. 가장 일반적인 스마트 포인터로는 Box, Rc, Arc, Cell, RefCell, Mutex, Ref, RefMut 등이 있습니다. 각각의 스마트 포인터는 특정한 상황에 사용될 수 있으며, 메모리 관리와 동시성을 다루는 다양한 요구 사항에 맞게 선택하여 사용할 수 있습니다.
  2. 스마트 포인터는 Rust의 안전성과 효율성을 높이는 데 중요한 역할을 합니다. 개발자가 메모리 관리에 대해 직접적인 관여 없이 안전하게 코드를 작성할 수 있도록 도와주며, 메모리 누수나 데드락과 같은 일반적인 문제들을 방지하는 데 도움을 줍니다.

스마트 포인터설명
Box<T>힙(heap)에 데이터를 할당하고 소유하는 가장 간단한 스마트 포인터입니다.
Rc<T>참조 카운팅(reference counting) 스마트 포인터로, 여러 개의 소유자를 허용합니다.
Arc<T>원자적(atomic) 참조 카운팅 스마트 포인터로, 다중 스레드 환경에서 안전하게 공유될 수 있습니다.
Cell<T>내부 가변성(mutable interior)을 제공하여 값을 변경할 수 있는 스마트 포인터입니다. 스레드 간에는 안전하지 않습니다.
RefCell<T>내부 가변성(mutable interior)을 제공하여 값을 변경할 수 있는 스마트 포인터입니다. 여러 개의 소유자를 허용합니다.
Mutex<T>동시 접근을 제어하기 위해 스레드 간에 안전하게 데이터에 상호 배타적인 접근을 제공하는 스마트 포인터입니다.

스마트 포인터 사용 예시

  1. Box<T>:
fn main() {
    let my_box: Box<i32> = Box::new(42);
    println!("Value: {}", *my_box);
}

1.1. let my_box: Box<i32> = Box::new(42);:

  • Box::new(42)를 사용하여 에 정수 42를 할당하고, my_box라는 변수에 Box<i32> 타입으로 바인딩합니다.
    Box 스마트 포인터는 데이터를 힙에 할당하고, 해당 데이터의 소유권을 갖습니다.
    println!("Value: {}", *my_box);:

    *my_box를 사용하여 my_box소유권을 해제하고 힙에 할당된 값을 가져옵니다.
    println! 매크로를 사용하여 값을 출력합니다.
    위 코드는 Box 스마트 포인터를 사용하여 힙에 정수 값을 할당하고 이를 안전하게 소유하며 접근하는 예시입니다. Box 스마트 포인터는 힙에 할당된 데이터의 소유권을 가지고 있기 때문에 메모리 안전성을 보장하면서 힙 데이터에 접근할 수 있습니다.

톺아보기-3

소유권

  • Rust에서 메모리 관리와 생명주기를 제어하는 개념입니다. Rust는 소유권 규칙을 통해 메모리 안전성을 보장하면서 자원의 생성, 소멸 및 이동을 관리합니다. 소유권의 라이프사이클은 다음과 같은 단계로 구성됩니다

  • 소유권의 생성:

    소유권은 값이 생성되면서 처음으로 소유자에게 부여됩니다.
    이때, let 키워드를 사용하여 변수에 값을 할당하거나 스마트 포인터를 생성함으로써 소유권이 생성됩니다.

  • 소유권의 이전 (Transfer):

    Rust에서는 한 번에 하나의 소유자만이 값을 소유할 수 있습니다.
    소유권을 다른 변수나 함수로 이전하는 과정을 소유권의 이전이라고 합니다.
    이때, 이전된 변수는 이후에 사용할 수 없습니다.

  • 소유권의 대여 (Borrowing):

    소유권이 이전된 변수나 함수는 대여자(Borrower)로서 소유권을 갖지 않으면서 값을 참조할 수 있습니다.
    대여자는 불변 참조자(immutable reference) 또는 가변 참조자(mutable reference)를 통해 값을 읽거나 변경할 수 있습니다.

  • 소유권의 소멸:

    소유권이 소멸되는 시점은 소유권을 가진 변수가 스코프를 벗어날 때입니다.
    변수의 스코프가 종료되면, 할당된 메모리는 자동으로 해제되고 리소스가 반환됩니다.
    이때, 스마트 포인터의 소멸자(destructor)가 호출되어 자원의 정리나 추가적인 작업을 수행할 수 있습니다.
    위의 life cycle을 예시 코드로 설명해보겠습니다:

  • 소유권 예시 코드

fn main() {
    let my_box: Box<i32> = Box::new(42); // 소유권의 생성

    {
        let borrowed_value: &i32 = &*my_box; // 소유권의 대여
        println!("Borrowed Value: {}", borrowed_value);
    } // borrowed_value가 스코프를 벗어나면서 대여 종료

    // 다른 작업 수행 가능

} // my_box가 스코프를 벗어나면서 소유권의 소멸 및 메모리 해제
  
  • &*my_box :
    1) * 연산자를 사용하여 my_box소유한 힙에 저장된 값을 가져옵니다. 따라서 *my_box는 힙에 저장된 i32 값에 접근합니다.

    2) &*my_box: & 연산자를 사용하여 *my_box의 값을 참조하는 불변 참조자(immutable reference)를 생성합니다. 이렇게 생성된 참조자는 my_box의 값에 대한 대여자가 되어 값을 읽을 수 있습니다. 참조자를 통해 my_box값을 변경할 수는 없습니다.`

    3) &*my_box는 my_box가 소유한 힙의 i32 값을 대여하여 참조하는 불변 참조자를 생성합니다. 이렇게 생성된 참조자를 사용하여 값을 읽을 수 있습니다.

  1. Rc<T>:
use std::rc::Rc;

fn main() {
    let shared_value: Rc<i32> = Rc::new(42);
    println!("Value: {}", *shared_value);
}
  • let shared_value: Rc<i32> = Rc::new(42);:
    Rc::new(42)를 사용하여 정수 42를 포인터로 감싸고, shared_value라는 변수에 Rc<i32> 타입으로 바인딩합니다. 이렇게 하면 shared_value가 42 값을 공유하는 스마트 포인터가 됩니다.

    • Rc 스마트 포인터는 참조 카운팅을 사용하여 여러 개의 소유자를 허용합니다. 각 소유자는 Rc의 복사본을 가지고 있으며, 소유자 수에 대한 카운트가 유지됩니다. 카운트가 0이 되면 리소스가 자동으로 해제됩니다.
  • 이 코드는 Rc 스마트 포인터를 사용하여 정수값을 공유합니다. Rc는 여러 소유자가 동일한 데이터를 공유하면서 데이터의 수명을 추적하고 메모리 누수를 방지하는 데 사용됩니다.

  1. Arc<T>:
use std::sync::Arc;
use std::thread;

fn main() {
    let shared_value: Arc<i32> = Arc::new(42);

    let thread1 = thread::spawn({
        let shared_value = Arc::clone(&shared_value);
        move || {
            println!("Thread 1: {}", *shared_value);
        }
    });

    let thread2 = thread::spawn({
        let shared_value = Arc::clone(&shared_value);
        move || {
            println!("Thread 2: {}", *shared_value);
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
}
  • Arc를 사용하여 정수값 42공유하는 예시. 두 개의 스레드가 생성되어 shared_value공유하고 동시에 접근하여 값을 출력합니다.

    • Arc::new(42)를 사용하여 정수 42를 Arc 스마트 포인터로 감싸서 생성합니다.

    • Arc::clone(&shared_value)을 사용하여 shared_value참조 카운트를 증가시키고, 새로운 Arc 포인터를 생성합니다.

      • Arc::clone() 함수에 &shared_value와 같이 참조자(reference)를 전달하는 이유는 Arc 스마트 포인터의 복제(clone) 동작수행하기 위해서입니다.
    • 각 스레드의 클로저에서 Arc를 이동시키고, 클로저 내부에서 *shared_value를 통해 값을 참조하여 출력합니다.

    • thread::spawn()을 통해 스레드를 생성하고, join()을 사용하여 스레드의 실행이 완료될 때까지 기다립니다.

톺아보기-4

참조자와 스마트 포인터

  • 참조자와 스마트 포인터는 Rust에서 데이터에 대한 참조를 제공하는 두 가지 다른 개념입니다.

    • 참조자 (Reference):

      하나. 참조자는 값을 소유하지 않고, 다른 값에 대한 참조를 만듭니다.

      둘. 참조자는 & 기호를 사용하여 생성되며, 변수 또는 값에 대한 불변 또는 가변 참조를 나타냅니다.

      셋. 참조자는 빌림(Borrowing) 개념으로, 데이터의 소유권을 가져가지 않고도 값을 빌려올 수 있습니다.

      넷. 참조자는 스코프를 벗어날 때까지 유효하며, 동일한 데이터에 대한 여러 참조자가 존재할 수 있습니다.

    • 스마트 포인터 (Smart Pointer):

      하나. 스마트 포인터는 값을 소유하고, 값을 가리키는 포인터 역할을 합니다.

      둘. 스마트 포인터는 일반적으로 특정한 동작 또는 소유권 규칙을 갖춘 데이터 구조입니다.

      셋. 스마트 포인터는 주로 메모리 관리, 동시성, 참조 카운팅 등의 작업을 수행하기 위해 사용됩니다.

      넷. Rust에서의 스마트 포인터에는 Box, Rc, Arc, Cell, RefCell, Mutex 등이 있습니다.

  1. Cell<T>:
use std::cell::Cell;

fn main() {
    let my_cell: Cell<i32> = Cell::new(42);
    let value = my_cell.get();
    println!("Value: {}", value);

    my_cell.set(24);
    let new_value = my_cell.get();
    println!("New Value: {}", new_value);
}
  • Cell<T>은 내부 값을 변경 가능한 셀을 제공하는 스마트 포인터 타입입니다

  • my_cellCell<i32> 타입으로 생성되었고, 초기값으로 42가 설정되었습니다. my_cell.get()을 사용하여 셀의 값을 가져와서 value 변수에 할당하고, 그 값을 출력합니다. 그리고 my_cell.set(24)를 사용하여 셀의 값을 변경하여 24로 설정한 후, my_cell.get()을 사용하여 변경된 값을 가져와서 new_value 변수에 할당하고, 그 값을 출력합니다.

  1. RefCell<T>:
use std::cell::RefCell;

fn main() {
    let my_ref_cell: RefCell<i32> = RefCell::new(42);
    let value = my_ref_cell.borrow();
    println!("Value: {}", *value);

    *my_ref_cell.borrow_mut() = 24;
    let new_value = my_ref_cell.borrow();
    println!("New Value: {}", *new_value);
}
  • 이 코드는 RefCell<i32>를 사용하여 가변성을 런타임에 관리합니다. RefCell불변 참조(borrow())가변 참조(borrow_mut())를 제공하므로, 같은 스코프 내에서도 해당 데이터에 대해 가변 참조를 가질 수 있습니다.

    • RefCell에 42를 저장합니다.
    • 42를 읽어서 출력합니다.
    • 42를 24로 변경합니다.
    • 24를 읽어서 출력합니다.
  • 이렇게 RefCell을 사용하면 컴파일 시점이 아닌 런타임 시점에 데이터의 가변성을 관리할 수 있습니다. 이는 Rust의 소유권 모델에서 일반적으로 허용되지 않는 동작을 가능하게 하지만, RefCell은 런타임에 borrow 규칙을 체크하여 안전성을 보장합니다. 이러한 유연성 때문에 RefCell은 여러 공유 상태를 가진 복잡한 데이터 구조를 다루는 데 유용합니다.

  1. Mutex<T>:
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let shared_value: Arc<Mutex<i32>> = Arc::new(Mutex::new(42));

    let thread1 = thread::spawn({
        let shared_value = Arc::clone(&shared_value);
        move || {
            let mut value = shared_value.lock().unwrap();
            *value += 10;
            println!("Thread 1: {}", *value);
        }
    });

    let thread2 = thread::spawn({
        let shared_value = Arc::clone(&shared_value);
        move || {
            let mut value = shared_value.lock().unwrap();
            *value -= 5;
            println!("Thread 2: {}", *value);
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
}
  • 이 코드는 두 개스레드를 사용하여 공유 상태를 동시에 업데이트하는 예제입니다. Rust의 std::sync::{Arc, Mutex}와 std::thread를 사용하여 공유 상태를 안전하게 처리하고 있습니다.
  • 하나. Arc<Mutex<i32>> 타입의 shared_value를 생성하고 초기값으로 42를 설정합니다. Arc는 Atomic Reference Counting을 사용하여 여러 스레드 간에 안전하게 공유할 수 있는 참조 카운팅 포인터입니다. Mutex는 상호 배제를 위한 동기화 프리미티브로, 한 번에 하나의 스레드만 데이터에 액세스할 수 있게 합니다.

  • 둘. 첫 번째 스레드(thread1)를 생성하고 실행합니다. Arc::clone(&shared_value)를 통해 shared_value의 참조 카운터를 증가시키고, 이를 새로운 스레드에서 사용할 수 있게 합니다. 스레드는 shared_value의 Mutex를 잠그고(lock()), 성공적으로 잠금을 획득하면 값을 10만큼 증가시키고 출력합니다.

  • 셋. 두 번째 스레드(thread2)생성하고 실행합니다. thread1과 마찬가지로 Arc::clone(&shared_value)를 사용하여 참조 카운터를 증가시키고, 스레드에서 사용할 수 있게 합니다. 스레드는 shared_value의 Mutex를 잠그고(lock()), 성공적으로 잠금을 획득하면 값을 5만큼 감소시키고 출력합니다.

  • 넷. join().unwrap()을 사용하여 두 스레드가 모두 완료될 때까지 기다립니다. 이렇게 하면 모든 스레드 작업이 완료되기 전에 메인 스레드가 종료되지 않도록 합니다.

톺아보기-5

let shared_value: Arc<Mutex<i32>> = Arc::new(Mutex::new(42));
  • 이 코드에서는 두 가지 주요 개념, 즉 ArcMutex가 사용됩니다.

  • Arc (Atomic Reference Counting): 이는 참조 카운팅 포인터로, 여러 스레드간에 안전하게 공유될 수 있습니다. 참조 카운터는 Arc의 복사본이 생성될 때마다 증가하고, 복사본이 drop될 때마다 감소합니다. 참조 카운터가 0이 되면, Arc는 자신이 소유하는 메모리를 정리(cleanup)합니다.

  • Mutex (Mutual Exclusion): 이는 상호 배제를 제공하는 동기화 프리미티브입니다. Mutex는 한 번에 하나의 스레드만데이터에 액세스하도록 보장합니다. 이는 데이터 레이스(data race)를 방지하는 데 사용되며, 두 개 이상의 스레드가 동시에 동일한 데이터를 변경하려고 할 때 발생할 수 있는 문제를 해결합니다.

  • 그런 다음 두 개의 스레드가 생성됩니다. 각 스레드는 Arc::clone(&shared_value)를 통해 shared_value에 대한 참조를 복제(clone)합니다. 이렇게 하면 각 스레드가 shared_value에 독립적으로 액세스할 수 있습니다.

  • 스레드 내부에서는 lock().unwrap() 메서드를 사용하여 Mutex의 잠금을 획득하려고 시도합니다. 이 메서드는 두 가지 가능한 결과를 반환합니다:

    • 잠금을 성공적으로 획득한 경우: 이는 Mutex가 현재 다른 스레드에 의해 잠겨있지 않음을 의미합니다. 이 경우, 해당 스레드는 Mutex가 보호하는 데이터에 액세스할 수 있습니다.

    • 잠금획득하지 못한 경우: 이는 Mutex가 현재 다른 스레드에 의해 잠겨있음을 의미합니다. 이 경우, lock() 메서드는 현재 스레드를 블록(block)하여, 잠금을 획득할 수 있을 때까지 대기하게 합니다.

  • unwrap() 메서드는 Result 타입을 처리하는 데 사용되며, 이는 lock() 메서드가 실패하면 패닉(즉, 프로그램 종료)을 유발합니다.

  • join().unwrap() 메서드는 각 스레드가 완료될 때까지 메인 스레드가 대기하도록 합니다. join() 메서드Result 타입을 반환하는데, 이는 스레드가 패닉 상태에서 종료된 경우 Err를 반환합니다. unwrap()는 이 Result를 처리하며, Err인 경우 프로그램을 패닉 상태로 만듭니다.

  • 스레드가 완료되면 Mutex의 잠금이 자동으로 해제되고, 다른 스레드가 잠금을 획득할 수 있게 됩니다. 이렇게 하면 여러 스레드가 동시에 동일한 데이터에 액세스하려고 할 때 발생할 수 있는 데이터 레이스 조건을 방지합니다.

  • 이러한 방식으로, Rust의 Arc와 Mutex는 여러 스레드에서 공유되는 데이터에 대한 동시 액세스를 안전하게 관리합니다. 이는 Rust의 메모리 안전성 보장에 중요한 역할을 합니다. 또한, 이를 통해 스레드 간에 데이터를 안전하게 공유하고 동기화하는 복잡한 작업을 수행할 수 있습니다.

아래 코드 설명중이었음

use futures::channel::mpsc; // 1
use futures::sink::SinkExt;
use std::sync::Arc;

type Sender<T> = mpsc::UnboundedSender<T>; // 2
type Receiver<T> = mpsc::UnboundedReceiver<T>;

async fn connection_writer_loop(
    mut messages: Receiver<String>,
    stream: Arc<TcpStream>, // 3
) -> Result<()> {
    let mut stream = &*stream;
    while let Some(msg) = messages.next().await {
        stream.write_all(msg.as_bytes()).await?;
    }
    Ok(())
}
  1. type Sender<T> = mpsc::UnboundedSender<T>; type Receiver<T> = mpsc::UnboundedReceiver<T>;

4.1. mpsc::UnboundedSender<T>mpsc::UnboundedReceiver<T>futures 라이브러리의 mpsc (multi-producer, single-consumer) 채널을 나타냅니다. 이것은 여러 생성자(데이터를 보내는 스레드)와 단일 소비자(데이터를 받는 스레드)가 있는 비동기 메시지 패싱 채널입니다.

4.2. Sender<T> = mpsc::UnboundedSender<T>;type Receiver<T> = mpsc::UnboundedReceiver<T>; 는 코드를 간결하게 만드는 편의성을 제공하는 타입 별칭입니다. 이를 통해 mpsc::UnboundedSender<T>mpsc::UnboundedReceiver<T> 대신 간단히 Sender<T>Receiver<T>를 사용할 수 있습니다.

4.3. UnboundedSender<T>UnboundedReceiver<T>"unbounded" 채널을 나타냅니다. 이는 채널이 버퍼링된 메시지의 수에 대한 상한선이 없음을 의미합니다. 이런 종류의 채널은 메시지가 전송되는 속도가 수신되는 속도보다 빠를 때 유용하지만, 메모리 사용에 주의해야 합니다. 메시지를 무한히 보낼 수 있기 때문에, 메시지를 소비하는 속도가 충분히 빠르지 않으면 메모리 부족이 발생할 수 있습니다.

  1. 함수 선언:

    5.1. async fn connection_writer_loop(mut messages: Receiver<String>, stream: Arc<TcpStream>) -> Result<()>

    5.2. 이 함수는 비동기적으로 실행되며, 두 개의 인자를 받습니다: 메시지를 받는 Receiver와 TcpStream에 대한 Arc(Atomic Reference Counting) 포인터. 이 함수는 Result<()>를 반환합니다.

  2. 스트림 레퍼런스 얻기

    6.1. let mut stream = &*stream;
    6.2. 이 코드는 Arc 포인터를 디레퍼런스하여 TcpStream에 대한 뮤터블 참조를 얻습니다.

  3. 메시지 처리 루프:
    7.1. while let Some(msg) = messages.next().await {...}
    7.2. 이 코드는 메시지를 가져와서 처리하는 비동기 루프입니다. Receiver<String>에서 다음 메시지를 가져오는 messages.next().await가 비동기적으로 실행되며, 메시지가 도착하면 Some(msg) 패턴에 바인딩되어 루프 내부로 들어갑니다.

  4. 메시지를 스트림에 쓰기:

    8.1. stream.write_all(msg.as_bytes()).await?;

    8.2. 이 코드는 msg의 내용을 스트림에 비동기적으로 쓰는 작업을 수행합니다. write_all주어진 바이트를 모두 쓸 때까지 계속적으로 호출되며, 작업이 완료되면 메시지의 모든 바이트네트워크 스트림에 쓰여지게 됩니다. 이 작업이 실패하면, 함수는 에러를 반환합니다(? 연산자 때문에).

  5. 함수의 끝:
    9.1. Ok(())
    9.2. 모든 메시지가 처리되고 나면, 함수는 Ok(())를 반환하여 성공적으로 완료되었음을 나타냅니다.

  6. 이 함수는 비동기적으로 동작하므로, 네트워크 I/O 작업을 기다리는 동안 다른 작업을 실행할 수 있습니다. 이는 서버가 여러 클라이언트를 효율적으로 처리할 수 있게 해주는 중요한 특성입니다.

Connecting Readers and Writers

profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글