[作って学ぶ]ブラウザの仕組みを読んで HTTPクライアント実装してみた

874 Views

May 12, 25

スライド概要

HTTPクライアントをmacOS上でRustを用いて実装した経緯やプロセスについて説明しています。Implementing a simple GET request using HTTP/1.1 standards, 主に技術書を参考にしながらも、一部機能に関してはChatGPTのサポートを受けています。また、実装中に直面した課題や、HTTPプロトコルの背景についても触れています。最後に、AIを活用した開発経験についても報告し、Rustに不慣れであったにもかかわらず、学習を自身で進めた過程を明らかにしています。

profile-image

札幌のウェブエンジニア

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

[作って学ぶ]ブラウザの仕組みを読んで HTTPクライアント実装してみた n13u / #low_layers_sapporo / 2025.05.11

2.

自己紹介 ● ● 2002年生まれ、札幌市みてきて北区生、あつべつ・く育、市電のふるさと中央区在住 Nishimura Wataru(_n13u_) ○ ○ ○ ○ ● 最近のお仕事 ○ ● ECMAScript 好きなWebフロントエンドフレームワーク ○ ● Next.js App Router 移行 好きな言語 ○ ● ちょっと株式会社 受託事業部 第二ユニットのエンジニアをやってます. 札幌市事業Sapporo Engineer Base 運営 一般社団法人未完 代表理事(予定) 他開発の副業お仕事 etc… Next.js(App Router) 最近ハマっていること ○ CANDY TUNE / こっちゃん 2

3.

今日する話 ● ● ● [作って学ぶ]ブラウザの仕組み HTTPクライアントを実装した話 macOS w/Rust でHTTPクライアント作った

4.

HTTPクライアント作ったよ 4hくらい?

5.

昨日(資料とセットで)作りました (今日もちょっとやった) ● ● シーシャ吸いながら作ってました(キラキラ系アピール) 本読んだだけの話にしようかなと思ったけど、“甘い”と思いやめました

6.

https://github.com/WataruNishimura/rusty-http

7.

ところで、低レイヤー...? ● ● ● どこからが“低”なのか.. 自称プロフロントエンドエンジニアなので、HTTPは低めではある 「ネットワークから作ろうな」 Web Application ーーーーーー Browser ーーーーーー HTTP, browser engines, etc. 👈ChatGPTに作らせたOSIとTCP/IPの画 像

8.

[作って学ぶ]ブラウザの仕組み ● ● ● ● ● 積読してた 技術評論社から出ている書籍 ブラウザの仕組みと銘打ち、HTML/CSSのパースとか網羅的に知識をカバーしている Rustで書くことが前提👈いいね 書籍で出てくるブラウザはWasabiOSというhikaliumsさん作成の自作OS(!?)の上で動くことを 目標に作られている ○ ○ ● https://github.com/hikalium/wasabi Web Browser + sabi(rust)でwasabiらしい、おしゃれすぎ 今回のhttpクライアントは第三章の実装をほぼ参考に作りました ○ macOS向けの実装だけはChatGPT先生とかに聞いてます https://gihyo.jp/book/2024/978-4-297-14546-0

9.

今回作ったもの ● ● ● HTTP 1.1準拠(らしい)でGETメソッドだけを実装したもの 標準入出力に値を入れたら返ってくるだけのもの もちろんリダイレクトとかはしていない((( ○ ● RFC9011だと推奨らしいのでRFC上はなくてもいいらしいけど、ないと使えないよね WasabiOSのインストールが面倒だったのでmacOS上で動くように ○ ○ ○ ちょこちょこ実装が異なります WasabiOSではRustのstdライブラリが使えないが、実装の都合上macOSで使うのでstdを使うように DNSライブラリ、TCPやurlパーサーが著者提供のものからstdかcrate.io経由で使うように

10.

ちなみに

11.

HTTPのGETだけなら 実装は簡単 ● HTTPはテキストベースのプロトコル ○ ○ ○ 空白と改行で構造を作っている 詰まるところ、文字列操作がメイン 文字コードは慣習的にUTF-8になっている ■ ■ ■ ● もとは、ISO-8859-1(Latin-1)が標準 本来はContent-TypeかHTMLで指定されたchartsetを利用する HTMLも基本的にはUTF-8なので問題ない なので、TCP接続を作成して文字列をバイトコードとしてやり取りするだけ

12.

簡単だね!

13.

そんなわけはない ● URLのパーサーは? ○ ○ ● ● ● ● ● ● Clineに書かせればおk ちゃんと第二章で書いてるので、良い子のみんなは第二章からやろう リダイレクト処理はRFCだと推奨だけど慣習的には必須 文字コードだって本来はしっかりとデータの中身を読んで判別するべき HTTPのバージョン切り替えやHTTP/3のQUIC対応、 RFCを踏襲した実装とは程遠い...orz 我々が普段使っているブラウザは相当なコストをかけて作られている 動けばいいコードなら確かに早いかも ○

14.

とりあえず動かしてみる

15.

という感じ

16.

ざっくりコード解説

17.

コード解説 ● ● ● メインの処理は src/http.rs で書いてる get関数の中に全て詰めている 他のファイルは必要な構造体とかだけを出している ○ response.rs でレスポンスの定義と実装だけしている

18.

流れ ● ● DNS名前解決 TCP接続作成 ○ ● ● ● ライブラリに任せるとSYN/ACKとかやらなくて良い バッファを読み込み 文字列ゴニョゴニョ 出力

19.
[beta]
DNS名前解決
●
●

ホスト情報から名前解決を行う
今回はTrust-DNSを利用
○

●

非同期でやるとやや面倒だったので同期で実装(((
○

●

本の方はなんとWasabiOS用のDNSライブラリがあります!?
std環境でやるなら大変じゃないかも、no-stdだとasync/awaitが大変

今回はもらったIPアドレスのうち0番目を利用

let resolver = Resolver::new(ResolverConfig::default(),
ResolverOpts::default()).unwrap();
let ips = match resolver.lookup_ip(&host) {
Ok(response) => response.iter().collect::<Vec<IpAddr>>(),
Err(_e) => {
return Err(Error::Network("Failed to find IP addresses".to_string()));
}
};

20.
[beta]
TCP接続作成
●
●

ソケットアドレスをIPとportから作成
TCP接続を作成
○

本の方だとこれも自作ライブラリのはず

let socket_addr = SocketAddr::new(ips[0], port);

// TCPコネクションを構築
let mut stream = match TcpStream::connect(socket_addr) {
Ok(stream) => stream,
Err(_e) => {
return Err(Error::Network("Failed to connect to
server".to_string()));
}
};

21.
[beta]
GETリクエストを作成
●

リクエストラインと呼ばれる文字列を作成(リクエストの1行目)
○
○
○

Method SP Path SP Version
あとは全部ヘッダー(CRLF改行)
最後にCRLFを2回入れることでヘッダー部の終わりを示せる
let request_line: String = format!("GET {} HTTP/1.1", path);

let headers: Vec<String> = vec![
format!("Host: {}", host),
"Connection: close".to_string(),
"User-Agent: RustyHttp/0.1".to_string(),
"Accept: */*".to_string(),
];

let request: String = format!("{}\r\n{}\r\n\r\n", request_line,
headers.join("\r\n"));

22.
[beta]
作成したリクエストをTCPソケットに送る
●
●
●

作成した文字列をバイト列にして書き込み
write_allにすると書き込みが完全に終わるまで待ってくれる
最後に、shutdownをして終了を宣言しないとレスポンスが返ってこない

match stream.write_all(request.as_bytes()) {
Ok(_bytes) => (),
Err(_e) => {
return Err(Error::Network("Failed to write to
server".to_string()));
}
};
stream.shutdown(std::net::Shutdown::Write).ok();

23.
[beta]
UTF-8にして終わり
●
●

receivedをバイト列からUTF-8の文字列にして終了
本来であればContent-Typeとかをみたり、bodyに含まれるHTML内のchartsetをみるべきなの
で、http通信のクライアントとしては責務が大きすぎるかも

match core::str::from_utf8(&received) {
Ok(response_str) => {
let response =
HttpResponse::new(response_str.to_string());
Ok(response)
}
Err(_e) => {
Err(Error::Network("Failed to parse
response".to_string()))
}
}

24.

最後に

25.

AI駆動開発の勘所 ● ● ● 書籍ベースだったので大して苦しまなかった 他言語の経験はあるけど、Rustの経験がほぼない状態だった Clineとかのエージェントにやらせると学習の意味がなくなる ○ ○ ● わからない概念が多々出てくるけど、それらはChatGPT先生に聞けば解決する ○ ○ ● ● ただし、本質からそれるErrorの列挙型の実装とかは任せちゃった あとURLのパース処理も最低限頑張ってくれるやろと思い... 借用とか、所有権とか あとそもそものRustの構文とか... ライブラリの検索も多分ChatGPTで事足りた RFCの検索は、どういうRFCがある?って聞いて参照しつつ実際は本物を見た方が良さそう