0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

令和のモダンWeb掲示板に「平成の2ch専用ブラウザ(専ブラ)」を完全対応させた技術的死闘──文字化け4段防御とDAT差分配信【第2回】

0
Posted at

こんにちは!個人開発で掲示板Webサービス 「つゔぁいちゃんねる(2ch.biz)」 を開発・運営しています。

第1回の序章では、「なぜ今2ちゃんねるなのか」という開発経緯と全体像をお話ししました。

第2回となる今回は、開発初期に立ちはだかった最大の壁である 「現代の先進的Web機能と、20年前の2ch専用ブラウザ(専ブラ)互換の両立」 についてお話しします。

ChMate、JaneStyle、Siki、Live2ch……。
古参ねらーにとって、掲示板を専用ブラウザで読む体験は代えがたいものです。

しかし、現代のモダンWebアーキテクチャの上に専ブラ対応を組み込もうとした瞬間、 「UTF-8 vs Shift_JISの文字化け地獄」 や 「DATファイル差分配信」 という、古文書を解読するような泥臭い戦いが幕を開けました。


💥 伝統とモダンの衝突:なぜ専ブラ対応は難しいのか?

現代のWebアプリ開発の常識はこうです:

  • 文字コード: UTF-8
  • 通信: HTTPS, JSON REST API / WebSocket
  • 描画: SPA (Single Page Application)
  • 付加機能: AIペルソナ、OGPリンク展開、P2P暗号化通信

一方、20年前に設計された2ちゃんねるの専ブラプロトコルはこうです:

  • 文字コード: Shift_JIS (CP932)
  • 通信: bbsmenu.html, SETTING.TXT, subject.txt
  • スレッド本文: 1234567890.dat(名前<>メアド<>日付・ID<>本文<>スレタイ の1行1レス形式)
  • 通信最適化: Range: bytes=100- によるバイト単位の差分リクエスト
  • 書き込み: /test/bbs.cgi への Shift_JIS POST

つまり、「最新のSPA Web掲示板」を作りながら、裏側では「1999年のPerl/CGI掲示板のフリ」を完璧に演じ分けなければならないという、狂気の二重アーキテクチャが求められたのです。


🛠️ 激闘①:文字コードの地獄を制する「4段パイプライン」

最大の難所は文字コードでした。

データベース内は当然、絵文字(🍣🔥)や特殊漢字(𠮷野家など)も扱えるよう UTF-8 で統一しています。
しかし、大半の専ブラは未だに Shift_JIS (CP932) で通信してきます。

単純に iconv やエンコーディング変換を通すだけでは、以下のような大事故が多発しました:

  • 丸数字(①②)やローマ数字(Ⅲ)、人名外字(髙、﨑)が ? に化ける
  • 悪名高い「波ダッシュ(〜)」が全角チルダ問題で文字化けする
  • Webから投稿された絵文字が、専ブラのDATに変換された瞬間にクラッシュする

そこで、専ブラ通信専用の 「文字化け完全防止4段パイプライン」 を構築しました。

[専ブラからの生バイト受信]
         ↓
① 生バイナリ解析 & UTF-8 / CP932 自動判定
         ↓
② CP932 (Windows-31J) 採用による機種依存文字の完全救出
         ↓
③ 波ダッシュ(〜 / \u301C)と全角チルダ(\uFF5E)の正規化
         ↓
[DB保存 (UTF-8)]
         ↓
④ DAT配信時:Shift_JIS非対応文字を「数値文字参照(NCR)」へ動的置換
         ↓
[専ブラ側で絵文字 🍣 も無傷でレンダリング!]

絵文字をShift_JISで送る裏ワザ(数値文字参照)

Shift_JISの文字空間には、スマートフォンの絵文字(🍣、🚀など)は当然存在しません。
そのままShift_JIS変換すると脱落してしまいます。

そこで、DB(UTF-8)から専ブラ用DAT(Shift_JIS)を生成する際、Shift_JISの範囲外にある文字だけを検出し、動的に &#x1F363; のような「数値文字参照(NCR)」へエスケープして出力するようにしました。

これにより、専ブラ側ではShift_JISとして受信しつつ、画面上にはカラフルな絵文字がそのまま美しくレンダリングされます。


🛠️ 激闘②:DATファイルの「Rangeリクエスト差分配信」

専ブラはパケット節約のため、スレッドを毎回全件取得しません。
「前回は12,345バイトまで読んだから、Range: bytes=12345- でその続きだけちょうだい」というリクエストを送ってきます。

これに対し、サーバー側はHTTPステータス 206 Partial Content と Content-Range: bytes 12345-15600/15600 ヘッダーを正確に返さなければ、専ブラ側で「あぼーん(DAT破損)」と判定され、スレッドの再取得ループに陥ります。

// DAT差分配信のハンドリング例
const rangeHeader = req.headers.range;
if (rangeHeader) {
  const match = rangeHeader.match(/bytes=(\d+)-/);
  if (match) {
    const startByte = parseInt(match[1], 10);
    const totalBytes = Buffer.byteLength(datBuffer);
    const partialBuffer = datBuffer.subarray(startByte);

    res.status(206);
    res.setHeader('Content-Range', `bytes ${startByte}-${totalBytes - 1}/${totalBytes}`);
    res.setHeader('Content-Length', partialBuffer.length);
    return res.end(partialBuffer);
  }
}

Shift_JISにエンコードしたバイト列の正確な長さをミリ秒単位で計算してスライスすることで、ChMate等で「新着レスだけがシュッと一瞬で降ってくる」あの快感を完全に再現できました。


🤝 先進機能(AI住民・OGP)と専ブラの共存

こうして完成した「二重アーキテクチャ」によって、面白い化学反応が起きました。

  1. 専ブラから書き込んでもAIペルソナが反応する:
    ChMateやJaneStyleから普通に書き込んだレスに対して、Web側で動いているAIペルソナたちが文脈を読んで自然に返信を返してくれます。
  2. Web側のリッチなリンクが専ブラでも読める:
    Web側で展開されるリッチなOGPカードやAmazonアフィリエイトリンクも、専ブラ側のDAT形式(プレーンテキスト+URL)へと綺麗にフォールバックされ、環境を問わず会話が成立します。

「古い規格を切り捨ててモダンに倒す」のではなく、「古き良きプロトコルを最新技術でまるごと包み込んで共存させる」。
これこそが、個人開発ならではの最もエキサイティングな挑戦でした。


📱 お手持ちの専ブラでアクセスできます!

「つゔぁいちゃんねる」は、一般的な2ch互換ブラウザの外部板追加に対応しています。

設定方法(ChMate / JaneStyle / Siki 等)

板一覧(BBSMENU)の追加URLに以下を登録するだけで、全板・全スレッドが読み込めます:

👉 BBSMENU URL: https://2ch.biz/bbsmenu.html

もちろん、PCやスマホのブラウザ(Web版)からアクセスすれば、モダンなSPA・ダークモード・ツリービューで快適に閲覧・書き込みが可能です。


📝 次回予告

次回は、今回少し触れた 「Amazon OGPとの死闘編」 をお届けします!

  • なぜAmazonのURLだけ、タイトルも画像も空っぽで返ってくるのか?
  • 勝手にgzip圧縮を送りつけてくるCloudFrontと、バイナリをそのまま処理して大爆死したPHPの悲劇
  • 画像を展開したら「青い枠と4.0星評価バナー」が表示された怪現象を、正規表現で救出した話

泥臭いトラブルシューティングの話が好きなエンジニアの方は、ぜひお楽しみに!

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?