この記事は、木更津高専 Advent Calendar 2023の16日目の記事です。
前→Arduino Uno R4 WiFiを使ってLAN内から文章を表示してみた by @kokastar
次→DiscordのWebhookをもっと便利に使う方法 by @nairoki
動機
9月上旬、突如として現れたTelnet電子公告1が話題となり、謎のTelnetブーム(?)が起こりました。
最近法人を設立したのですが、電子公告のURLってschemaはhttp/httpsに限定されていないことがわかりました!なんとtelnet://でもいける!! pic.twitter.com/4UXDNLMuhJ
— Hirotaka Nakajima (@nunnun) September 4, 2023
一体何番煎じなんだという感じですが、このプログラムは、一方的にテキストデータをクライアントに送りつけるだけの単純なものなので、ソケットプログラミングの勉強としては非常に良い題材です。
そこで、高専でやったソケットプログラミングの授業の復習がてらC言語を用いてプログラムを書いてみます。
サーバーを作る
全体的に、TCP/IPソケットプログラミング C言語編を参考にしました。
プログラムを書いている時、なぜserver_sock
とclient_sock
を親プロセスと子プロセス両方でクローズするのかが良く分かりませんでしたが、fork()
すると両方のプロセスが同じソケットディスクリプタを持つので、それぞれでクローズしないとソケットが終了しないみたいです2。
#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クライアントでサーバーにアクセスしてみましょう。すると、ちゃんとメッセージが送られてきます。ふう!
WAIT_US
の値をいじってみるとそれっぽい雰囲気が出て良いかも。
あとがき
やはりそこそこ低レイヤーな部分でプログラミングをすると、日常的に使ってるプログラムの裏側が見えて面白いですね。
今回のプログラムも、本家のように機能をどんどん追加していくと面白いかもしれませんね。