WebTransportでもプロトコル理解がしたい!

:::note warn
2023/12 この記事はだいぶ古く、理解が足りていないところが多々あります。
最近のWebTransportについては下記の記事にまとめてあります。
:::

https://qiita.com/alivelime/items/70d06a2bb8da697066ff

---

:::note info
2022/03/20 Go実装によるデモサーバーを作りました。
:::

https://qiita.com/alivelime/items/135f67d7fd56b0968785

---

### 前回のechoサンプルを少し改良しました。

[前回はechoサンプルを動くようにしましたが、](https://qiita.com/alivelime/items/07c2dc838abd08e816ba)
今回はHTML側に切断処理を入れ、WebTransportのセッションが閉じられた時にQUICのイベントログを出力するようにしました。
今回はそのログも合わせてプロトコルを理解していきます。

[ソースコードはこちら](https://github.com/alivelime/sample_webtransport_echo/echo)


# WebTransport? HTTP/3?? QUIC??? 何それおいしいの?

WebTransportに何となくワクワクと闇の気配を感じている皆様こんにちは。
今回は[前回](https://qiita.com/alivelime/items/07c2dc838abd08e816ba)のechoサンプルとドラフトを睨めっこしながらプロトコルについて理解していきたいと思います。
ここではChrome M97に実装されている WebTransport over HTTP/3について調べていきます。

### どうしてWebTransportが必要なの?

1. ブラウザで双方向通信がしたい
1. けどWebSocketはTCPベースだからリアルタイム通信とか向いてないよね?
1. え? WebRTCのデータチャネルがあるじゃないかって? あれはP2Pだし使いにくいんだよねー
1. けどUDPだけだとコネクションとかストリームの概念もないし、TCPとUDPのいいとこ取りのプロトコルが欲しいよね。
1. そんな問題を解決するためにGoogleがQUICを作っていたよ! IETFで標準化するからそれを使おう!
1. あ、直接QUICを使おうと思ったけど、標準化するしHTTP/3使うよ!
1. HTTP/3が何かって? HTTP/2で複数画像とかを通信していてパケロスすると詰まることがあるよね? (これをHead of Line ブロッキングという。ちなみにnetlifyの無料プランが遅いのもこれの影響だったはず??) それをQUICで解決するものさ!

多分、こんな感じかと思います。

# まずはレイヤー構造の確認

色々なプロトコルや用語が出てくるので、まずは基本となるレイヤー構造をまとめてみます。
WebTransportはHTTP/3を使い
HTTP/3はTLSで必ず暗号化され、かつQUICを使い
QUICは実装上の理由でUDPを使っています。

名称|WebSocket|WebRTC(メディア)|WebRTC(データ)|HTTP/2|WebTransport over HTTP3/
---|---|---|---|---|---
(標準化年)|2011|2014|2014|2015|2022予定
-|-|SRTP|SCTP|-|HTTP/3
暗号化|TLS(任意)|DTLS|DTLS|TLS(任意)|TLS1.3相当(必須)
トランスポート|TCP|UDP|UDP|TCP|QUIC(UDP)

さて、こうしてレイヤーを見てみるとWebRTC(メディア)には(S)RTPがあるのにWebTransportにはありません。
ということはビデオチャットをWebTransportでやろうと思ったら自前で処理する必要がありそうですね。

# 通信の全体像について

まずは**コネクション**、**ストリーム**、**フレーム**、**パケット**といったそれぞれのレイヤーの基本概念について確認していきましょう。
静的なデータ構造と、実際にパケットとしてやり取りされるデータ構造をそれぞれ図にしてみます。

## コネクションとストリーム

WebTransport及びHTTP/3ではQUICのコネクションを一つ作成します。
コネクションにはIDがあり、これで管理することでIPが変わってもストリームを維持できるようです。
ちなみにWebTransportは接続をセッションIDで管理します。(WebTransportをクローズしてもQUICのコネクションは維持されます。実装依存?)

またコネクションの中に複数のストリームがあり、QUICではストリームごとにパケットの順番や整合性を保証します。
そしてHTTP/3では決められたストリームをいくつか作成します。
HTTP/3での接続が完了したら、必要に応じてWebTransportのストリームを作成することができます。
そして、UDPのように順番保証のない(つまりストリームを使わない)datagramがあります。

```plantuml
left to right direction

actor browser
actor server

package QUICコネクション as connection {

  package "ストリーム2_Control Strem(HTTP/3)" {
    rectangle "HTTP/3で必ず一つ作成される制御用ストリーム\n閉じてはいけない" as control_stream_send
  }
  package "ストリーム3_Control Strem(HTTP/3)" {
    rectangle "上記の逆向きストリーム" as control_stream_recv
  }
  package "ストリーム(HTTP/3)" {
    rectangle "ヘッダ圧縮用のEncoder・Decoder\n単方向2本ずつで計4本"
  }
  package "ストリーム0(HTTP/3)" {
    rectangle "HTTP/3のヘッダー・データ通信用" as request
  }
  folder "WebStreamセッション" {
  package "ストリームn(WebTransport)" {
    rectangle "WebTransportで作ったストリーム"
  }
  rectangle "datagramはストリームに紐づかない\nWebTransportのdatagramも同じ"
  }
}

browser <--> request
request <--> server

browser --> control_stream_send
control_stream_send --> server
browser <-- control_stream_recv
control_stream_recv <-- server


```

### ストリームについて

QUIC、HTTP/3、WebTransport共にスリトームとはQUIC上のコネクションで複数扱うことのできる通信単位のことを指します。

#### QUICのストリームタイプ

QUICにはストリームの種類は双方向、単方向、クライアントもしくはサーバのどちらから開始されたかという区別があります。
これはストリームIDの下位4bitで区別されます。
IDが偶数だとクライアントから、奇数だとサーバーから。になります。


IDの下位4bit|from|type
---|---|---
0x00|client|双方向
0x01|server|双方向
0x02|client|単方向
0x03|server|単方向

実際にechoサンプルでWebTransportのセッションを張ると、以下のストリームIDが振られます。

ストリームID|用途|向き|方向
---|---|---|---
0|HTTP/3リクエスト・レスポンス|clientから|双方向
2|HTTP/3 Control Stream|clientから|単方向
6|HTTP/3 QPACK Encoder|clientから|単方向
10|HTTP/3 QPACK Decoder|clientから|単方向
3|HTTP/3 Control Stream|serverから|単方向
7|HTTP/3 QPACK Encoder|serverから|単方向
11|HTTP/3 QPACK Decoder|serverから|単方向


#### HTTP/3のストリームタイプ

HTTP/3はQUICのストリームを使用します。
クライアントからの双方向のストリーム(0)はHTTPのリクエストとレスポンス処理に使われます。
WebTransportの単方向と双方向ストリームはHTTP/3のストリームと同じですが、新しく別に作成されます。
そして、**単方向ストリームのみ**ストリームタイプを独自に定義しています。

key|Value
---|---
CONTROL|0x00
PUSH|0x01
QPACK Encoder|0x02
QPACK Decoder|0x03
WEBTRANSPORT|0x54


また、このパラメータはQUICストリームの最初のデータとして送られます。

```
Unidirectional Stream Header {
  Stream Type (i),
}
```

### WebTransportのストリームタイプ

そろそろ訳が分からなくなってきましたが、echoデモの通り3種類です。
HTTP/3のフレームタイプでストリームタイプを示すようです。

下位レイヤでの表現|Datagram|Unidirectional|Bidirectional
---|---|---|---
HTTP/3のstream type|なし|0x54|0x54
HTTP/3のframe type|0xffd277|0x54|0x41
QUIC|Datagram|Unidirectional|Bidirectional


## パケットとフレーム

ここではざっくりと通信されるデータのまとまりを書きます。
UDPはパケットを送信します。
その中にQUICのパケットがあります。このパケット単位でQUICは管理を行います。
QUICのパケットには複数のフレームがあります。これの用途をframe_typeで識別します(20種類ほど)
で、さらにややこしいのが、**QUICフレームの中にHTTP/3フレームが入ります**

図にするとこんな感じです。
制御用の短いメッセージはたくさん詰めて、データは分割して並行でガンガン送る感じです。


```plantuml

package UDPパケット as p1 {
  package QUICパケット1 {
    package "QUICフレーム2(stream)" {
      package "HTTP/3フレーム3(DATA)" {
        [Body]
      }
      package "HTTP/3フレーム2(HEADERS)" {
        [HTTPヘッダー]
      }
    }
    package "QUICフレーム1(stream)" {
      package "HTTP/3フレーム1(DATA)" {
        [WebTransport\nストリームデータ]
      }
    }
  }
}

package UDPパケット2 as p2 {
  package QUICパケット2 {
     package "QUICフレーム3" {
       [WebTransport\nデータグラム]
     }
  }
}
p1 --- p2
```

ちなみにHTTP Datagramの場合は逆に多重化したくないのでQUICのdatagramを使い、最大限データを送信できるようにしているらしいです。

### QUICのフレームとフレームタイプ

QUICフレームではACKやPINGなどの小さなメッセージやデータを扱います。
一つのパケットには複数のフレームが入り、パケット番号やヘッダーにオフセット情報があります。(詳しくは後述)
フレームタイプは20種類近くあるのでWebTransportに関係するところだけ抜粋します。

名前|値|説明
---|---|---
PADDING|0x00|サイズ調整のためのデータ
PING|0x01|応答要求
ACK|0x02|応答確認
CRYPTO|0x06|TLS関連
STREAM|0x08|アプリケーションデータ<br />HTTP/3とWebTransportのストリームはこれ
DATAGRAM|0x30|ストリームを使わないデータ<br />WebTransportのdatagramはこれ


### HTTP/3のフレームとフレームタイプ

HTTP/3にもフレームがあり、一つのQUICフレームに複数のフレームを含めることができます。
フレームには長さ、データ、ストリームならIDがあります。
単方向ストリームの場合は最初の4バイトがフレームタイプになり、双方向の場合はストリームタイプ(4byte) + フレームタイプ(4byte)になるようです。

ここでもWebTransport関係するものだけ上げておきます。
フレームが大きい場合は分割され、QUICフレームのoffsetなどでよしなに繋げられます。

名前|値|用途
---|---|---
DATA|0x00|データ
HEADER|0x01|HTTP/3のヘッダー<br />QUICフレームのヘッダとは別です
SETTING|0x4|最初のハンドシェイクで以下のフラグを確認します<br />WEBTRANSPORTとH3_DATAGRAM
WEBTRANSPORT_STREAM|0x41|双方向(Bidirectional)の時の値<br />単方向の時はStreamType.WEBTRANSPORT 0x54が使われます

### WebTransportのフレーム

HTTP/3の単方向ストリームにはストリームタイプとフレームタイプがあり、双方向の時はフレームタイプのみとなります。
ややこしいですが、WebTransportではHTTP/3フレームのフォーマットはドラフトの通り、
タイプ(u32)、セッションID(u32)、データとなります。

データレイアウト|datagram|単方向|双方向
---|--|---|---
4byte|session_id|0x54|0x41
4byte|data|session_id|session_id
それ以降|...|data|data


## ハンドシェイク

TCPは3ウェイハンドシェイクですが、TLSがあるとさらに1往復かかります。
そこでHTTP/3は1RTT(1往復)できるTLS1.3相当のハンドシェイクを行うらしいです。

```plantuml

actor client
actor server

client --> server: stream_id : 2 crypto(ClientHello)とSETTING
client <-- server: stream_id : 3 crypto(ServerHello)など
client --> server: stream_id : 0 \nHTTP/3 CONNECT リクエスト(new WebTransport(url))
client <-- server: stream_id : 0 response 200, (draft version header)


```

ハンドシェイクやTLS周りは複雑で追いきれないので、
今回はWebTransportで意識するHTTP/3のリクエスト周りから見ていきます。

# 実際のコードとログを追う

それでは実際にコードとログを見ながら確認して見ましょう。
以下の要点となるところを追っていきたいと思います。

- WebTransportが接続できるまで(HTTP/3リクエスト)
- datagram echo
- unidirectional stream echo
- bidirectinal echo 

## ログを見れるようにする

まずはPythonのサンプルでログを見れるようにします。
QuicLoggerというものをサーバー作成時に渡すとイベントログを記録してくれるようです。
ただし、to_dict()で出力しないといけないようなので、WebTransportセッションがクローズされたら出力するようにします。

[実際のコードはこちら](https://github.com/alivelime/sample_webtransport_echo/blob/master/py_server/server.py)

```python:server.py

# ロガーをインポート
from aioquic.quic.logger import QuicLogger


# ログ周りを初期化し
quic_logger = QuicLogger()
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

# セッションがクローズしたらログを出力するようにし
class WebTransportProtocol(QuicConnectionProtocol):
    ...
    def quic_event_received(self, event: QuicEvent) -> None:
        if isinstance(event, ProtocolNegotiated):
        ...
        elif isinstance(event, StreamDataReceived):
            if event.end_stream is True:
                pprint(quic_logger.to_dict())
        # WebTransportをcloseしてもすぐにはQUICコネクションは切断されない
        elif isinstance(event, ConnectionTerminated):
            print("terminated!")

# mainのサーバー起動のconfigに追加します。
if __name__ == '__main__':
　　
　　  configuration = QuicConfiguration(
        alpn_protocols=H3_ALPN,
        is_client=False,
        max_datagram_frame_size=65536,
        quic_logger=quic_logger,
    )


```

するとこんな感じのログが出力されます。

```python
             'events': [('0',
                         'transport',
                         'datagrams_received',
                         {'byte_length': 1250, 'count': 1}),
                        ('207',
                         'transport',
                         'parameters_set',
                         {'ack_delay_exponent': 3,
                          'active_connection_id_limit': 8,
                          'disable_active_migration': False,
                          'initial_max_data': 1048576,
                          'initial_max_stream_data_bidi_local': 1048576,
                          'initial_max_stream_data_bidi_remote': 1048576,
                          'initial_max_stream_data_uni': 1048576,
(以下略)
```

こんな感じで接続終了後にログが出力されます。
ただしこのログ、イベントのログであって通信ログではないのです。
また、HTTP/3のイベントも出るには出ますが、SETTINGフレームが出力されないなど痒いところに手が届きません。

ログの見方としては
- datagramが一番外側 : datagram sent / received
- 次にパケット単位 : packet sent / received
- 次にフレームを紐解く : frame_parsed
のようにデータ構造と階層を念頭において見ていくと良いと思います。

## HTTP/3のリクエスト

ではHTTP/3のリクエストとレスポンスを確認してみましょう。
まず最初に`datagram_received`とあり、219バイトのデータを受信したことがわかります。

```python
                        ('5785',
                         'transport',
                         'datagrams_received',
                         {'byte_length': 219, 'count': 1}),
```

次に`packet_received`とあり、二つのフレームを受信したことがわかります。それぞれstream_id 0, 10 です。
headerによるとこれらは`パケット番号5`として送信されたことがわかります。

```python

                        ('5806',
                         'transport',
                         'packet_received',
                         {'frames': [{'fin': False,
                                      'frame_type': 'stream',
                                      'length': 84,
                                      'offset': '0',
                                      'stream_id': '10'},
                                     {'fin': False,
                                      'frame_type': 'stream',
                                      'length': 103,
                                      'offset': '0',
                                      'stream_id': '0'}],
                          'header': {'dcid': '252925e0ab119a61',
                                     'packet_number': '5',
                                     'packet_size': 219,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
```

ストリーム10はヘッダ圧縮用のデータなので省きます。
`stream_id : 10`のフレームを紐解くと一つの`header`フレームと3つの`data`フレームが入っています。
headerは普段見慣れたHTTP/1.1とは少し形は違いますが、`:method`や`:origin`、`:path`などのヘッダーがあります。
HTTP/3のヘッダはコロンで始まり全て小文字です。
WebTransportでは`:protocol`を`webtransport`にする必要があります。
(残りの三つのデータは、、よくわかりません😭)

```python
                        ('6032',
                         'http',
                         'frame_parsed',
                         {'byte_length': '9',
                          'frame': {'frame_type': 'headers',
                                    'headers': [{'name': ':scheme',
                                                 'value': 'https'},
                                                {'name': ':method',
                                                 'value': 'CONNECT'},
                                                {'name': ':authority',
                                                 'value': 'localhost:4433'},
                                                {'name': ':path',
                                                 'value': '/counter'},
                                                {'name': ':protocol',
                                                 'value': 'webtransport'},
                                                {'name': 'sec-webtransport-http3-draft02',
                                                 'value': '1'},
                                                {'name': 'origin',
                                                 'value': 'http://localhost:8000'}]},
                          'stream_id': '0'}),
                        ('6040',
                         'http',
                         'frame_parsed',
                         {'byte_length': '12',
                          'frame': {'frame_type': 'data'},
                          'stream_id': '0'}),
                        ('6049',
                         'http',
                         'frame_parsed',
                         {'byte_length': '9',
                          'frame': {'frame_type': 'data'},
                          'stream_id': '0'}),
                        ('6053',
                         'http',
                         'frame_parsed',
                         {'byte_length': '64',
                          'frame': {'frame_type': 'data'},
                          'stream_id': '0'}),
```

### レスポンス200

HTTP/3のリクエストに対して200を返します。
(またドラフト中のみバージョンを示すヘッダーを含めます。リリース時には削除される為、ここでは省略します)
クライアントがこれを確認することでWebTransportの接続が管理消します。(WebTransport.readyが解決される)

サーバー側の動作としては受信時とは逆で、フレームを作って他のフレームと共にパケットに詰めて送信しています。
`stream_id : 0`のデータがレスポンスになります。

```python
                        ('7641',
                         'http',
                         'frame_created',
                         {'byte_length': '3',
                          'frame': {'frame_type': 'headers',
                                    'headers': [{'name': ':status',
                                                 'value': '200'}]},
                          'stream_id': '0'}),
                        ('7806',
                         'transport',
                         'packet_sent',
                         {'frames': [{'ack_delay': '486',
                                      'acked_ranges': [['4', '5']],
                                      'frame_type': 'ack'},
                                     {'fin': False,
                                      'frame_type': 'stream',
                                      'length': 1,
                                      'offset': '1',
                                      'stream_id': '11'},
                                     {'fin': False,
                                      'frame_type': 'stream',
                                      'length': 5,
                                      'offset': '0',
                                      'stream_id': '0'}],
                          'header': {'dcid': '',
                                     'packet_number': '5',
                                     'packet_size': 39,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
                        ('7807',
                         'transport',
                         'datagrams_sent',
                         {'byte_length': 39, 'count': 1}),
```

## datagramをechoしたとき

```plantuml
actor client
actor server

client --> server: "hello" on datagram
client <-- server: "hello" on datagram
```

上記で接続はできたので、実際にデータを送信した時の流れを確認して見ましょう。
`datagram`をブラウザから送ってみます。
ストリームは使わないため、`frame_type : datagram`のデータが来ています。('a'と1byteだけ送信したのでパディングでサイズ調整されています)

```python
                        ('7345530',
                         'transport',
                         'datagrams_received',
                         {'byte_length': 33, 'count': 1}),
                        ('7345582',
                         'transport',
                         'packet_received',
                         {'frames': [{'frame_type': 'datagram', 'length': 2},
                                     {'frame_type': 'padding'}],
                          'header': {'dcid': '9e4ed475f6755965',
                                     'packet_number': '7',
                                     'packet_size': 33,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
```

サーバー側ではすぐにechoしています。
datagramなのでこれだけです。

```python
                        ('7345939',
                         'transport',
                         'packet_sent',
                         {'frames': [{'frame_type': 'datagram', 'length': 2}],
                          'header': {'dcid': '',
                                     'packet_number': '6',
                                     'packet_size': 23,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
                        ('7345942',
                         'transport',
                         'datagrams_sent',
                         {'byte_length': 23, 'count': 1}),
```

## Unidirectional stream の echo

```plantuml
actor client
actor server

client --> server: "hello" on unidirectional stream 14
client <-- server: ack
client <-- server: "hello" on unidirectional stream 15
client --> server: ack

```

次にUnidirectional Streamでechoした時のログを追って見ます。

```python
                        ('29391394',
                         'transport',
                         'packet_received',
                         {'frames': [{'fin': False,
                                      'frame_type': 'stream',
                                      'length': 3,
                                      'offset': '0',
                                      'stream_id': '14'},
                                     {'frame_type': 'padding'}],
                          'header': {'dcid': '866a80df52137f04',
                                     'packet_number': '12',
                                     'packet_size': 33,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
```

ストリームなので受信したら`ack`で`packet_number : 12`を受け取ったことを送信しています。

```python
                      ('29392606',
                         'transport',
                         'packet_sent',
                         {'frames': [{'ack_delay': '1134',
                                      'acked_ranges': [['9', '12']], # ← これ
                                      'frame_type': 'ack'}],
                          'header': {'dcid': '',
                                     'packet_number': '9',
                                     'packet_size': 25,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
```

そしてすぐにechoの為の逆向きのストリームを作成してデータを送信しています。

```python
                        ('7155743',
                         'transport',
                         'packet_sent',
                         {'frames': [{'ack_delay': '103044',
                                      'acked_ranges': [['6', '9']],
                                      'frame_type': 'ack'},
                                     {'fin': True,
                                      'frame_type': 'stream',
                                      'length': 3,
                                      'offset': '0',
                                      'stream_id': '15'}],
                          'header': {'dcid': '',
                                     'packet_number': '10',
                                     'packet_size': 50,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
```

そしてクライアントからすぐに`packet_number : 10`に対して`ack`が帰ってきます。

```python
                        ('7181473',
                         'transport',
                         'packet_received',
                         {'frames': [{'ack_delay': '25424',
                                      'acked_ranges': [['5', '6']],
                                      'frame_type': 'ack'},
                                     {'frame_type': 'padding'}],
                          'header': {'dcid': '62897e69c651cbb4',
                                     'packet_number': '10',
                                     'packet_size': 33,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
```



## Bidirectional Stream の echo

```plantuml
actor client
actor server

client --> server: "test" on bidirectional stream 4
client <-- server: ack and "test" on stream 4
client ->> server: ack
```

最後に双方向ストリームのechoです。
概ね単方向と同じですが、ストリームが既に貼られている為、`ack`とechoデータの送信をまとめて行っています。

```python
                        ('7829309',
                         'transport',
                         'packet_sent',
                         {'frames': [{'ack_delay': '10389',
                                      'acked_ranges': [['6', '9']], # packet_number : 9 が送られてきたパケット番号
                                      'frame_type': 'ack'},
                                     {'fin': True,                  # ストリームを閉じる
                                      'frame_type': 'stream',
                                      'length': 4,                  # 返信するデータ
                                      'offset': '0',
                                      'stream_id': '4'}],           # クライアント開始の双方向なので0の次は4
                          'header': {'dcid': '',
                                     'packet_number': '7',
                                     'packet_size': 33,
                                     'scid': ''},
                          'packet_type': '1RTT'}),
```

# まとめ

まだまだ追いきれていないところが多く、仕様も理解しきれていないですが、
ログとコードを追うことである程度、用語や概念などが理解できたかなと思います。
本当はQUICフレームのデータレイアウトなども理解したいところなのですが、
日が暮れるどころか土日が終わってしまった為また来週にしたく思います。

こんな記事でも最後まで目を通してくれる方がいれば嬉しいです。
ここまでお読みいただきありがとうございました。

次は他言語でもWebTransportがしたい!

### 変更履歴

~~※2021/11/7現在、Canaryが`98.0`になり動作しましせん。
そのためChrome Dev 97.0で動作を確認しています。~~
レスポンスにヘッダを含める必要があるようです。下記を参照ください。

##### 2021/11/13追加 chromeの公式サンプルが動くようになりました! やったね!

[chrome公式サンプル](https://github.com/GoogleChrome/samples/tree/gh-pages/webtransport)はM97以降で動きます。
HTTP/3リクエストのレスポンスに `headers.append((b"sec-webtransport-http3-draft", b"draft02"))`というヘッダを返す必要があります。
返さない場合は、`net::ERR_PROTOCOL_NOT_SUPPORT` エラーがブラウザで発生し、エラ〜コード `0x10c = 268' がサーバーに送られてしまいます。

##### 2021/12/25追加 参考リンクを下記の記事一覧ページにまとめました。

[他のWebTransport関連記事はこちら](https://qiita.com/alivelime/items/70d06a2bb8da697066ff#%E4%BB%8A%E3%81%BE%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%9F%E8%A8%98%E4%BA%8B)