お久しぶりです。季節の移り変わりとともに、もはや誰も TELNET の電子公告の話をしなくなってしまいましたが、今回は TELNET サーバ用のプログラムを作ったという内容の記事です。

非常に今更感があるのでお蔵入りにしようかとも思いましたが、最近マジで記事に書くネタがないのでやっぱり書いておきます。

実験環境など

  • OS : Windows 10, macOS 13.3.1

  • Rust 1.72.0

  • encoding_rs 0.8.23

  • tokio 1.32.0

  • clap 4.4.2

実験環境など

  • OS : Windows 10, macOS 13.3.1

  • Rust 1.72.0

  • encoding_rs 0.8.23

  • tokio 1.32.0

  • clap 4.4.2

リポジトリ

前回に引き続き、rustel にサーバモードとして実装しました。

GitHub - yotiosoft/rustel at rustel_v3

サーバの立て方

  • メッセージを引数で渡す場合:
$ rustel -s -u [URL(127.0.0.1 etc.)] -p [Port Number] -e [Encode (utf8 or sjis)] -m [message]
  • メッセージをファイルから読み込む場合:
$ rustel -s -u [URL(127.0.0.1 etc.)] -p [Port Number] -e [Encode (utf8 or sjis)] -f [filepath]
  • 1文字ずつ送信する場合:
$ rustel -s -u [URL(127.0.0.1 etc.)] -p [Port Number] -e [Encode (utf8 or sjis)] -f [filepath] -o

実装

サーバと言っても、メッセージを平文でソケット通信で送出するだけです。

クライアントモードとは異なり、サーバ側ですので指定したポート番号を bind し、listener からの要求を待機します。

その後クライアントからのアクセスがあり次第、定型文 server_message を送信します。

今回、複数のクライアントからの同時接続に対応するため、クライアントごとにスレッドを生成しています。

async fn server(host: String, port: u16, encode: args::Encode, ipv: IPv, server_one_char: bool, server_message: Option<String>, wait_ms: u64) -> Result<(), std::io::Error> {
    let host_and_port = format!("{}:{}", host, port);
    let mut addresses = host_and_port.to_socket_addrs()?;

    let address = match ipv {
        IPv::IPv4 => addresses.find(|x| x.is_ipv4()),
        IPv::IPv6 => addresses.find(|x| x.is_ipv6()),
    };

    let listener = TcpListener::bind(address.unwrap()).await?;
    loop {
        let (mut stream, _) = listener.accept().await?;
        let encode_clone = encode.clone();
        let server_message = server_message.clone();
        tokio::spawn(async move {
            let addr = stream.peer_addr().unwrap();
            println!("Accepted connection from: {}", addr);
            let (reader, writer) = tokio::io::split(stream);

            // read
            let reader = tokio::spawn(telnet::telnet_recv(reader, encode_clone.clone()));

            // write
            let writer = if let Some(server_message) = server_message {
                if server_one_char {
                    tokio::spawn(telnet::telnet_send_message_per_one_char(writer, encode_clone.clone(), server_message, wait_ms))
                }
                else {
                    tokio::spawn(telnet::telnet_send_message(writer, encode_clone.clone(), server_message))
                }
            }
            else {
                tokio::spawn(telnet::telnet_send(writer, encode_clone.clone()))
            };

            let _ = reader.await;
            writer.abort();
            println!("Connection with {} closed.", addr);
        });
    }

    Ok(())
}

工夫した点として、例の TELNET 電子公告は RPG のごとく 1 文字ずつメッセージが表示されます。

これはサーバが 1 パケットに対し 1 文字ずつ送信しているからなのですが、これを再現するために 1 文字ずつ送信するモード (–one-char) を設けました。パケット送信時の wait 時間を調整 (-wait-ms) することも可能です。

Usage: rustel [OPTIONS] --url <URL>

Options:
  -s, --server             set as server mode
  -c, --client             set as client mode (default)
  -u, --url <URL>          destination URL (required)
  -p, --port <PORT>        destination port number (default: 23) [default: 23]
  -e, --encode <ENCODE>    encode (utf8 or shift_jis; default: utf8) [default: utf8]
  -i, --ipv <IPV>          IP version (4 or 6; default: 4) [default: 4]
  -o, --one-char           send one character to client (server mode only)
  -w, --wait-ms <WAIT_MS>  wait time for sending the message (millisecond) (server mode only; default: 100) [default: 100]
  -m, --message <MESSAGE>  message to send to client (server mode only)
  -f, --file <FILE>        message file to send to client (server mode only)
  -h, --help               Print help
  -V, --version            Print version

ファイルから読み込ませて送信することもできます (–file) 。

動作確認

ファイルから読み込み、1文字ずつサーバからクライアントに送信している様子です。

今後の予定

  • サーバ側でチャット機能に対応する

  • パイプからサーバ用メッセージを受け取れるようにする

  • TELNET on SSL に対応する(できたら)

参考文献