もう“非対応”なんて言わせない☆ TCPソケット通信でどこでもChatGPT

突然ですが...古い環境でChatGPTが動いたら、なんか楽しそうじゃないですか？

今回はOpenAI APIと通信してくれるリクエスト中継用TCPサーバーを実装して、レガシー環境で動くChatGPTクライアントを作ってみようと思います！

作ったものは以下のリポジトリで公開しています。

https://github.com/avaice/tcp-llm

---

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730661/ece13e81-6e17-4d78-9db6-a650d31c7877.png)


## TCPサーバーを作る

まずはOpenAIにリクエストを飛ばしてくれる中継TCPサーバーを作りましょう。
別に中継サーバーはHTTPサーバーでも良いのですが、TCPサーバーとして実装しておけばHTTPリクエストに対応できない環境でも通信できるので...！

以下はリクエストを中継してくれる最低限のTCPサーバーを実装するコードです。

```ts
import * as net from "node:net";
import { config } from "dotenv";
import OpenAI from "openai";

// 環境変数の読み込み
config();

// 環境変数からの設定取得
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const HOST = process.env.HOST || "0.0.0.0";
const DEFAULT_MODEL = "gpt-4.1-nano-2025-04-14";

// OpenAI APIクライアントの初期化
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// クライアントごとの会話履歴を保持するMap
const conversations = new Map();

// TCPサーバーの作成
const server = net.createServer((socket) => {
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
  console.log(`クライアント接続: ${clientId}`);

  // 新しいクライアント接続時に会話履歴を初期化
  conversations.set(clientId, [
    { role: "system", content: "あなたは役立つアシスタントです。" },
  ]);

  // データ受信時の処理
  let buffer = "";
  socket.on("data", async (data) => {
    // データをUTF-8文字列として解釈
    const chunk = data.toString("utf-8");
    buffer += chunk;

    // メッセージの終端を検出（改行文字を終端とする）
    if (chunk.includes("\n")) {
      const messages = buffer.split("\n");
      buffer = messages.pop() || "";

      for (const message of messages) {
        if (message.trim()) {
          console.log(`受信メッセージ: ${message}`);

          // 会話履歴をクリアするコマンド
          if (message.trim().toLowerCase() === "/clear") {
            conversations.set(clientId, [
              { role: "system", content: "あなたは役立つアシスタントです。" },
            ]);
            socket.write("会話履歴をクリアしました。\n");
            continue;
          }

          try {
            // ユーザーメッセージを履歴に追加
            const history = conversations.get(clientId) || [];
            history.push({ role: "user", content: message });

            // OpenAI APIにリクエスト送信
            const completion = await openai.chat.completions.create({
              model: DEFAULT_MODEL,
              messages: history,
            });

            // アシスタントの応答を取得
            const responseContent =
              completion.choices[0]?.message?.content ||
              "レスポンスがありませんでした。";

            // アシスタントの応答を履歴に追加
            history.push({ role: "assistant", content: responseContent });

            // 履歴が長すぎる場合、古いメッセージを削除（システムメッセージは保持）
            if (history.length > 11) {
              // システムメッセージ + 10メッセージ
              history.splice(1, 2); // 最古のユーザーとアシスタントのメッセージを削除
            }

            // 更新された履歴を保存
            conversations.set(clientId, history);

            // レスポンスをクライアントに送信
            socket.write(`${responseContent}\n`);
          } catch (error) {
            console.error("OpenAI API エラー:", error);
            socket.write("エラーが発生しました\n");
          }
        }
      }
    }
  });

  // クライアント切断時の処理
  socket.on("end", () => {
    console.log(`クライアント切断: ${clientId}`);
    conversations.delete(clientId); // メモリリーク防止
  });

  // エラー発生時の処理
  socket.on("error", (err) => {
    console.error("ソケットエラー:", err);
  });
});

// サーバー起動
server.listen(PORT, HOST, () => {
  console.log(`TCP/IPサーバーが起動しました - ${HOST}:${PORT}`);
});

```

データのやり取りにはXMLを使いました。JSONだとレガシーな環境で扱えないので😢

実際に実装したアプリケーションでは、これに加えてモデル選択機能などを追加しました。

作ったサーバーは、Raspberry Piに起動させておくことにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730661/f57e6bc1-c5cb-4460-ab15-201e5d338e2f.png)



## クライアントを作る

次に、実際にレガシー環境で動かすためのクライアントを作ってみましょう。
WindowsXPが入ったPCに移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730661/23d24f9f-e205-4112-8435-91885346786d.png)


クライアントを作るといってもやることは簡単で、それっぽいテキストボックスを用意して、そこに入力された内容をサーバーに送るだけです。

クライアントは日本語プログラミング言語「ひまわり」を使って開発します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730661/d10e7e0a-3f39-4978-b4fa-48de6b66c0b5.png)

```
サーバーIP=「192.168.11.81」
TCPポート=3000

母艦の、タイトルは、「ChatGPT」
ステータスバーを、作る
その、テキストは、「{サーバーIP}へ接続します」



サーバー接続処理
メインUI描画

ステータスバーの、テキストは、「モデルの取得中...」
「/models\n」を、TCP送信

会話履歴は、「」
会話履歴レンダリング


待機

＊サーバー接続処理
サーバーIPへ、TCP接続。

もし、TCP接続状態が、「接続中」ならば、（
    受信イベントに、TCP受信処理設定。
）
戻る

＊メインUI描画

新しい会話メニューを、作る
その、イベントは、新しい会話イベント
モデル変更メニューを、作る
その、イベントは、モデル変更イベント

会話履歴メモを、作る
その、レイアウトは、「全体」
その、編集は、いいえ
その、フォントサイズは、9
その、折り返しは、はい

メッセージパネルを、作る
その、レイアウトは、「下」
その、高さは、26
メッセージ入力エディタを、作る
その、レイアウトは、「全体」
その、キー押した時は、（
	もし、押されたキー=13{Enter}ならば、送信イベント
）
メッセージ送信ボタンを、作る
その、レイアウトは、「右」
その、フォントサイズは、10
その、幅は、50
その、テキストは、「送信」
その、イベントは、送信イベント
メッセージ入力エディタを、メッセージパネルへ、乗せる
メッセージ送信ボタンを、メッセージパネルへ、乗せる

メッセージ入力エディタに、注目

戻る

＊新しい会話イベント
「/clear\n」を、TCP送信
会話履歴は、「」
会話履歴レンダリング
戻る

＊モデル変更イベント

「どのモデルに変更しますか？\n現在は{利用中モデル}を使用しています。」を、利用可能モデルの、リストで選択
もし、それが、「」ならば、戻る
利用中モデルは、それ

「/model {利用中モデル}\n」を、TCP送信
戻る

＊会話履歴レンダリング
会話履歴メモは、会話履歴
戻る

＊送信イベント

もし、（メッセージ送信ボタンの、有効）が、いいえならば、戻る

メッセージ入力エディタの、文字コードをUTF8に変換
送信内容は、それ
会話履歴は、会話履歴＆「あなた: ｛メッセージ入力エディタ｝\n\n」
ステータスバーの、テキストは、「会話を送信中...」
送信内容＆「￥ｎ」を、TCP送信。
会話履歴レンダリング
メッセージ入力エディタは、「」
メッセージ送信ボタンの、有効は、いいえ

ステータスバーの、テキストは、「応答の待機中」
0.5秒後に、読込アニメーションへ飛ぶ、タイマー設定。

戻る

＊読込アニメーション
ステータスバーの、テキストは、（ステータスバーの、テキスト）＆「.」
0.5秒後に、読込アニメーションへ飛ぶ、タイマー設定。
戻る

＊受信イベント
タイマー解除。
ステータスバーの、テキストは、「応答の読込中...」

TCP文字列の、文字コードをSJISに変換。
レスポンスは、それ
レスポンスの、『response\command』を、XMLテキストデータ取得。
コマンドは、それ

コマンドで、条件分岐
「models」の時、（
	レスポンスの、『response\available_models』を、XMLテキストデータ取得。
    利用可能モデル=それ
    レスポンスの、『response\current_model』を、XMLテキストデータ取得。
    利用中モデル=それ
）
「clear」の時、（
	レスポンスの、『response\message』を、XMLテキストデータ取得。
    アンエスケープ処理。
    言う
）
「model_change」の時、（
	レスポンスの、『response\message』を、XMLテキストデータ取得。
    アンエスケープ処理。
    言う
）
その他の時、（
	レスポンスの、『response\model』を、XMLテキストデータ取得。
	モデルは、それ
	レスポンスの、『response\content』を、XMLテキストデータ取得。
	アンエスケープ処理。
	応答は、それ
	会話履歴は、会話履歴＆「ChatGPT(｛モデル｝): ｛応答｝\n\n------\n\n\n」
	会話履歴レンダリング
）

メッセージ送信ボタンの、有効は、はい
ステータスバーの、テキストは、「準備完了」
戻る

＊アンエスケープ処理（？を）
引数取得
それの、『&lt;』を、『<』に、置換
それの、『&gt;』を、『>』に、置換
それの、『&ap;』を、『&』に、置換
それの、『&quot;』を、『"』に、置換
それの、『&apos;』を、『'』に、置換
戻る
```

実際に起動させてみると、こんな感じです。ちゃんと会話ができています！

![2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730661/9cb5516a-fb51-4067-9f4a-3031dfec349d.jpeg)

IE6で動くFizzBazzも作ってくれました。
レガシー環境でのエラー解決やコーディング作業などでもChatGPTを活用できるのは普通に便利そうです。

ちなみに、このクライアントは動作環境的にはWindows98以降でなら動く想定なのですが、残念ながら自分の手元にWindows98が入っているPCがないので、誰か動かしてみてください＞＜

## もっとニッチな環境で動かす

WindowsXPでChatGPTを動かすのは、あっさり出来ちゃいました。拍子抜けです。
もっと変な環境で動かさないと皆さんも満足できませんよね？

### Zaurusで動かす

皆さんZaurusってご存知ですか？20年前くらいまでシャープが売っていたPDA（電子手帳）です。
Zaurusといっても機種は色々あるのですが、晩年に発売していたSLシリーズのZaurusはLinuxで動いていて、ガジェオタにとっては結構面白い端末です。

家に眠っていたZaurus SL-C760でChatGPTを使ってみたくなったので、Zaurus向けのクライアントを開発してみます。

言語は（開発環境のMacでも動くという意味で）デバッグのしやすいC言語で行います。
ただしZaurusに入っているGCCはバージョンがめちゃくちゃ古くて、version 2.95.1でした。1999/08/16リリースのバージョンです。当時の規格のgnu89に準拠したコードを書く必要があります。そんなのわからないので、ChatGPTと相談しながら実装しました。
また、特殊な環境のため外部ライブラリに頼ることもできず、XMLのパースなどは独自実装する必要がありました。

実際に作ったコードは、ここに貼ると長くなりすぎるので割愛します。

https://github.com/avaice/tcp-llm/tree/main/c_client

作ったコードをMacでビルドしてみました。

```zsh
c_client % make 
mkdir -p obj bin
gcc -Wall -pedantic -std=c89 -D_POSIX_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE -c src/client.c -o obj/client.o
gcc  -o bin/client obj/client.o
c_client %
```

ビルドできたっぽいです！動かしてみましょう。

```zsh
c_client % bin/client 
Connected to server (127.0.0.1:3000)
Enter a message (type '/help' for commands, 'exit' to quit):
> hello!

=== Server Response ===
Hello! How can I assist you today?


Enter your next message (type '/help' for commands, 'exit' to quit):
> 日本語は話せますか？

=== Server Response ===
はい、話せます。何かお手伝いできることはありますか？


Enter your next message (type '/help' for commands, 'exit' to quit):
> 
```

ちゃんと動いています！😊

では、コードを実機に持っていって動作を確かめてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730661/6d295acb-16ba-4b03-bb13-bed526697efe.png)

Warningは出ていますが、うまくビルドできていそうです。

動かしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730661/0bff672f-55c7-4409-8171-1603f72a950c.png)

おお、ちゃんと動いている！！！！
微妙に文字化けしていますが、ZaurusでChatGPTが使えています！すごい！

## まとめ

TCPを使ったソケット通信であれば、相当古い端末でも対応できると思うので移植性が高いなぁと思いました。もし他にも面白い移植ができたら教えてください。

やっぱり、古い端末で動かなさそうなものを動かすって楽しいですね...