第5章: ソフトウェア工学とクライアント・サーバー実装

5.1 ソフトウェア工学の誕生:NATOソフトウェア工学会議

5.1.1 ソフトウェア危機(1960年代)

1960年代後半、コンピュータ産業は深刻な問題に直面していた。ハードウェアの性能は急速に向上し、大規模で複雑なソフトウェアプロジェクトが可能になったが、そのプロジェクトの多くは失敗に終わっていた。予算超過、スケジュール遅延、品質問題が蔓延し、この状況は「ソフトウェア危機」(Software Crisis)と呼ばれた。

1968年のNATOソフトウェア工学会議

1968年10月、NATO科学委員会はドイツのガルミッシュで歴史的な会議を開催した。この会議で「ソフトウェア工学」(Software Engineering)という用語が公式に提唱され、ソフトウェア開発を工学的規律として確立する必要性が議論された。

会議の報告書より:

> "The phrase 'software engineering' was deliberately chosen as being provocative, in implying the need for software manufacture to be based on the types of theoretical foundations and practical disciplines that are traditional in the established branches of engineering." > > — Brian Randell, NATO Software Engineering Conference Report, 1968

Edsger Dijkstraの構造化プログラミング

1968年、Dijkstraは「Go To Statement Considered Harmful」という論文をCommunications of the ACMに発表し、goto文の無秩序な使用がプログラムの理解と検証を困難にすると主張した。

// 1960年代のスパゲッティコード(擬似コード)
10: IF condition GOTO 50
20: statement1
30: GOTO 10
40: statement2
50: IF condition2 GOTO 20
60: GOTO 40

// Dijkstraが提唱した構造化プログラミング
while (condition) {
    if (condition2) {
        statement1;
    } else {
        statement2;
    }
}

Dijkstraの構造化プログラミングの原則:

  • 逐次実行(Sequence):文は順番に実行される
  • 選択(Selection):if-then-else構造
  • 反復(Iteration):while/forループ
  • 抽象化(Abstraction):サブルーチンと関数

5.1.2 Fred Brooksと「人月の神話」

1975年、IBMのFred Brooksは『人月の神話』(The Mythical Man-Month)を出版した。OS/360プロジェクトの経験から、ソフトウェア開発の本質的な困難さを分析した。

Brooksの法則

> "Adding manpower to a late software project makes it later." > > — Fred Brooks, "The Mythical Man-Month", 1975

本質的複雑性と偶有的複雑性

Brooksは1986年の論文「No Silver Bullet」で、ソフトウェアの複雑性を2種類に分類した:

  • 本質的複雑性(Essential Complexity)
- 問題ドメイン自体に内在する複雑性 - 顧客の要求、ビジネスルール、実世界の制約 - 技術では除去できない

  • 偶有的複雑性(Accidental Complexity)
- 実装の選択から生じる複雑性 - プログラミング言語、ツール、プラットフォームに依存 - 技術の進歩で軽減可能

// Minitalkにおける複雑性の分析

// 本質的複雑性(除去不可能)
// - プロセス間でデータを転送する必要性
// - 信頼性のある通信の要件
// - 同期の必要性

// 偶有的複雑性(設計で軽減可能)
// - ビット単位の送信(シグナルの制約)
// - タイミング調整(OSのスケジューリング)
// - エラー処理の実装

5.1.3 UNIXの哲学と設計原則

Ken Thompson、Dennis Ritchie、Doug McIlroyらが開発したUNIXは、独自の設計哲学を確立した。

UNIXの哲学(1978年、Doug McIlroy)

> "This is the Unix philosophy: Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface." > > — Doug McIlroy, Bell System Technical Journal, 1978

UNIX設計原則

| 原則 | 説明 | Minitalkへの適用 | |------|------|-----------------| | 単一責任 | 各プログラムは一つのことを行う | サーバーは受信、クライアントは送信 | | モジュール性 | 独立したコンポーネントに分割 | handlers.c, sender.c, utils/ | | 透明性 | 動作を理解しやすく | PID表示、プログレス表示 | | 経済性 | 必要最小限の機能 | libftの再利用 | | 修復性 | 障害からの回復が容易 | タイムアウトとリトライ |

Rob Pikeの追加原則(Notes on Programming in C, 1989)

  • Rule 1: プログラミングで最も難しい部分はアルゴリズムではなく、データ構造だ
  • Rule 2: データが複雑なら、コードをシンプルに
  • Rule 3: データを正しく構造化すれば、アルゴリズムは自明になる
  • Rule 4: ボトルネックは予想外の場所に発生する

5.2 クライアント・サーバーアーキテクチャの理論

5.2.1 分散システムの歴史

初期の分散コンピューティング

1960年代後半、ARPANETの開発と並行して、分散システムの理論が発展した。

時系列:分散システムの発展

1969年 - ARPANETが4ノードで運用開始
1971年 - 最初のネットワークメールシステム
1973年 - Xerox PARCでEthernet開発
1975年 - 最初のクライアント・サーバーモデルの理論化
1978年 - Leslie Lamportの分散システム時間論文
1983年 - SunのNFS(Network File System)
1985年 - RPCの標準化

Leslie Lamportの貢献

1978年、Leslie Lamportは「Time, Clocks, and the Ordering of Events in a Distributed System」を発表し、分散システムにおける因果関係と順序の理論を確立した。

Lamportのhappened-before関係(→):

定義:イベントaがイベントbより前に発生した(a → b)とは:
1. aとbが同じプロセス内にあり、aがbより前に発生した
2. aがメッセージの送信で、bがそのメッセージの受信
3. 推移律:a → c かつ c → b ならば a → b

Minitalkにおけるhappened-before関係:

クライアント                    サーバー
    |                              |
send_bit(0) ─────────────────→ receive_bit
    |                              |
    ← ─────────────────────── send_ack
    |                              |
send_bit(1) ─────────────────→ receive_bit
    |                              |
    ↓                              ↓

happened-before関係:
send_bit(0) → receive_bit(0) → send_ack(0) → send_bit(1) → ...

5.2.2 クライアント・サーバーモデルの形式化

形式的定義

クライアント・サーバーシステムは、以下の要素で構成される:

定義(クライアント・サーバーシステム)

M = (C, S, R, P) where:
  C = {c₁, c₂, ..., cₙ}  : クライアントの集合
  S = {s₁, s₂, ..., sₘ}  : サーバーの集合
  R = {request, response} : メッセージタイプ
  P : C × S × R → R       : プロトコル関数

プロトコルの性質:
1. 要求応答性:∀ request ∃ response(すべての要求に応答がある)
2. 順序保存性:request₁ → request₂ ⇒ response₁ → response₂
3. べき等性(理想):同じ要求は同じ結果を生む

通信パターン

クライアント・サーバー通信には、いくつかのパターンがある:

1. 同期通信(Synchronous)
   クライアント: send(request) → block → receive(response)

   利点:シンプル、順序保証
   欠点:ブロッキング、スケーラビリティ制限

2. 非同期通信(Asynchronous)
   クライアント: send(request) → continue → callback(response)

   利点:並行性、効率的
   欠点:複雑、デバッグ困難

3. Minitalkの半同期パターン
   クライアント: send(bit) → wait(ack) → send(next_bit)

   利点:信頼性、フロー制御
   欠点:レイテンシ、オーバーヘッド

5.2.3 プロセス間通信(IPC)のパターン

IPCメカニズムの分類

UNIXシステムは、複数のIPCメカニズムを提供する:

| メカニズム | 特性 | 適用場面 | |------------|------|----------| | パイプ | 単方向、親子間 | シェルコマンド | | 名前付きパイプ | 単方向、任意プロセス | ロギング | | シグナル | 非同期、イベント通知 | Minitalk | | 共有メモリ | 高速、同期必要 | 大量データ | | ソケット | 双方向、ネットワーク | ネットワーク通信 | | メッセージキュー | 非同期、バッファリング | ジョブキュー |

シグナルによる通信の特性

シグナルは本来、イベント通知のためのメカニズムであり、データ転送には設計されていない:

// シグナル通信の制約
// 1. 帯域幅:1ビット/シグナル(SIGUSR1 = 0, SIGUSR2 = 1)
// 2. 信頼性:シグナルは消失する可能性がある
// 3. 順序:保証されない場合がある
// 4. オーバーヘッド:コンテキストスイッチが必要

// 設計上の決定:
// - 同期通信でシグナル消失を防止
// - ACK機構で順序を保証
// - タイムアウトで障害検出

5.3 有限状態機械の理論

5.3.1 オートマトン理論の起源

Warren McCullochとWalter Pitts(1943年)

1943年、神経科学者McCullochと論理学者Pittsは「A Logical Calculus of the Ideas Immanent in Nervous Activity」を発表し、人工ニューロンのモデルを提案した。これは後のオートマトン理論の基礎となった。

Stephen Kleeneの有限オートマトン(1956年)

1956年、Stephen Kleeneは「Representation of Events in Nerve Nets and Finite Automata」で、正規表現と有限オートマトンの等価性を証明した。

5.3.2 有限状態機械の形式的定義

決定性有限オートマトン(DFA)

定義(決定性有限オートマトン)

M = (Q, Σ, δ, q₀, F) where:
  Q  : 状態の有限集合
  Σ  : 入力アルファベット(有限集合)
  δ  : Q × Σ → Q(遷移関数)
  q₀ : 初期状態(q₀ ∈ Q)
  F  : 受理状態の集合(F ⊆ Q)

Minitalkサーバーの状態機械モデル

サーバーオートマトン M_server = (Q, Σ, δ, q₀, F)

Q = {IDLE, RECEIVING, CHAR_COMPLETE}

Σ = {SIGUSR1, SIGUSR2, BIT_8_COMPLETE, CHAR_IS_NULL, CHAR_NOT_NULL}

δ(遷移関数):
  δ(IDLE, SIGUSR1)              = RECEIVING
  δ(IDLE, SIGUSR2)              = RECEIVING
  δ(RECEIVING, SIGUSR1)         = RECEIVING (bit_count < 8)
  δ(RECEIVING, SIGUSR2)         = RECEIVING (bit_count < 8)
  δ(RECEIVING, BIT_8_COMPLETE)  = CHAR_COMPLETE
  δ(CHAR_COMPLETE, CHAR_IS_NULL)    = IDLE
  δ(CHAR_COMPLETE, CHAR_NOT_NULL)   = IDLE

q₀ = IDLE

F = {IDLE}  (文字列受信完了後にIDLEに戻る)

状態遷移図:

                    SIGUSR1/SIGUSR2
                    (bit_count < 8)
                          ↓ ↑
                    ┌─────┴─────┐
                    │           │
  SIGUSR1/SIGUSR2   │ RECEIVING │
      ┌─────────────│           │
      │             └─────┬─────┘
      │                   │ BIT_8_COMPLETE
      ▼                   ▼
  ┌──────┐         ┌─────────────┐
  │      │         │    CHAR     │
  │ IDLE │◀────────│  COMPLETE   │
  │      │         └─────────────┘
  └──────┘           CHAR_IS_NULL or
                     CHAR_NOT_NULL

5.3.3 Mealy機械とMoore機械

George Mealy(1955年)とEdward Moore(1956年)

状態機械には2つの主要なモデルがある:

Mealy機械

定義(Mealy機械)

出力は現在の状態と入力の両方に依存する

M = (Q, Σ, Ω, δ, λ, q₀) where:
  Ω : 出力アルファベット
  λ : Q × Σ → Ω(出力関数)

Moore機械

定義(Moore機械)

出力は現在の状態のみに依存する

M = (Q, Σ, Ω, δ, γ, q₀) where:
  γ : Q → Ω(出力関数)

Minitalkサーバーの実装(Mealy機械として)

// Mealy機械の実装
// 出力(アクション)は状態と入力の両方に依存する

typedef enum e_state {
    STATE_IDLE,
    STATE_RECEIVING,
    STATE_CHAR_COMPLETE
} t_state;

typedef struct s_mealy_machine {
    t_state         current_state;
    unsigned char   current_char;
    int             bit_count;
    pid_t           client_pid;
} t_mealy_machine;

// 遷移関数と出力関数の統合
void transition(t_mealy_machine *m, int signum, siginfo_t *info)
{
    // 状態と入力に基づく遷移
    switch (m->current_state)
    {
        case STATE_IDLE:
            m->client_pid = info->si_pid;
            m->current_char = 0;
            m->bit_count = 0;
            m->current_state = STATE_RECEIVING;
            // フォールスルーで最初のビットを処理

        case STATE_RECEIVING:
            m->current_char <<= 1;
            if (signum == SIGUSR2)
                m->current_char |= 1;
            m->bit_count++;

            if (m->bit_count == 8)
                m->current_state = STATE_CHAR_COMPLETE;
            break;

        case STATE_CHAR_COMPLETE:
            // 出力(Mealy機械の特徴)
            if (m->current_char == '\0')
            {
                write(1, "\n", 1);
                kill(m->client_pid, SIGUSR2);  // 終了通知
            }
            else
            {
                write(1, &m->current_char, 1);
                kill(m->client_pid, SIGUSR1);  // ACK
            }

            m->current_state = STATE_IDLE;
            m->current_char = 0;
            m->bit_count = 0;
            break;
    }
}

5.4 エラー処理と信頼性の理論

5.4.1 障害モデル

分散システムにおける障害は、いくつかのカテゴリに分類される:

障害の分類(Cristian, 1991)

| 障害タイプ | 説明 | Minitalkでの例 | |------------|------|----------------| | クラッシュ障害 | プロセスが停止 | サーバーがkillされる | | 省略障害 | メッセージの消失 | シグナルがドロップ | | タイミング障害 | 時間制約違反 | タイムアウト | | ビザンチン障害 | 任意の誤動作 | 悪意ある入力 |

フェイルセーフ vs フェイルファスト

フェイルセーフ設計:
- 障害時に安全な状態に遷移
- 可能な限り処理を継続
- 例:リトライ機構

フェイルファスト設計:
- 障害を早期に検出して報告
- 不整合な状態を防止
- 例:アサーション、タイムアウト

Minitalkでの適用:
- タイムアウト:フェイルファスト(障害を検出)
- リトライ:フェイルセーフ(回復を試みる)
- 最終的な失敗:フェイルファスト(エラー報告)

5.4.2 信頼性メトリクス

MTBF(Mean Time Between Failures)

MTBF = 総稼働時間 / 障害回数

例:100時間の稼働で2回の障害
MTBF = 100 / 2 = 50時間

MTTR(Mean Time To Repair)

MTTR = 総修復時間 / 障害回数

例:2回の障害、合計修復時間10分
MTTR = 10 / 2 = 5分

可用性(Availability)

A = MTBF / (MTBF + MTTR)

例:MTBF = 50時間、MTTR = 5分 = 0.083時間
A = 50 / (50 + 0.083) ≈ 0.998 (99.8%)

5.4.3 再送戦略

指数バックオフ(Exponential Backoff)

1988年、EthernetのCSMA/CD仕様で形式化された:

// 指数バックオフの実装
int exponential_backoff(int retry_count, int base_delay)
{
    int max_delay;
    int delay;

    // 最大遅延を計算(2^retry * base)
    max_delay = base_delay * (1 << retry_count);

    // 上限を設定
    if (max_delay > 1000000)  // 1秒
        max_delay = 1000000;

    // ランダム化(ジッター)
    delay = rand() % max_delay;

    return delay;
}

// 使用例
int send_bit_with_backoff(pid_t pid, int bit)
{
    int retry;
    int delay;

    for (retry = 0; retry < MAX_RETRY; retry++)
    {
        if (try_send_bit(pid, bit))
            return 1;  // 成功

        delay = exponential_backoff(retry, 100);  // 100μs基準
        usleep(delay);
    }

    return 0;  // 失敗
}

5.5 完全なMinitalk実装

5.5.1 プロジェクト構成

UNIXの哲学に基づく、モジュール化されたディレクトリ構造:

minitalk/
├── Makefile              # ビルドシステム
├── includes/
│   └── minitalk.h        # 公開インターフェース
├── srcs/
│   ├── server/
│   │   ├── server.c      # サーバーメイン(エントリポイント)
│   │   └── handlers.c    # 状態機械の実装
│   ├── client/
│   │   ├── client.c      # クライアントメイン
│   │   └── sender.c      # 送信ロジック
│   └── utils/
│       ├── ft_atoi.c     # 文字列→数値変換
│       ├── ft_putnbr.c   # 数値出力
│       └── ft_strlen.c   # 文字列長取得
└── libft/                # libftライブラリ(オプション)

5.5.2 ヘッダーファイル(公開インターフェース)

/* minitalk.h
 * 公開インターフェースの定義
 * 情報隠蔽の原則に従い、必要最小限のみ公開
 */
#ifndef MINITALK_H
# define MINITALK_H

# include <signal.h>
# include <unistd.h>
# include <stdlib.h>

/*
** 設計パラメータ
** これらの値は経験的に調整される
*/
# define SIGNAL_DELAY   100     /* ビット間の遅延(μs)*/
# define ACK_TIMEOUT    1000    /* ACK待機タイムアウト(μs)*/
# define MAX_RETRY      3       /* 最大再送回数 */

/*
** サーバー状態構造体
** 有限状態機械の状態を保持
*/
typedef struct s_server_state
{
    unsigned char   current_char;   /* 受信中の文字 */
    int             bit_count;      /* 受信ビット数(0-7)*/
    pid_t           client_pid;     /* 現在のクライアント */
}   t_server_state;

/*
** クライアント状態構造体
** 非同期ACK受信の状態を保持
*/
typedef struct s_client_state
{
    volatile sig_atomic_t   ack_received;   /* ACK受信フラグ */
    int                     delay;          /* 現在の遅延値 */
    int                     retry_count;    /* 再送回数 */
}   t_client_state;

/* サーバーインターフェース */
void    setup_server_handlers(void);
void    signal_handler(int signum, siginfo_t *info, void *context);

/* クライアントインターフェース */
void    setup_client_handlers(void);
void    ack_handler(int signum);
int     send_string(pid_t server_pid, const char *str);
int     send_char(pid_t server_pid, unsigned char c);
int     send_bit(pid_t server_pid, int bit);

/* ユーティリティ */
pid_t   ft_atoi(const char *str);
void    ft_putnbr(int n);
int     ft_strlen(const char *str);
void    error_exit(const char *msg);

#endif

5.5.3 Makefile(ビルドシステム)

# Makefile
# Stuart Feldmanのmake(1976年)の規約に従う

NAME_SERVER = server
NAME_CLIENT = client

CC = cc
CFLAGS = -Wall -Wextra -Werror -I./includes

# ソースファイル(依存関係を明示)
SRCS_SERVER = srcs/server/server.c \
              srcs/server/handlers.c \
              srcs/utils/ft_putnbr.c

SRCS_CLIENT = srcs/client/client.c \
              srcs/client/sender.c \
              srcs/utils/ft_atoi.c \
              srcs/utils/ft_strlen.c

# オブジェクトファイル
OBJS_SERVER = $(SRCS_SERVER:.c=.o)
OBJS_CLIENT = $(SRCS_CLIENT:.c=.o)

# デフォルトターゲット
all: $(NAME_SERVER) $(NAME_CLIENT)

# サーバーのリンク
$(NAME_SERVER): $(OBJS_SERVER)
	$(CC) $(CFLAGS) -o $(NAME_SERVER) $(OBJS_SERVER)

# クライアントのリンク
$(NAME_CLIENT): $(OBJS_CLIENT)
	$(CC) $(CFLAGS) -o $(NAME_CLIENT) $(OBJS_CLIENT)

# パターンルール(暗黙のルール)
%.o: %.c
	$(CC) $(CFLAGS) -c ___CODE_BLOCK_21___lt; -o $@

# クリーンアップ
clean:
	rm -f $(OBJS_SERVER) $(OBJS_CLIENT)

fclean: clean
	rm -f $(NAME_SERVER) $(NAME_CLIENT)

re: fclean all

# PHONYターゲット(ファイルでないターゲット)
.PHONY: all clean fclean re

5.5.4 サーバー実装

server.c - エントリポイント

/* srcs/server/server.c
 * サーバーのメインエントリポイント
 * UNIXデーモンのパターンに従う
 */
#include "minitalk.h"

int main(void)
{
    pid_t pid;

    /* プロセスIDを取得・表示 */
    pid = getpid();
    write(1, "Server PID: ", 12);
    ft_putnbr(pid);
    write(1, "\n", 1);

    /* シグナルハンドラを設定 */
    setup_server_handlers();

    /* 待機状態を通知 */
    write(1, "Waiting for messages...\n", 24);

    /*
     * 無限ループでシグナルを待機
     * pause()は効率的にCPUを解放してシグナルを待つ
     * これはUNIXデーモンの標準パターン
     */
    while (1)
        pause();

    return (0);
}

handlers.c - 状態機械の実装

/* srcs/server/handlers.c
 * 有限状態機械としてのシグナルハンドラ
 * Mealy機械のパターンを使用
 */
#include "minitalk.h"

/*
 * グローバル状態変数
 * シグナルハンドラからアクセスする必要があるため
 * グローバルスコープで定義
 */
static t_server_state g_state = {0, 0, 0};

/*
 * signal_handler - メインの遷移関数
 *
 * この関数は有限状態機械の遷移を実装する:
 * - 入力:signum(SIGUSR1 = 0, SIGUSR2 = 1)
 * - 状態:g_state.bit_count(0-7)
 * - 出力:文字の表示、ACK送信
 */
void signal_handler(int signum, siginfo_t *info, void *context)
{
    (void)context;

    /*
     * 新しいクライアントの検出
     * 状態機械のリセット条件
     */
    if (g_state.client_pid != info->si_pid)
    {
        if (g_state.bit_count != 0)
        {
            write(2, "\nWarning: New client, resetting state\n", 38);
        }
        g_state.client_pid = info->si_pid;
        g_state.current_char = 0;
        g_state.bit_count = 0;
    }

    /*
     * ビット受信処理
     * MSBファーストのシリアル化を逆変換
     */
    g_state.current_char <<= 1;
    if (signum == SIGUSR2)
        g_state.current_char |= 1;
    g_state.bit_count++;

    /*
     * 8ビット受信完了 - 状態遷移
     */
    if (g_state.bit_count == 8)
    {
        if (g_state.current_char == '\0')
        {
            /* 文字列終端 - 終了通知を送信 */
            write(1, "\n", 1);
            kill(g_state.client_pid, SIGUSR2);
        }
        else
        {
            /* 通常文字 - 表示してACK */
            write(1, &g_state.current_char, 1);
            kill(g_state.client_pid, SIGUSR1);
        }

        /* 次の文字のために状態をリセット */
        g_state.current_char = 0;
        g_state.bit_count = 0;
    }
}

/*
 * setup_server_handlers - シグナルハンドラの設定
 *
 * sigaction()を使用してPOSIX準拠の設定を行う
 * SA_SIGINFOで送信元PIDを取得可能にする
 */
void setup_server_handlers(void)
{
    struct sigaction sa;

    /* シグナルマスクを初期化 */
    sigemptyset(&sa.sa_mask);

    /*
     * クリティカルセクションの保護
     * ハンドラ実行中は同種のシグナルをブロック
     */
    sigaddset(&sa.sa_mask, SIGUSR1);
    sigaddset(&sa.sa_mask, SIGUSR2);

    /* ハンドラとフラグを設定 */
    sa.sa_sigaction = signal_handler;
    sa.sa_flags = SA_SIGINFO | SA_RESTART;

    /* SIGUSR1とSIGUSR2にハンドラを登録 */
    if (sigaction(SIGUSR1, &sa, NULL) == -1)
        error_exit("Error: sigaction SIGUSR1 failed");
    if (sigaction(SIGUSR2, &sa, NULL) == -1)
        error_exit("Error: sigaction SIGUSR2 failed");
}

5.5.5 クライアント実装

client.c - エントリポイント

/* srcs/client/client.c
 * クライアントのメインエントリポイント
 * 引数の検証と送信の開始
 */
#include "minitalk.h"

int main(int argc, char **argv)
{
    pid_t server_pid;

    /* 引数の検証 */
    if (argc != 3)
    {
        write(2, "Usage: ", 7);
        write(2, argv[0], ft_strlen(argv[0]));
        write(2, " <server_pid> <message>\n", 24);
        return (1);
    }

    /* サーバーPIDの取得と検証 */
    server_pid = ft_atoi(argv[1]);
    if (server_pid <= 0)
        error_exit("Error: Invalid PID");

    /*
     * プロセスの存在確認
     * kill(pid, 0)はシグナルを送らずに権限を確認
     */
    if (kill(server_pid, 0) == -1)
        error_exit("Error: Server process not found");

    /* クライアントハンドラを設定 */
    setup_client_handlers();

    /* メッセージ送信 */
    write(1, "Sending message...\n", 19);
    if (!send_string(server_pid, argv[2]))
    {
        write(2, "Error: Failed to send message\n", 30);
        return (1);
    }

    write(1, "Message sent successfully!\n", 27);
    return (0);
}

sender.c - 送信ロジック

/* srcs/client/sender.c
 * ビット送信ロジックの実装
 * フロー制御とエラー回復を含む
 */
#include "minitalk.h"

/* グローバル状態(シグナルハンドラと共有)*/
static t_client_state g_client = {0, SIGNAL_DELAY, 0};

/*
 * ack_handler - ACK受信ハンドラ
 *
 * この関数はasync-signal-safeでなければならない
 * volatile sig_atomic_tへの代入のみを行う
 */
void ack_handler(int signum)
{
    (void)signum;
    g_client.ack_received = 1;
}

/*
 * setup_client_handlers - クライアントのシグナル設定
 */
void setup_client_handlers(void)
{
    struct sigaction sa;

    sigemptyset(&sa.sa_mask);
    sa.sa_handler = ack_handler;
    sa.sa_flags = SA_RESTART;

    if (sigaction(SIGUSR1, &sa, NULL) == -1)
        error_exit("Error: sigaction SIGUSR1 failed");
    if (sigaction(SIGUSR2, &sa, NULL) == -1)
        error_exit("Error: sigaction SIGUSR2 failed");
}

/*
 * send_bit - 1ビットを送信
 *
 * 信頼性のある送信のためにリトライ機構を実装
 * タイムアウトで障害を検出
 */
int send_bit(pid_t server_pid, int bit)
{
    int timeout;
    int retry;

    retry = 0;
    while (retry < MAX_RETRY)
    {
        g_client.ack_received = 0;

        /* シグナル送信 */
        if (kill(server_pid, bit ? SIGUSR2 : SIGUSR1) == -1)
            return (0);

        /* ACK待機(ポーリング)*/
        timeout = ACK_TIMEOUT;
        while (!g_client.ack_received && timeout > 0)
        {
            usleep(10);
            timeout -= 10;
        }

        /* ACK受信成功 */
        if (g_client.ack_received)
            return (1);

        /* リトライ */
        retry++;
        write(2, "Retrying...\n", 12);
    }

    return (0);  /* 最大リトライ回数に達して失敗 */
}

/*
 * send_char - 1文字(8ビット)を送信
 *
 * MSBファーストで送信(ネットワークバイトオーダー)
 */
int send_char(pid_t server_pid, unsigned char c)
{
    int bit;

    bit = 7;
    while (bit >= 0)
    {
        if (!send_bit(server_pid, (c >> bit) & 1))
            return (0);
        bit--;
    }

    return (1);
}

/*
 * send_string - 文字列を送信
 *
 * NULL終端子も送信して文字列の終端を通知
 */
int send_string(pid_t server_pid, const char *str)
{
    int i;

    i = 0;
    while (str[i])
    {
        if (!send_char(server_pid, str[i]))
            return (0);
        i++;
    }

    /* NULL終端を送信 */
    if (!send_char(server_pid, '\0'))
        return (0);

    return (1);
}

5.5.6 ユーティリティ関数

ft_atoi.c - 文字列から数値への変換

/* srcs/utils/ft_atoi.c
 * ASCII to Integer変換
 * PIDの解析に特化した実装
 */
#include "minitalk.h"

pid_t ft_atoi(const char *str)
{
    pid_t   result;
    int     i;

    if (!str)
        return (0);

    result = 0;
    i = 0;

    /* 先頭の空白をスキップ */
    while (str[i] == ' ' || str[i] == '\t' || str[i] == '\n')
        i++;

    /* 符号の処理(PIDは正の値のみ)*/
    if (str[i] == '-' || str[i] == '+')
    {
        if (str[i] == '-')
            return (0);  /* 負のPIDは無効 */
        i++;
    }

    /* 数字を変換 */
    while (str[i] >= '0' && str[i] <= '9')
    {
        result = result * 10 + (str[i] - '0');
        i++;
    }

    return (result);
}

ft_putnbr.c - 数値出力

/* srcs/utils/ft_putnbr.c
 * 整数を文字列として出力
 * 再帰を使用して桁順を処理
 */
#include "minitalk.h"

void ft_putnbr(int n)
{
    char c;

    /* INT_MINの特殊処理 */
    if (n == -2147483648)
    {
        write(1, "-2147483648", 11);
        return ;
    }

    /* 負数の処理 */
    if (n < 0)
    {
        write(1, "-", 1);
        n = -n;
    }

    /* 再帰で上位桁を処理 */
    if (n >= 10)
        ft_putnbr(n / 10);

    /* 現在の桁を出力 */
    c = (n % 10) + '0';
    write(1, &c, 1);
}

ft_strlen.c - 文字列長とエラー処理

/* srcs/utils/ft_strlen.c
 * 文字列長の取得とエラー終了処理
 */
#include "minitalk.h"

int ft_strlen(const char *str)
{
    int len;

    if (!str)
        return (0);

    len = 0;
    while (str[len])
        len++;

    return (len);
}

/*
 * error_exit - エラーメッセージを表示して終了
 *
 * フェイルファスト設計:障害を検出したら即座に報告して終了
 */
void error_exit(const char *msg)
{
    if (msg)
        write(2, msg, ft_strlen(msg));
    write(2, "\n", 1);
    exit(1);
}

5.6 最適化と拡張

5.6.1 適応型遅延調整

ネットワーク輻輳制御のアルゴリズム(TCP Reno, 1990年)に着想を得た適応型遅延調整:

/* 適応型遅延調整
 * AIMD(Additive Increase, Multiplicative Decrease)に類似
 */
typedef struct s_adaptive_client
{
    volatile sig_atomic_t   ack_received;
    int                     delay;
    int                     success_count;
    int                     fail_count;
}   t_adaptive_client;

static t_adaptive_client g_adaptive = {0, 100, 0, 0};

/*
 * adjust_delay - 遅延の動的調整
 *
 * 成功時:遅延を段階的に減少(Additive Decrease)
 * 失敗時:遅延を急激に増加(Multiplicative Increase)
 */
void adjust_delay(int success)
{
    if (success)
    {
        g_adaptive.success_count++;
        g_adaptive.fail_count = 0;

        /* 10連続成功で遅延を減少 */
        if (g_adaptive.success_count >= 10 && g_adaptive.delay > 50)
        {
            g_adaptive.delay -= 10;  /* Additive Decrease */
            g_adaptive.success_count = 0;
        }
    }
    else
    {
        g_adaptive.fail_count++;
        g_adaptive.success_count = 0;

        /* 失敗時は遅延を増加 */
        if (g_adaptive.delay < 500)
            g_adaptive.delay += 50;  /* Multiplicative Increase */
    }
}

5.6.2 プログレスバー

ユーザーインターフェース設計の原則(Nielsen, 1993)に基づくプログレス表示:

/* プログレスバーの実装
 * カーソル制御文字を使用
 */
void print_progress(int current, int total)
{
    int     percent;
    int     bar_width;
    int     filled;
    int     i;
    char    bar[52];

    if (total == 0)
        return ;

    percent = (current * 100) / total;
    bar_width = 20;
    filled = (current * bar_width) / total;

    /* バーを構築 */
    i = 0;
    bar[i++] = '\r';  /* 行頭に戻る */
    bar[i++] = '[';

    /* 埋まった部分 */
    while (i - 2 < filled)
        bar[i++] = '#';

    /* 空の部分 */
    while (i - 2 < bar_width)
        bar[i++] = ' ';

    bar[i++] = ']';
    bar[i++] = ' ';

    /* パーセント表示 */
    if (percent >= 100)
        bar[i++] = '1';
    if (percent >= 10)
        bar[i++] = '0' + ((percent / 10) % 10);
    bar[i++] = '0' + (percent % 10);
    bar[i++] = '%';

    write(1, bar, i);
}

5.6.3 統計情報の収集

計測駆動開発の実践:

/* 統計情報の収集
 * パフォーマンス分析と最適化のため
 */
typedef struct s_statistics
{
    unsigned long   bits_sent;
    unsigned long   chars_sent;
    unsigned long   retries;
    unsigned long   errors;
    time_t          start_time;
    time_t          end_time;
}   t_statistics;

static t_statistics g_stats = {0};

void stats_init(void)
{
    g_stats.bits_sent = 0;
    g_stats.chars_sent = 0;
    g_stats.retries = 0;
    g_stats.errors = 0;
    g_stats.start_time = time(NULL);
}

void stats_print(void)
{
    unsigned long   duration;
    unsigned long   bps;

    g_stats.end_time = time(NULL);
    duration = g_stats.end_time - g_stats.start_time;

    write(1, "\n=== Statistics ===\n", 20);
    write(1, "Bits sent: ", 11);
    ft_putnbr(g_stats.bits_sent);
    write(1, "\nCharacters sent: ", 18);
    ft_putnbr(g_stats.chars_sent);
    write(1, "\nRetries: ", 10);
    ft_putnbr(g_stats.retries);

    if (duration > 0)
    {
        bps = g_stats.bits_sent / duration;
        write(1, "\nThroughput: ", 13);
        ft_putnbr(bps);
        write(1, " bits/sec", 9);
    }

    write(1, "\nDuration: ", 11);
    ft_putnbr(duration);
    write(1, " seconds\n", 9);
}

5.7 エラーハンドリングのパターン

5.7.1 防御的プログラミング

/* 安全なkill呼び出し
 * エラーコードに基づく適切な処理
 */
int safe_kill(pid_t pid, int sig)
{
    if (kill(pid, sig) == -1)
    {
        if (errno == ESRCH)
            write(2, "Error: Process not found\n", 25);
        else if (errno == EPERM)
            write(2, "Error: Permission denied\n", 25);
        else if (errno == EINVAL)
            write(2, "Error: Invalid signal\n", 22);
        else
            write(2, "Error: kill() failed\n", 21);

        return (0);
    }

    return (1);
}

5.7.2 回復機構

/* 文字レベルでのリトライ
 * 粒度の細かいエラー回復
 */
int send_char_with_recovery(pid_t server_pid, unsigned char c)
{
    int bit;
    int retry;

    bit = 7;
    while (bit >= 0)
    {
        retry = 0;
        while (retry < MAX_RETRY)
        {
            if (send_bit(server_pid, (c >> bit) & 1))
                break ;  /* 成功 */

            retry++;
            if (retry < MAX_RETRY)
            {
                write(2, "Retrying bit...\n", 16);
                usleep(1000);  /* 1ms待機 */
            }
        }

        if (retry == MAX_RETRY)
        {
            write(2, "Error: Failed after retries\n", 28);
            return (0);
        }

        bit--;
    }

    return (1);
}

5.8 テストとデバッグ

5.8.1 テストスクリプト

#!/bin/bash
# test.sh - 体系的なテストスイート

SERVER_PID=$1

echo "=== Basic Tests ==="
./client $SERVER_PID "Hello"
./client $SERVER_PID "World!"
./client $SERVER_PID "42"

echo ""
echo "=== Special Characters ==="
./client $SERVER_PID "Tab:	End"
./client $SERVER_PID "Line1
Line2"

echo ""
echo "=== Boundary Tests ==="
./client $SERVER_PID ""                              # 空文字列
./client $SERVER_PID "A"                             # 1文字
./client $SERVER_PID "$(python3 -c 'print("A"*100)')" # 長い文字列

echo ""
echo "=== ASCII Range ==="
./client $SERVER_PID "$(python3 -c 'import string; print(string.printable)')"

5.8.2 デバッグモード

/* 条件付きコンパイルによるデバッグ
 * -DDEBUGフラグで有効化
 */
#ifdef DEBUG
# define DEBUG_PRINT(msg) write(2, msg, ft_strlen(msg))
# define DEBUG_NUM(n) ft_putnbr(n)
#else
# define DEBUG_PRINT(msg)
# define DEBUG_NUM(n)
#endif

void debug_signal_handler(int signum, siginfo_t *info, void *context)
{
    DEBUG_PRINT("Signal received: ");
    DEBUG_NUM(signum);
    DEBUG_PRINT(" from PID: ");
    DEBUG_NUM(info->si_pid);
    DEBUG_PRINT("\n");

    signal_handler(signum, info, context);
}

5.9 参考文献

  • NATO Software Engineering Conference (1968). "Report on a conference sponsored by the NATO Science Committee, Garmisch, Germany"
  • Dijkstra, E. W. (1968). "Go To Statement Considered Harmful". Communications of the ACM, 11(3)
  • Brooks, F. P. (1975). "The Mythical Man-Month: Essays on Software Engineering". Addison-Wesley
  • Brooks, F. P. (1986). "No Silver Bullet: Essence and Accidents of Software Engineering". IEEE Computer
  • McIlroy, M. D. (1978). "Unix Time-Sharing System: Forward". Bell System Technical Journal, 57(6)
  • Pike, R. (1989). "Notes on Programming in C". Bell Labs internal document
  • Lamport, L. (1978). "Time, Clocks, and the Ordering of Events in a Distributed System". Communications of the ACM, 21(7)
  • McCulloch, W. & Pitts, W. (1943). "A Logical Calculus of Ideas Immanent in Nervous Activity". Bulletin of Mathematical Biophysics, 5
  • Kleene, S. C. (1956). "Representation of Events in Nerve Nets and Finite Automata". Automata Studies
  • Mealy, G. H. (1955). "A Method for Synthesizing Sequential Circuits". Bell System Technical Journal, 34(5)
  • Moore, E. F. (1956). "Gedanken-experiments on Sequential Machines". Automata Studies
  • Cristian, F. (1991). "Understanding Fault-tolerant Distributed Systems". Communications of the ACM, 34(2)
  • Nielsen, J. (1993). "Usability Engineering". Academic Press
  • 5.10 まとめ

    この章では、ソフトウェア工学の歴史的基盤からMinitalkの完全な実装までを学んだ:

  • ソフトウェア工学の誕生:1968年のNATO会議とソフトウェア危機への対応
  • 構造化プログラミング:Dijkstraの原則とUNIXの哲学
  • クライアント・サーバー理論:分散システムとIPCパターン
  • 有限状態機械:オートマトン理論とMealy/Moore機械
  • エラー処理理論:障害モデルと信頼性メトリクス
  • 完全な実装:モジュール化されたMinitalkシステム

次の章では、ボーナス機能(Unicode対応、複数クライアント、確認応答機構)の実装を学ぶ。