7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

木更津高専Advent Calendar 2023

Day 16

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

Last updated at Posted at 2023-12-15

この記事は、木更津高専 Advent Calendar 2023の16日目の記事です。

前→Arduino Uno R4 WiFiを使ってLAN内から文章を表示してみた by @kokastar
次→DiscordのWebhookをもっと便利に使う方法 by @nairoki

動機

9月上旬、突如として現れたTelnet電子公告1が話題となり、謎のTelnetブーム(?)が起こりました。

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

サーバーを作る

全体的に、TCP/IPソケットプログラミング C言語編を参考にしました。
プログラムを書いている時、なぜserver_sockclient_sockを親プロセスと子プロセス両方でクローズするのかが良く分かりませんでしたが、fork()すると両方のプロセスが同じソケットディスクリプタを持つので、それぞれでクローズしないとソケットが終了しないみたいです2

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等のTelnetクライアントでサーバーにアクセスしてみましょう。すると、ちゃんとメッセージが送られてきます。ふう!
Screen Recording 2023-12-16 at 5.02.52.gif

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

あとがき

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

  1. その後TELNETS(TELNET over SSL)版が公開されたり(??)、チャット機能が追加されたり(????)してます。どゆこと。

  2. 実のところ,OSの内側はあまり理解していない.スミマセン.

7
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?