某電子公告的なTelnetサーバーをC言語で作る

この記事は、[木更津高専 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/nit_kisarazu)の16日目の記事です。

前→[Arduino Uno R4 WiFiを使ってLAN内から文章を表示してみた](https://zenn.dev/kokastar/articles/5155d098d8d6a2) by [@kokastar](https://qiita.com/kokastar)
次→[DiscordのWebhookをもっと便利に使う方法](https://qiita.com/nairoki/items/0ff44cb4a2b8042374c5) by [@nairoki](https://qiita.com/nairoki)

# 動機
9月上旬、突如として現れたTelnet電子公告[^1]が話題となり、謎のTelnetブーム（？）が起こりました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">最近法人を設立したのですが、電子公告のURLってschemaはhttp/httpsに限定されていないことがわかりました！なんとtelnet://でもいける！！ <a href="https://t.co/4UXDNLMuhJ">pic.twitter.com/4UXDNLMuhJ</a></p>&mdash; Hirotaka Nakajima (@nunnun) <a href="https://twitter.com/nunnun/status/1698542290183303409?ref_src=twsrc%5Etfw">September 4, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

一体何番煎じなんだという感じですが、このプログラムは、一方的にテキストデータをクライアントに送りつけるだけの単純なものなので、ソケットプログラミングの勉強としては非常に良い題材です。
そこで、高専でやったソケットプログラミングの授業の復習がてらC言語を用いてプログラムを書いてみます。

# サーバーを作る
全体的に、[TCP/IPソケットプログラミング C言語編](https://www.amazon.co.jp/TCP-IP%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-C%E8%A8%80%E8%AA%9E%E7%B7%A8-Michael-Donahoo/dp/4274065197)を参考にしました。
プログラムを書いている時、なぜ`server_sock`と`client_sock`を親プロセスと子プロセス両方でクローズするのかが良く分かりませんでしたが、`fork()`すると両方のプロセスが同じソケットディスクリプタを持つので、それぞれでクローズしないとソケットが終了しないみたいです[^2]。
```c:server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>

#define PORT 23
#define MAXPENDING 10
#define WAIT_US 20000

void exit_with_error(char *message) {
    perror(message);
    exit(EXIT_FAILURE);
}

int create_socket(void) {
    int sock;
    struct sockaddr_in server_addr;

    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        exit_with_error("socket() failed");
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        exit_with_error("bind() failed");
    }

    if (listen(sock, MAXPENDING) < 0) {
        exit_with_error("listen() failed");
    }

    return sock;
}

int accept_connection(int server_sock) {
    int client_sock;
    struct sockaddr_in client_addr;
    unsigned int client_struct_len;

    client_struct_len = sizeof(client_addr);

    if ((client_sock = accept(
        server_sock, 
        (struct sockaddr *)&client_addr, 
        &client_struct_len
    )) < 0) {
        exit_with_error("accept() failed");
    }

    return client_sock;
}

void send_message(int client_sock) {
    char message[] =
        "This page uses UTF-8 encoding.\r\n"
        "Don't use Shift_JIS encoding!!!\r\n"
        "\r\n"
        "toma09to.comへようこそ!\r\n"
        "何とか記事が書けてほっとしています……\r\n"
        " _                              ___    ___   _\r\n"
        "| |_   ___   _ __ ___    __ _  / _ \\  / _ \\ | |_   ___       ___   ___   _ __ ___\r\n"
        "| __| / _ \\ | '_ ` _ \\  / _` || | | || (_) || __| / _ \\     / __| / _ \\ | '_ ` _ \\\r\n"
        "| |_ | (_) || | | | | || (_| || |_| | \\__, || |_ | (_) | _ | (__ | (_) || | | | | |\r\n"
        " \\__| \\___/ |_| |_| |_| \\__,_| \\___/    /_/  \\__| \\___/ (_) \\___| \\___/ |_| |_| |_|\r\n"
        "\r\n";

    for (int i = 0; message[i] != '\0'; i++) {
        if (send(client_sock, &message[i], 1, 0) != 1) {
            fprintf(stderr, "send() failed\n");
            break;
        }
        usleep(WAIT_US);
    }

    close(client_sock);
}

int main(void) {
    int server_sock;
    int client_sock;
    pid_t process_id;
    unsigned int child_count = 0;

    server_sock = create_socket();

    while (1) {
        client_sock = accept_connection(server_sock);
        if ((process_id = fork()) < 0) {
            exit_with_error("fork() failed");
        } else if (process_id == 0) {
            // child process
            close(server_sock);
            send_message(client_sock);

            exit(0);
        }

        // parent process
        close(client_sock);
        child_count += 1;

        // harvest zombie processes
        while (child_count > 0) {
            process_id = waitpid((pid_t)-1, NULL, WNOHANG);
            if (process_id < 0) {
                exit_with_error("waitpid() failed");
            } else if (process_id == 0) {
                // no child process exists
                break;
            } else {
                child_count -= 1;
            }
        }
    }
}
```

# 動作確認
作ったプログラムをコンパイルしてサーバーで実行してみます。
システムポートである23番（Telnetのポート番号）を割り当てるので、実行には管理者権限が必要になるはずです。
プログラムが起動したら、[Tera Term](https://forest.watch.impress.co.jp/library/software/utf8teraterm/)等のTelnetクライアントでサーバーにアクセスしてみましょう。すると、ちゃんとメッセージが送られてきます。ふう！
![Screen Recording 2023-12-16 at 5.02.52.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1647235/f9ba1e76-e94e-c5e6-0304-bee41eca17bb.gif)

`WAIT_US`の値をいじってみるとそれっぽい雰囲気が出て良いかも。

# あとがき
やはりそこそこ低レイヤーな部分でプログラミングをすると、日常的に使ってるプログラムの裏側が見えて面白いですね。
今回のプログラムも、本家のように機能をどんどん追加していくと面白いかもしれませんね。

[^1]:その後TELNETS（TELNET over SSL）版が公開されたり（？？）、チャット機能が追加されたり（？？？？）してます。どゆこと。
[^2]:実のところ，OSの内側はあまり理解していない．スミマセン．