Posted in: 프로그래밍

유챗 봇 만들기 #1 – Tokio 적용

이전 글에서 유챗이 어떤 프로토콜·구조를 가지고 통신을 하는지 알아 보았다. 이번 글에서는 Tokio를 이용해서 프로그램의 기본적인 구조를 짜 볼 것이다. 또한 tokio-tungstenite 라이브러리까지 결합하여 웹소켓 접속까지 다루어 볼 것이다.

Rust 프로젝트 생성

가장 먼저 Rust를 설치해야 한다. Rust는 rustup으로 간단한게 설치 할 수 있다. rustup은 Rust를 사용하기 위해 필요한 툴체인들을 설치하도록 도와주는 공식 프로그램이다. rustup은 https://rustup.rs/ 에서 다운로드 및 설치 할 수 있다.

설치가 완료되었다면 cargo, rustup 등의 프로그램을 사용 할 수 있게 된다. cargo나 rustup가 바로 실행되지 않는다면 쉘(터미널)을 재시작 해야 한다. /usr/bin등이 아닌 /root/.cargo/bin 와 같은 별도의 폴더에 프로그램이 설치되기 때문에 $PATH를 업데이트 해야 하기 때문이다. 쉘을 재시작 하면 정상적으로 $PATH가 다시 읽혀저서 사용이 가능할 것이다. 그래도 안된다면 폴더를 $PATH에 집어넣든 ln -s $/.cargo/bin /usr/local/bin 등으로 적절하게 기존 $PATH에 집어넣든 해서 조치를 취하자.

rustup까지 설정이 완료되었으면 Rust 프로젝트를 위한 작업공간을 만들어야 한다. Rust 프로젝트는 Cargo 라는 패키지 관리 프로그램으로 생성 할 수 있다. (npm 같은것이라고 생각하면 된다) cargo new 또는 cargo init를 사용하면 된다. cargo new 이름 을 실행하면 현재 위치 아래에 새로 이름에 해당하는 폴더가 생길것이다. 이미 있는 폴더에 프로젝트를 만들고 싶다면 해당 폴더안에서 바로 cargo init를 실행해도 된다.

[email protected]:/tmp$ tree uchat-bot
uchat-bot
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

정상적으로 실행 됐다면 위와 같은 파일 구조를 가질것이다. 여기서 Cargo.toml은 프로젝트의 설정 및 의존 라이브러리 지정에 사용된다. 실제 프로그램의 코드는 src/ 폴더 아래에 들어가게 된다. 이미 src/ 폴더 아래에는 main.rs가 존재하는데, src/main.rs 파일이 프로그램의 진입점이 된다.

Tokio 라이브러리 사용

Tokio는 비동기 작업 스케줄링 라이브러리이다. tokio를 이용하면 빠르고 간편하게 프로그램을 비동기로 짤 수 있다. 우선 우리 프로젝트에 tokio를 적용해 보자. 먼저 Cargo.toml을 열어서 [dependencies] 하단에 tokio = { version = “0.3”, features = [“full”, “io-util”, “net”] } 를 추가한다.

[package]
name = "uchat2-bot"
version = "0.1.0"
authors = ["esukmean"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "0.3", features = ["full", "io-util", "net"] }

그 후, cargo build 를 실행해 보면 알아서 tokio 라이브러리를 다운로드 받고 컴파일 하는 rustc의 모습을 볼 수 있게 된다.

라이브러리는 준비되었고, 이제 main.rs를 열어서 코드를 작성해 보자. 

#[tokio::main]
async fn main() {
	println!("test");

	use tokio::prelude::*;
	let mut buf = [0u8; 2048];

	let mut tcp = tokio::net::TcpStream::connect("blog.esukmean.com:80").await.unwrap();
	
	tcp.write("GET / HTTP/1.1\r\nHost:blog.esukmean.com\r\n\r\n".as_bytes()).await;
	println!("rcv {:?}", tcp.read(&mut buf).await);
	println!("rcv data {}", std::str::from_utf8(&buf).unwrap());
}

main.rs의 내용을 위와 같이 채운 후 cargo run 으로 실행해 보면 막 HTTP 요청이 출력 될 것이다. 그렇다면 정상적으로 된 것이다. 기존에 있던 fn main() 을 위와 같은 모양으로 수정하였다. 맨 첫줄인 #[tokio::main] 은 async fn main을 비동기로 바로 실행시켜 주기 위한 일종의 메크로다. 저 첫줄을 통해 지동으로 tokio의 런타임을 초기화하고 async fn main() 을 실행한다.

실행시 나오는 화면

tungstenite 적용

tungstenite라는 라이브러리는 원래 비동기(Future)를 지원하지 않는 웹소켓 라이브러리이다. 그렇기에 비동기 구조인 우리 프로그램에서 tungstenite 라이브러리를 쓰려면 별도의 처리가 필요하다. 별도의 처리를 한 것이 tokio-tungstenite 라이브러리이다. tungstenite라는 라이브러리가 원래 있고, 비동기로 돌려먹기 위해 조금 개량한 버전이 tokio-tungstenite라는것임을 유의하길 바란다.

tokio를 적용할 때와 마찬가지로 Config.toml을 열고 [dependencies] 아래에 tokio-tungstenite = "*" 을 추가한다. futures-util 라이브러리도 추가한다. 해당 라이브러리는 tungstenite 작동시 필요한 trait (일종의 interface)인 Sink (+SinkExt)를 지원해 준다.

[package]
name = "uchat2-bot"
version = "0.1.0"
authors = ["esukmean"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "0.3", features = ["full", "io-util", "net"] }
tokio-tungstenite = "*"
futures-util = "*"

마찬가지로 cargo check 또는 cargo build 등을 실행하면 tokio-tungstenite 라이브러리를 다운로드 하여 컴파일 하는 모습을 볼 수 있다.

시험 작동

라이브러리도 이제 준비가 되었으니 이제 유챗에 붙여보는 작업을 해 보자. 아직까지 프로토콜 구현이나 방 입장 처리, 그 외 필요한 부분들은 전혀 작성을 하지 않았기에 접속이 되는지만 확인해 보도록 한다.

#[tokio::main]
async fn main() {
	let sock = match tokio::net::TcpStream::connect("kr-a-worker1.uchat.io:5050").await {
		Err(_) => panic!("TCP 연결 실패"),
		Ok(v) => v
	};
	let mut ws_stream = match tokio_tungstenite::client_async("ws://kr-a-worker1.uchat.io:5050", sock).await {
		Err(_) => panic!("ws 연결 실패"),
		Ok((ws, _http_resp)) => ws
	};

	use futures_util::*;
	if let Err(_) = ws_stream.send(tokio_tungstenite::tungstenite::Message::binary("j#ZXN1a21lYW4=AR7sEOzREQsBrdAUoSdRHtu18bvG1QtKutf-8\n")).await {
		panic!("")
	}
	
	while let Some(rcv) = ws_stream.next().await {
		let rcv = match rcv {
			Err(_) => panic!("WS 프로토콜 오류 발생"),
			Ok(v) => v
		};
		
		println!("수신: {:?}", String::from_utf8_lossy(&rcv.into_data()));
	}
}

원래라면 처음에 ws_stream.send로 접속 정보를 보낼 때 제대로 정리해서 보내야 하지만.. 아직 유챗 프로토콜쪽 구현은 하지 않았기에 저렇게만 보내려고 한다. 정상적으로 코딩이 되었다면 아래와 같이 실행 될 것이다.

맨 아랫줄에 “WS 프로토콜 오류 발생” 메세지는 아직 우리가 유챗 프로토콜을 전부 구현하지 않아서 생기는 문제이다. 일단 여기까지만 됐으면 당장 유챗하고 통신이 가능하다.  

이제 앞으로는 이렇게 통신된 데이터를 프로그램에서 사용하기 적절하게 1) protocol 처리 코드와, 2) 방 내의 처리를 원할하게 하기 위한 구조체(클래스 느낌)를 만들면 된다. 그 뒤는 말끔하게 다듬는거니까.. Rust가 간단하다면 간단하지만 let mut 기능이나 if let Ok(_)match 등의 생소한 기능이 어느정도 있어서 익숙하지 않을 수 있다. 일단은 잘 모르겠어도 코드를 옮겨와 써보는것을 꼭 추천한다.

답글 남기기

이메일 주소는 공개되지 않습니다.

댓글을 작성하기 위해 아래의 숫자를 입력해 주세요. *