百合小説をサーバクライアントプログラムとして実装したい

1. Introduction

経緯

ある梅雨の深夜,じんわりと汗を感じながら寝苦しさに唸っていると,次のような発想がお告げのように降ってきました.

「百合小説をRFC形式で記述しなさい」

私は百合作品に造詣が深いわけでもないため,まったく自分とは違った存在から接続されたような気がして,古代の巫女の畏れを感じました.
そんな突拍子の無さを感じつつも,その試みは自分にとっても興味深く映り,非常に興が乗ったのでさっそく翌日から取り掛かることにしました.

方針

RFC形式で小説を書く」としてしまうと,原稿ファイルをRFCのレイアウトにして文章を書いても達成できてしまうので,少し面白みが欠けているかもしれません.

そこで,お互いの恋愛関係とそのやり取りを記述できるような通信方式を考えてみることにしました.そのプロトコルでは散文でもやり取りを行えるような設計にしておきます.最終的にそのプロトコルに則ったサーバクライアントプログラムを作り,手動か自動でやり取りさせます.これにより,ちょうど往復書簡のような塩梅で,小説に近い形式で物語を作成できるのではないかと考えました.

設計したプロトコル

百合小説の実装のため,オリジナルの通信プロトコルSAPPを設計し,その仕様をRFC形式でまとめていくことにしました.
SAPP(Simple Affection Protection Protocol)は非常に簡単なプロトコルで,主に以下のルールで通信を行います.

  1. SAPPクライアントが魅力的で付き合いたいと思う相手(SAPPサーバ)に告白
  2. 告白方法は,「普通に告白」「曖昧に告白」「脅迫的に告白」の3種類から選択
  3. SAPPサーバはその告白に対し,受諾 / 保留 / 拒絶することができる
  4. 受諾された場合,SAPPクライアントがSAPPサーバとメッセージをやり取りできるようになる
  5. 保留された場合は通信を切断される
  6. 拒絶された場合は通信を切断されたのち,しばらくSAPPサーバと連絡を取れなくなる

以下に,作成したRFC風文書を公開します.なお,クオリティがクオリティなので影響力は微塵も無いと思いますが,少しでも本家にご迷惑をおかけするわけにはいかないので,数字は割り当てずに???としました.
RFCは技術文書ですので基本的に晦渋な言い回しを避ける方針を取りましたが,小説を書くというゴールを考えて,遊び心としてある程度小説的な表現も入れようとしています.

drive.google.com

記事の構成

2章ではRFC風文書を書くために勉強した,RFCについての基本的な事項について説明し,3章で設計したプロトコルの設計思想を紹介します.その後,4章でRFCに則って実装したプログラムを紹介し,5章で動作実験を行い,簡単に物語を創作できるかどうか確認します.

2. RFC(Request For Comment)

概要

RFC(Request For Comment)とは,IETF(Internet Engineering Task Force)という機関から発行される,インターネットに関する様々な技術・事柄をまとめた文書群のことです.再配布が可能という特徴があり,非常に容易に入手可能な文書です.

主に通信プロトコルの仕様を定義し標準化するための文書として利用されることが多いですが様々な種類があります.文書は表題もありますが,特に通し番号で区別されます.

ところで,Request For Commentという名前は,ARPANETの研究グループが様々なアイディアを出して公開したものの,論文として出せるようなものではなかったため,あくまでコメントを求むという意味合いで発表した時の名残のようです[RFC-HOWTO].

種類

RFC文書には複数のタイプがあります.そのうち,インターネット技術として標準化されたStandards Trackとそれ以外のNon-Standards Trackがあります.
Standards Trackの文書は以下の流れで
Internet Draft ==> Proposed Standard ==> Draft Standard ==> Standard
まずIETFなどで議論された後に,その文書はInternet Draftという状態になります.
その後ある期限内にIESGに承認されると,Proposed Standardとなり,この段階からRFC番号が振られるようになります.
その6か月後,IESGに承認されると,Draft Standardとなり,さらに4カ月以上が経過して承認を得ると,Standardという文書になり,新たにSTD番号が振られるようになります.

Non Standards Trackには以下の3つがあります.

  • Informational

業界において,有益な情報として認められた仕様

  • Experimental

研究機関や企業などが実現した仕様

  • Historic

代替技術などの登場により現在は使用されていない仕様

自分の文書をどこに置こうかと考えたのですが,後述のジョークRFCもExperimentalで発表されているのでこちらを採用します.

ジョークRFC

自分のような初学者にはRFCは固い技術文書というイメージがありますが,実態はそういう面ばかりではありません.例えば4月1日には,ジョークRFCという形でユーモアたっぷりなRFCのパロディ文書が発表されます.

この中で有名なのが,RFC 1149「鳥類伝送によるIPデータグラムの転送」[RFC1149]です.これは通信に鳩を用いた時の通信プロトコルを書いた文書です.初めて読んだときに,ユーモアが巧みで非常に感動しました.実際に鳩を使って実装してみようとした人もいるらしいです.

自分の設計したいプロトコルは実用性が皆無であるため,ゴールとしてジョークRFCのようなものを目指すことにしました.

ちなみにRFCで文芸作品を表現しているものは無いのかなと探してみたところ何件かありました.RFC 527[RFC527]では,ARPAWOCKYという詩が書かれています.また,RFC 1121[RFC1121]ではARPANETの20周年記念を祝したシンポジウムACT ONEにて,一部のスピーカーが発表した詩をまとめています.英語の詩の楽しみ方がまだ分からないのでクオリティは判断できませんが,韻の踏み方が好きでした.
他にもご存知の方がいらっしゃれば教えていただけますと幸いです.

RFCの読み方

RFCは再配布可能なので検索すればすぐに情報が出てきますが,公式には以下のRFC Editorが使われます.Referecesとして引用する際にはこちらのURLが使われています.
www.rfc-editor.org

ところで,RFCは公開された後に修正されることはなく,技術仕様に修正が必要な時は,また新たなRFC文書が作成されます.
この時に重要となるキーワードがUpdateとObsoleteです.例えば数字の若いRFCを探してみると,上部にUpdated ByやObsoleted Byという文字の後ろにRFC番号が書かれていることが分かります.
ここで,Updated By〇〇〇となっている場合は,現在参照しているRFC文書への追加情報が書かれたRFC番号が〇〇〇に入ります.
また,Obsoleted By〇〇〇となっている場合は,現在参照しているRFC文書を廃止させたRFC番号が〇〇〇に入ります.
つまり,もし最新情報を得ようと思うのならば,以下のような読み方になります.
Updated By:現在読んでいるRFCと合わせて,Updated Byに続くRFC番号の文書を読む
Obsoleted By:現在読んでいるRFCではなく,Obsoleted Byに続くRFC番号の文書を読む

RFCの書き方

RFCの書き方もまたRFC 7322[RFC7322]などで定義されています.今回は,RFC 7322とそれをUpdateするRFC 7997[RFC7997]を参考にして書き進めることにしました.
ところでこれら二つの文書には,ObsolteしているRFC 2223[RFC2223]に記載されていた,Post Scriptで作成する際のページレイアウトやフォントの情報などが盛り込まれていませんでした.
そのため,ページデザインはRFC 2223を参考にしました.

RFC 7322では,RFCに盛り込まなければならない章・節とその書き方の指針,また参考文献などの体裁についての情報が記載されています.
RFCは英語で書かれなければならないことが示されていますが,正直自分の英文は目も当てられない出来だと思うので,ここは日本語で書くことにしました.

拡張バッカスナウア記法

RFCではある規則を表現するため,拡張バッカスナウア記法(Augmented BNF:ABNF)が使われることがあります.この記法自体もRFC 5234[RFC5234]で定義されています.
自分の勉強のためRFC風文書にこちらを盛り込んだので,簡単に触れようと思います.不慣れなのでおかしいぞという箇所がありましたらご指摘いただければ幸いです.

ABNFでは,基本的なルールを<規則> = <仕様>という形式で定義していきます.
そうやって定義した規則を組み合わせて,多様な規則を表現することができます.

ここからは,実際にSAPPプロトコルのREQUESTコマンドをABNF形式で表現していくことにします.
以下にREQUESTコマンドの一例を示します.これは,ハグによってSAPPサーバに対して告白することを意味します.

REQUEST DECLARE HUG

このコマンドは以下のように構成されています.

<req_cmd_name><スペース><req_type><スペース><situation><改行>

ここで,req_cmd_nameに該当する文字列は"REQUEST"のみです.

一方,req_typeは候補の文字列が3種類あります.DECLARE,PRAY,FORCEです.DECLAREが普通の告白であれば,PRAYは踏ん切りのつかない,あるいは自覚のない迂遠な告白,FORCEは壁ドンなどで強引に迫ったり,はては監禁・拷問などで相手の心をへし折り服従させたりするような告白を意味します.

そしてsituationでは,相手にどのように好意を伝えたのかを,0~400文字のUTF-8の文字列によって表現できます.

しかしこうした規則は上述の構成を眺めるだけでは伝わりません.

そのため,それを表現するためにABNF形式に直していきます.

まず,req_cmd_nameを定義します.文字列の候補はREQUESTのみであるため,素直に次のように書きます.これは「req_cmd_nameは"REQUEST"という文字列である」という意味になります.

req_cmd_name = "REQUEST"

続いて,req_typeです.DECLARE,PRAY,FORCEの3種類がありました.このような時,ABNFでは「/」をつかって「選択」という操作を使えます.

req_type = "DECLARE / "PRAY" / "FORCE"

これは,「req_typeはDECLARE,PRAY,FORCEの3つのうちのどれかである」という意味になります.

最後に,situationです.situationは0~400のUTF-8文字から表現されますが,それをABNF形式で書き直すと以下のようになります.

situation = *400 ( UTF8-char )

これは「situationは0~400回,UTF8-charが繰り返された文字列である」という意味になります.UTF8-charはUTF-8の文字を意味します(RFC 3629[RFC3629]で規則が定義されています).ちなみに,カッコの中の規則は数式と同じで先に評価されます.

他にも,スペースと改行が必要でした.スペースはSPですが,改行はCRLFで表現できます.

これらを踏まえ,REQUESTコマンドを表現する規則req_commandは以下のようになります.下と同じようにすっきりしている上,記法に慣れれば想定している規則もきちんと伝わります.

req_cmd_name = "REQUEST"
req_type = "DECLARE / "PRAY" / "FORCE"
situation = * 400 (UTF8-char  /  SP)
req_command = req_cmd_name  SP  req_type  SP  situation  CRLF
<req_cmd_name><スペース><req_type><スペース><situation><改行>

3. Protocol

概要

ここからは設計したSAPPの詳細をまとめていきます.

SAPPの簡単なルールは冒頭で述べましたが,以下により詳細にルールを示します.なお,通信内容は全てSSL/TLS通信により暗号化されます.

  1. SAPPクライアントが魅力的で付き合いたいと思う相手(SAPPサーバ)に対してREQUESTプロトコルコマンドを用いて告白
  2. SAPPサーバに受け入れられると,SAPPクライアントとサーバ間でメッセージをやり取りできる状態に遷移
  3. メッセージをやりとりできる状態のみMESSAGEプロトコルコマンドを用いてメッセージを送信
  4. もしリクエストがSAPPサーバに拒絶 or 保留された場合は通信が終了
  5. 拒絶したSAPPサーバはDENYテーブルにIPアドレスを登録し通信をしばらく受け付けないようにする

ちなみにプロトコルの名前は,Sapphism(女性同性愛)などの単語の元になった古代ギリシャの女性詩人サッポーから取っています.
TwitterInstagramで検索してみたところ侮辱的な言葉ではなさそうだったので採用しましたが,もしも侮辱的な言葉であれば早急に取り下げます.

設計思想

プロトコルの最終目的は,自分たちの関係を秘密にしておきたい人が周囲の目を気にせずに安心してやりとりできることを実現することです.
ただし,あくまで秘密にしておきたいと思う人のためのものであり,その関係は大っぴらに見せるべきではないと主張するものではありません.

記事タイトルでは「百合」と標榜していますが,特に一般的な関係と差別化を図ったり,対象となる人物像を制限したりしないようにしました.恋愛はどんなものも恋愛であり,その形式に当人たちの属性は関係ないためです.
また,別れなどの悲しい出来事や,浮気や二股など醜い部分も表現できるものを目指し,より細かく関係性を表現できるようにします.

4. プロトコル実装

作ったRFCを基に,実際にプロトコルを実装しました.
プログラミング言語はGoを選択しました.自分はGoは触ったことがなかったのですが,ゴルーチンという非同期処理により,サーバプログラムを作成するのが非常に容易だと聞いたので利用することにしました.
以下が実装したプログラムになります.
gitlab.com


プログラムにおいて,SAPPクライアントからの告白に対して,SAPPサーバの対応を決めるところは,request_func関数とmessage_func関数の箇所になります.
例えば,以下のように実装することができます.
素直に告白されたら受け入れ,あいまいであれば保留,強権的であれば拒否するようにしています.
シチュエーション情報は何でも良いのですが,便宜的にオウム返ししています.
柔軟に対応を変えるには,相手から届いたシチュエーション情報を解析して対応を変えるような実装に変更する必要があります.
このプログラムを書き換えることで,サーバとクライアントの関係を記述し,小説の展開で通信できるのではないかと考えました.

func request_func(connInfo *ConnInfo, mtype string, situation string) int {
	
	resp_situation := situation
	if mtype == "DECLARE"{ //普通に告白されたら...
		response := fmt.Sprintf("+OK %d ACCEPT %s",OK_CODE, resp_situation) //告白を受け入れる(ACCEPT)
		sendCommands(connInfo.conn, response)
		if recvAck(connInfo) {
			sendCommands(connInfo.conn, "ACK")
			connInfo.status = MESSAGE
		}else {
			r := fmt.Sprintf("-ERR %d",ER_CODE4)
			sendCommands(connInfo.conn, r)
		}

	}else if mtype == "PRAY"{ //曖昧に告白されたら...
		response := fmt.Sprintf("+OK %d SUSTAIN %s",OK_CODE, resp_situation) //返事を保留(SUSTAIN)
		sendCommands(connInfo.conn, response)
		if recvAck(connInfo) {
			sendCommands(connInfo.conn, "ACK")
			return -1
		}else {
			r := fmt.Sprintf("-ERR %d",ER_CODE4)
			sendCommands(connInfo.conn, r)
		}

	} else if mtype == "FORCE" { //脅迫的に告白されたら...
		ip := paser_IP_from_conn(connInfo.conn)
		DENY_TABLE.AddTable(ip,time.Now()) //相手をブラックリストに入れて,
		DENY_TABLE.ShowTable()
		response := fmt.Sprintf("+OK %d DECLINE %s",OK_CODE, resp_situation) //相手を拒否する(DECLINE)
		sendCommands(connInfo.conn, response)
		if recvAck(connInfo){
			sendCommands(connInfo.conn, "ACK")
			return -1
		}else {
			r := fmt.Sprintf("-ERR %d",ER_CODE4)
			sendCommands(connInfo.conn, r)
		}
	} else {
		r := fmt.Sprintf("-ERR %d",ER_CODE1)
		sendCommands(connInfo.conn, r)
	}
	return 0
}

5. 実験

ここでは,実際にSAPPサーバとSAPPクライアントの間で通信を行い,百合小説のようなものを作れるかどうかを確認します.
物語を創作する技術は乏しいので,その利用用途でも使えるよね,ということを単に確認するだけにとどめようと思います.

設定ファイルの環境

SAPPサーバのsapp.confファイルを下記のように設定しました.Timeout系は全て単位は秒になります.

#コネクションのタイムアウト時間
ConnectionTimeout       60

#ACK待機のタイムアウト時間
AckTimeout              60

#bindするIPアドレス
IPAdress                127.0.0.1

#ポート番号
Port                    10000

#同時に通信可能な人数(何股までするか)
ListenLimit             3

#サーバにアクセス可能なアドレス
AllowAddresses          "127.0.0.1", "192.168.0.1/24"

#DENYテーブルの数
TableSize               10

#DENYテーブルから排除されるまでの時間(タイプではない相手から告白された時にどのくらいの間連絡を取らないようにするか)
TableTimeout            3600

SAPPサーバ・クライアントの背景

SAPPサーバとクライアントの設定を決めることにします.

SAPPサーバはあまり分かりやすく好意をぶつけられることが苦手です.
まじめな性格で,自分の好意と誠実に向き合っては,これは果たして好意なのだろうかと考え込む面倒臭い面があります.
また,自分の気持ちを認めることに気恥ずかしさを覚えており,素直じゃない言い方しかできません.
しかしSAPPクライアントとともにいる時間は気持ちが穏やかになり,ずっとこの時間が続けば良いのにと願うこともあります.

SAPPクライアントは普段明朗な性格ですが,告白するにあたっていざ本人を目の前にすると,茶化してしまう癖があります.
SAPPサーバに好意を抱いているものの,その気持ちを押し殺して,SAPPサーバとは友人関係のまま行くことにしようと密かに考えていました.
そう思っていたのに,ふとしたきっかけで,公園で二人きり,ベンチに座って話すことになりました.
SAPPクライアントは,月明りに照らされたSAPPサーバの横顔を見て,押し殺していた気持ちがあふれ出し,たどたどしく告白をしてしまいます.

そうした背景を踏まえ,以下のような実装を行いました.

func request_func(connInfo *ConnInfo, mtype string, situation string) int {
        resp_situation := situation
        if mtype != "DECLARE" && mtype != "PRAY" && mtype != "FORCE" {
                r := fmt.Sprintf("-ERR %d",ER_CODE1)
                sendCommands(connInfo.conn, r)
                return 0
        }

        if mtype == "PRAY" && strings.Contains(situation, "手を繋いだ") {
                resp_situation = "私は躊躇いながら手を握り返した"

                response := fmt.Sprintf("+OK %d ACCEPT %s",OK_CODE, resp_situation)
                sendCommands(connInfo.conn, response)
                if recvAck(connInfo) {
                        sendCommands(connInfo.conn, "ACK")
                        connInfo.status = MESSAGE
                }else {
                        r := fmt.Sprintf("-ERR %d",ER_CODE4)
                        sendCommands(connInfo.conn, r)
                }
                return 0
        }

        if strings.Contains(situation, "キスをした"){
                ip := paser_IP_from_conn(connInfo.conn)
                DENY_TABLE.AddTable(ip,time.Now())
                DENY_TABLE.ShowTable()
                resp_situation = "「ごめん、そういうのじゃないから」私はぐいと押し返した。"
                response := fmt.Sprintf("+OK %d DECLINE %s",OK_CODE, resp_situation)
                sendCommands(connInfo.conn, response)
                if recvAck(connInfo){
                        sendCommands(connInfo.conn, "ACK")
                        return -1
                }else {
                        r := fmt.Sprintf("-ERR %d",ER_CODE4)
                        sendCommands(connInfo.conn, r)
                }

        }
        resp_situation = "「じゃあ、また今度ね」と私はそそくさとその場を後にした"

        response := fmt.Sprintf("+OK %d SUSTAIN %s",OK_CODE, resp_situation)
        sendCommands(connInfo.conn, response)
        if recvAck(connInfo) {
                sendCommands(connInfo.conn, "ACK")
                return -1
        }else {
                r := fmt.Sprintf("-ERR %d",ER_CODE4)
                sendCommands(connInfo.conn, r)
        }
        return 0
}

func message_func(connInfo *ConnInfo, message string) int {
        var response string
        resp_message := message

        if strings.Contains(message,"もう少しこのままでも"){
                resp_message = "「いいよ」なんでもないように答えたつもりで、うまく声が出なかった。"
        }else{
                resp_message = "..."
        }
        if connInfo.status == MESSAGE {
                response = fmt.Sprintf("ACK")
        } else {
                response = fmt.Sprintf("-ERR %d",ER_CODE4)
        }
        sendCommands(connInfo.conn, response + " " + resp_message)
        return 0
}

実験結果

まずは成功パターンを見ます.

REQUEST PRAY 私はおずおずと手を繋いだ
[RECV] ==> +OK 0 ACCEPT 私は躊躇いながら手を握り返した
ACK
[RECV] ==> ACK
message 「もう少しこのままでもいいですか」
[RECV] ==> ACK 「いいよ」なんでもないように答えたつもりで、うまく声が出なかった。
ACK

次が断られるパターンです.SAPPクライアントは枕を濡らして眠ることでしょう.

REQUEST FORCE 「ねぇ」私は彼女に呼びかけ、キスをした
[RECV] ==> +OK 0 DECLINE 「ごめん、そういうのじゃないから」私はぐいと押し返した。
ACK
[RECV] ==> ACK
REQUEST PRAY 私は彼女の肩に頭を傾けた。「今日は帰りたくないなぁ」
[RECV] ==> +OK 0 SUSTAIN 「じゃあ、また今度ね」と私はそそくさとその場を後にした
ACK
[RECV] ==> ACK

それっぽく設計をすることができました.とはいえ,単純な解析なのでいくらでも悪戯は可能だと思いますが…

6. まとめ

非常に単純なプロトコルですが,自分のやりたかったことを実現することができました.
幾度となく,自分は何をやっているんだ?と我に返ってしまったので細かい部分で誤りがあると思いますがご容赦ください.
効率が悪い,セキュリティ的にまずい実装があると思うので細々と修正を加えていきます.

RFC風文書にも書いたように,ポート番号が広く知られている場合には,通信内容は分からなくとも,SAPPを利用していることがわかります.
このサービスは利用していることもセンシティブな情報となりうるので,個人的にはSAPP over HTTPSを実装してHTTPS通信に紛れさせようとしたのですが,モチベーションがいったん枯れてしまったのであきらめました.今後気が向いたら実装しようと思います.

ご意見・ご指摘等がありましたらよろしくお願いいたします.

7. 参考文献

[RFC-HOWTO] 瓜生聖,秋月昭彦(2004),『RFCの読み方 インターネット技術の公式仕様書』,株式会社すばる舎
[RFC1149] Waitzman, D., "Standard for the transmission of IP datagrams on avian carriers", RFC 1149, DOI 10.17487/RFC1149, April 1 1990, https://www.rfc-editor.org/info/rfc1149.
[RFC527] Merryman, R., "ARPAWOCKY", RFC 527, DOI 10.17487/RFC0527, May 1973, https://www.rfc-editor.org/info/rfc527.
[RFC1121] Postel, J., Kleinrock, L., Cerf, V., and B. Boehm, "Act one - the poems", RFC 1121, DOI 10.17487/RFC1121, September 1989, https://www.rfc-editor.org/info/rfc1121.
[RFC2234] Crocker, D., Ed., and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", RFC 2234, DOI 10.17487/RFC2234, November 1997, https://www.rfc-editor.org/info/rfc2234.
[RFC7322] Flanagan, H. and S. Ginoza, "RFC Style Guide", RFC 7322, DOI 10.17487/RFC7322, September 2014, https://www.rfc-editor.org/info/rfc7322.
[RFC7997] Flanagan, H., Ed., "The Use of Non-ASCII Characters in RFCs", RFC 7997, DOI 10.17487/RFC7997, December 2016, https://www.rfc-editor.org/info/rfc7997.
[RFC2223] Postel, J. and J. Reynolds, "Instructions to RFC Authors", RFC 2223, DOI 10.17487/RFC2223, October 1997, https://www.rfc-editor.org/info/rfc2223.
https://www.rfc-editor.org/info/rfc5234
[RFC5234] Crocker, D., Ed., and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234, DOI 10.17487/RFC5234, January 2008, https://www.rfc-editor.org/info/rfc5234.
[RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003, https://www.rfc-editor.org/info/rfc3629.