Telnetが話題になってたのでGoで受信して表示するをやっていきした日記

2023年09月14日にQiitaに公開していたもののアーカイブです。

いきさつ

https://forest.watch.impress.co.jp/docs/serial/yajiuma/1528962.html

Twitter経由でこんな記事を見かけました。

ある社団法人が公告を行う際に、Telnetという伝統的な技術を使用されたようです。 ターミナルにアドレスとポート番号を打ち込むと、ニフティサーブみたいに文字がパラパラと浮かび上がってきて非常にノスタルジーな気分が味わえました。

今回、皆さんが注目されたのはセキュリティの方、具体的には暗号化通信が堅牢でなかったため指摘が多数相次ぎ、法人側が迅速に修正した、という点と、Telnetの是非だったと思うんですけど、僕は

え!!!なんじゃこれ!すげぇぇ!どうやって受信してんの!?

という部分だったので、Telnetの正体の興味もさながら、Goの練習に簡単な受信ツールを作ってみました。

Telnetについてのメモ

・Telnetは伝統的な遠隔通信プロトコルである。IPを使い通信を行う ・SSHと同様に遠隔操作を行う事を目的としているが、SSHと異なる点はTelnet自体に暗号機能はない。 ・23番ポート等を使用し通信、対話的に入力を反映できる

Goでどうやって処理しよう

今回は

①TCP接続で対象となるTelnetサーバーをロックオンする ②Telnetサーバーの情報を読み取る ③画面に出力する

というシンプルな構成でやっていきます。

まず、Goに備わっている便利機能を使うためにツールをインポートします。 ①で必要となるのはネットワーク通信関連なのでnet ②③で必要なのはテキストデータの処理を効率化するbufio、出力フォーマットなどに使うfmtかなと絞りました。 ...あとエラー処理でosも使いますね(うっかり忘れてた)

https://pkg.go.dev/bufio https://pkg.go.dev/net

従ってimportツールは最終的にこんな構成に

import (
    "bufio"
    "fmt"
    "net"
    "os"
)

ではここから処理を書いていきます。

①TCP接続で対象となるTelnetサーバーをロックオンする

func connectTelnetServer(host string, port int) (net.Conn, error) {
     //ホスト名とポートを見て、アドレスにする。
    address := fmt.Sprintf("%s:%d", host, port)
     //TCPを使って上で指定したアドレスに接続する
    conn, err := net.Dial("tcp", address)
     //エラー処理
    if err != nil {
        return nil, err
    }
    
    return conn, nil
}

TelnetはIPで接続するのですが、さすがにURIで接続しないと番号入力が煩瑣になってしまうのでhostとportの2つのパラメータを指定し、接続先のサーバー名とポートを割り振ります。

その次に割り振ったパラメータから接続先となるアドレスへと変換したものをプリントします。 これを利用してネットに接続。接続にはネットダイアルを使用するのですが、必ず成功するとは限らないのでエラーを張っておきます。

もしうまく接続行かなかった場合は接続状態をnilにし、エラーを返すようにします。 仮にうまくいけばconn(connect)処理によって接続された状態が変数内に格納されるのでこれを使用するようにします。

func main() {
    host := "example.com"
    port := 23
    //ここで接続先の情報、ポートを入力する

    conn, err := connectTelnetServer(host, port)
    //接続を試行 エラーの可能性も考える

    if err != nil {
        fmt.Println("くそやろう、接続に失敗したぜ。:", err)
        os.Exit(1)
    }
    //エラーが発生したら悲しいお知らせを出す
    defer conn.Close()

先に出したTCP接続を行う関数によって接続を試行します。ここで戻り値として使ったconnは一番上の方にあるnet.Connと同義です。

もしエラーが発生したら接続が失敗した旨を伝えるようにします。 接続が失敗しているのにひたすら接続を試行すればこっちからはいつ諦めればいいのかが曖昧になってしまうのでos.Exit(1)で異常終了させます。

悲しいお知らせの跡にdeferという関数が出されていますが、これは関数の中で実行させてるコードを遅延させる効果を持っています。

deferの何が凄いかというと、関数内のコードが遅延されるのであとに来るコードは現在の関数の実行が終了するまで実行されなくなります。

これによりエラー表示や異常終了、ファイルを閉じるなどのあんまり失敗してほしくない動作を確実に実行できるようにできます。

deferは基本的には今回のように関数の最後の部分に記述することが多いんですけど、仮に複数の実行順序がある場合、 実行順序が逆転します。

通常の場合はコードは上から下に進行していきますが、deferで遅延処理が入るので逆転するわけです。

package main

import (
    "fmt"
)

func main() {
    defer fmt.Println("貴様は最後だ!")
    defer fmt.Println("次にこれが出るよ")
    fmt.Println("最初にこれが出るよ")
}

https://go.dev/play/p/LD_rWqDZ4F1

試しにこのコードを実行して頂けると幸いです。

②③Telnetサーバーの情報を読み取る+画面に出力する

scanner := bufio.NewScanner(conn)
  //データを読み取る

    for scanner.Scan() {
  //接続中はずっと読み取るようにする

        text := scanner.Text()
        fmt.Println(text)
    }

conn(connect)によってTelnet接続されたサーバーから情報を抜き出します。 ここではscannerがこの役割を行います。

二段目でforを使ってスキャンを何度も実行させることによって読み取りのおこぼれを防いでます。 スキャナーで読み取られた内容はtextとして格納され、後半のPrintへと回します。

ちなみにこれは今回採用していません。

今回実装した②③

go func() {
        scanner := bufio.NewScanner(conn)
        for scanner.Scan() {
            fmt.Println(scanner.Text())
        }
    }()

実は本家実装では最近本を読み漁り始めて使いたくてたまらなかったゴルーチン処理を使いました。

ゴルーチン(非同期スレッド)を使うことによってTelnetサーバーに接続している時間を待ちながら、他のタスクを実行できるようにできます。

今回の場合は閲覧だけなので抜群の効果を発揮するかは曖昧ですが、今後仮に入力機能などを足したりする際に効率よく処理が出来るようになるんじゃないかなぁと思い実装しました。

恐らく閲覧においてもなるべくリアルタイムで接続できるようになるといった効果が出るんじゃないかなぁとも思ってます。

一行目では匿名関数を使っています。匿名関数は関数名を持たないので、即座な呼び出し、ラムダ式、外部変数のアクセスショートカットみたいなものに使えて非常に便利です。動的な処理が得意なのでゴルーチンに盛り込んじゃいました。てへ。

具体的な処理内容は実装しなかった方と同じです。

いざ実食

Telnet早速繋いでみたいけど、Goは基本的にはUTF-8で駆動するため、話題の電子公告は文字コード違いで上手く出力されません。まだコード変換は実装していないので、今回はこのサイトの中から漁りました。

https://store.chipkin.com/articles/telnet-list-of-telnet-servers

freechess.org (ポート番号5000)

え!チェスTelnetで出来んの!?

という事でコード内に入れこんで実行...

うおおおおおおお!まじじゃん!すげえぇぇぇ!

感想

今回はTelnet接続をWindows純正の物を使用するのではなく、ちょっと遠回りしてGoで書いて作ってみました。

おおよそ30年以上前の通信ですが、通信方式が明確で非常に面白いですね。

そのあと、文字入力までは実装したのですが、まだ文字コードを弄れる機能は未実装なので勉強が必要です。

何よりもチェスのルールも覚えないと!

楽しかった😊

ここに書かれている内容に関しては根拠や出典の存在を一切保証しません。

また、実際の人物や団体に影響が出ないようにある程度抽象化したり脚色したりすることもあります。

ぶび