前回の続きです。例の電子公告にチャットが実装されましたので、チャット書き込み機能を実装しました。
実験環境など
-
OS : Windows 10, macOS 13.3.1
-
Rust 1.72.0
-
encoding_rs 0.8.23
-
tokio 1.32.0
-
clap 4.4.2
今回のターゲット
「一般社団法人サイバー技術・インターネット自由研究会
インターネット公告システム ホームページ」
telnet://koukoku.shadan.open.ad.jp
リポジトリ
今回実装したものは「Rustel」と名付け、GitHub リポジトリにて公開しています。
GitHub - yotiosoft/rustel at rustel_v2
利用方法
主な使用方法は以下のとおりです。
$ rustel -u [URL] -p [Port Number] -e [Encode (utf8 or sjis)] -i [IP version (4 or 6)]
例の電子公告にアクセスする場合はこんな感じになります。
$ rustel -u koukoku.shadan.open.ad.jp -p 23 -e sjis
実装
実装の全体像についてはリポジトリをご覧いただくとして、今回実装した内容を以下に示します。
チャット書き込みの仕組み
公告の受信が単純な平文での TCP パケットの読み取りなのと同じように、書き込みについても単純に TCP パケットを送るだけです。これはチャットだけでなく、nobody
といったコマンド送信についても同様です。
Rust の std::net::TcpStream
であれば、write()
関数などを使用してメッセージをサーバに対して平文で送れば書き込みが完了します。
TELNET 版の場合は受信時と同様に、チャット書き込み内容についても Shift_JIS で送信する必要があります。手っ取り早いのは UTF-8 で標準入力から書き込み文字列を取得し、 encoding_rs::SHIFT_JIS.encode()
で Shift_JIS のバイナリに変換する方法です。
async fn telnet_write_sjis(stream: &mut WriteHalf<TcpStream>, str: &str) -> Result<(), std::io::Error> {
println!("send: {}", str);
let buf_writer = stream;
let (cow, _, _) = encoding_rs::SHIFT_JIS.encode(str);
buf_writer.write(&cow).await?;
buf_writer.flush().await?;
Ok(())
}
非同期処理化
厄介なのが標準入力からの書き込み受け付けと、電子公告およびチャットの受信・標準出力への出力処理を並行して行わなければならない点です。
通常の std::net::TcpStream
に対する入力では、文字列の入力が完了し Enter キーが押されるまで処理をブロッキングしてしまいます。これでは、受信処理と送信処理をループ内で順に実行させようとしたときに、何か文字列を標準入力に入力(あるいは空の状態で Enter キーを押下)するまで、次の TCP パケットの受信・表示処理が行えません。
そこで今回は tokio による非同期処理を実装し、入力処理・出力処理を別々のスレッドとして実行させています。
具体的には、tokio::net::tcp::stream::TcpStream
を tokio::io::split()
で ReadHalf<TcpStream>
、WriteHalf<TcpStream>
に分割し、それぞれを受信と表示を行う telnet_read()
、入力受付と送信を行う telnet_write()
に渡します。これはもともと一つであった TcpStream を入力・出力に分け、それぞれを別々のスレッドとして同時実行させるための処置です。これらの関数は tokio::spawn()
で非同期タスクとして実行しています。
match TcpStream::connect(address).await {
Ok(stream) => {
println!("Connected to the server!");
let (reader, writer) = tokio::io::split(stream);
// read
let reader = tokio::spawn(telnet_read(reader, encode.clone()));
// write
let writer = tokio::spawn(telnet_input(writer, encode.clone()));
let _ = reader.await?;
writer.abort();
},
Err(e) => println!("Failed to connect: {}", e),
}
これに伴い、受信を行う telnet_read()
の方も修正を加えました。前回は main()
内で受信内容を表示していましたが、今回は受信用スレッド内で完結させるためにtelnet_read()
内にループを設け、受信内容の表示まで行います。
async fn telnet_read_utf8(stream: &mut ReadHalf<TcpStream>) -> Result<Option<String>, std::io::Error> {
let mut buf_reader = BufReader::new(stream);
let buffer = buf_reader.fill_buf().await?;
//println!("Received message: {}", buffer);
if buffer.len() == 0 {
return Ok(None);
}
Ok(Some(buffer.iter().map(|&x| x as char).collect::<String>()))
}
async fn telnet_read_sjis(stream: &mut ReadHalf<TcpStream>) -> Result<Option<String>, std::io::Error> {
let mut buf_reader = BufReader::new(stream);
let buffer = buf_reader.fill_buf().await?;
if buffer[0] == 0 {
return Ok(None);
}
let (cow, _, _) = encoding_rs::SHIFT_JIS.decode(&buffer);
let text = cow.into_owned();
//println!("Received message: {:?} {}", buffer, text);
Ok(Some(text))
}
async fn telnet_read(mut stream: ReadHalf<TcpStream>, encode: Encode) -> Result<(), std::io::Error> {
loop {
let str = match encode {
Encode::UTF8 => telnet_read_utf8(&mut stream).await?,
Encode::SHIFTJIS => telnet_read_sjis(&mut stream).await?,
};
if let Some(str) = str {
print!("{}", str);
std::io::stdout().flush()?;
if str == "\0" {
break;
}
// is buffer contains EOF?
if str.contains("\u{1a}") {
break;
}
}
else {
break;
}
};
Ok(())
}
送信用スレッドで実行する telnet_input()
も同様に、関数内にループを設けて入力の受付と送信を行います。
async fn telnet_write(stream: &mut WriteHalf<TcpStream>, encode: &Encode, str: &str) -> Result<(), std::io::Error> {
match encode {
Encode::UTF8 => telnet_write_utf8(stream, str).await,
Encode::SHIFTJIS => telnet_write_sjis(stream, str).await,
}
}
async fn telnet_input(mut stream: WriteHalf<TcpStream>, encode: Encode) -> Result<(), std::io::Error> {
loop {
let mut input = String::new();
match std::io::stdin().read_line(&mut input) {
Ok(_) => {
telnet_write(&mut stream, &encode, &input).await?;
},
Err(e) => {
return Err(e);
}
}
};
}
動作確認
緑色の書き込みが Rustel で書き込んだ内容です。きちんとチャットに書き込めていることが確認できました。
今後の予定
-
Telnet 標準表現に対応する
-
TELNET サーバも立てられるようにする(なるべく)
-
TELNET on SSL に対応する(できたら)