Helmの基本的な使い方
chartの検索
helm search repo <repo名>
helm search hub <hub名>
% helm search hub bitnami
repoはローカル環境に追加されたレポジトリ。
インストールしたいchartを探し、それがローカルのレポジトリにない場合は、そのレポジトリをローカルに落としてくる必要がある。
repoの確認
% helm repo list
repoの追加
helm repo add <repo名>
% helm repo add bitnami https://charts.bitnami.com/bitnami
追加後は定期的に、レポジトリのupdateが必要。この辺りはaptとかhomebrewとかと同じ概念。
% helm repo update
デプロイ
helm install <リリース名> -f custom-values.yaml <chart名>
helm install <リリース名> --set key=value <chart名>
% helm install sample-nginx -f custom-values.yaml bitnami/nginx % helm install sample-nginx --set imagePullPolicy=Always bitnami/nginx
テンプレートに対してのデフォルトの設定がvalues.yamlに記述されている。直接values.yamlを編集しても良いが、上書きしたい設定を別ファイルにして-fで指定するのが一般的。
更新
% helm upgrade sample-nginx -f values.yaml bitnami/nginx % helm upgrade sample-nginx --set imagePullPolicy=Always bitnami/nginx
helmによりデプロイされたリリースの確認
% helm list
リリースの詳細情報
Usage: helm get [command] Available Commands: all download all information for a named release hooks download all hooks for a named release manifest download the manifest for a named release notes download the notes for a named release values download the values file for a named release
# マニフェストを確認 helm get manifest sample-nginx # chartに渡したvaluesを確認 helm get values sample-nginx
チャート自体の詳細確認
Usage: helm show [command] Aliases: show, inspect Available Commands: all show all information of the chart chart show the chart's definition crds show the chart's CRDs readme show the chart's README values show the chart's values
# チャートに設定可能なvaluesの確認
% helm show values bitnami/nginx
history確認
helm history <リリース名>
% helm history sample-nginx
helmはhistoryを管理するので、REVISIONを確認することができるし、rollbackすることもできる。
rollback
helm rollback <リリース名> <REVISION>
% helm rollback sample-nginx 2
ロールバックしても、ロールバック対象のリビジョンが新しいリビジョンとしてデプロイされるので注意。
アンデプロイ
helm uninstall <リリース名>
% helm uninstall sample-nginx
historyも全て削除されるので、rollbackで戻せない。
historyを残したい場合は、--keep-historyをつける。
既存のchartをダウンロードする
helm pull <chart名>
% helm pull bitnami/nginx % tar xvzf nginx-9.9.3.tgz # 色々編集する % helm install sample-nginx ./nginx
Homebrewでよく使うコマンド
homebrew自体のupdate, formulaeリストの更新
% brew update
インストールされているformulaeの一覧
% brew list
% brew list --versions
インストール可能なformulaeの取得
% brew search <formulae名> % brew search mysql ==> Formulae automysqlbackup mysql++ mysql-client@5.7 mysql-sandbox mysql@5.6 mysqltuner mysql ✔ mysql-client mysql-connector-c++ mysql-search-replace mysql@5.7 qt-mysql
チェックマークが入っているものはインストール済み。 思ったよりインストールできるバージョンが少ないですね、細かくバージョン指定したい場合は直接ダウンロードする感じかな。
formulaeの詳細情報
% brew info <formulae名> % brew info mysql mysql: stable 8.0.28 (bottled) Open source relational database management system https://dev.mysql.com/doc/refman/8.0/en/ Conflicts with: mariadb (because mysql, mariadb, and percona install the same binaries) percona-server (because mysql, mariadb, and percona install the same binaries) /usr/local/Cellar/mysql/8.0.25 (301 files, 301.5MB) * Poured from bottle on 2021-05-20 at 01:52:38 From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/mysql.rb License: GPL-2.0-only with Universal-FOSS-exception-1.0 ==> Dependencies Build: cmake ✘, pkg-config ✔ Required: icu4c ✔, libevent ✔, libfido2 ✘, lz4 ✔, openssl@1.1 ✔, protobuf ✘, zstd ✔ ....
formulaeであるmysqlは最新が8.0.28で、今インストールされているのは8.0.25なので、アップデートできる。
% brew upgrade mysql
# インストール済みのformulaeを全部最新にしたい場合
% brew upgrade
インストールされていない場合は、Not installedと出る。
% brew info mysql@5.7 mysql@5.7: stable 5.7.37 (bottled) [keg-only] Open source relational database management system https://dev.mysql.com/doc/refman/5.7/en/ Not installed From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/mysql@5.7.rb License: GPL-2.0-only ==> Dependencies Build: cmake ✘ Required: openssl@1.1 ✔ ...
brew infoはインストール済みのパッケージの情報ではなく、現在のバージョンのhomebrewが管理できる全formulaeに関する情報を取得できる。
つまりこれ。https://formulae.brew.sh/formula/
今pcにインストールされているhomebrewが、この最新のformulaeのリストを参照するようにbrew updateをまずするのが基本だと思うが、今は、brew installすると、
Running `brew update --preinstall`...
と最初に出るので勝手にやってくれるみたい。
バージョン切り替え
最新版から(8.0系)から5.7系のmysqlに切り替えたいとする。一旦現在のformulaeをunlinkしてからlinkを張る。
% brew install mysql@5.7 % brew unlink mysql && brew link mysql@5.7 --force
使われてないformulaeの削除
% brew cleanup
Remove stale lock files and outdated downloads for all formulae and casks, and remove old versions of installed formulae. If arguments are specified, only do this for the given formulae and casks. Removes all downloads more than 120 days old. This can be adjusted with HOMEBREW_CLEANUP_MAX_AGE_DAYS.
120日以上の前のダウンロードが対象みたい。--dry-runができるので確認してから消す方が良さそう。
requestAnimationFrameで端末負荷を測定する
背景
最近ブラウザから端末負荷を測定したい場面に出くわしました。 弊社ではwebrtcを扱っていて、ライブ中の端末負荷の変化を測定したかったのですが、残念ながらブラウザAPIは用意されていません。
そこでrequestAnimationFrameapiを使用して画面の描画回数を測定することで、間接的に端末負荷の指標とすることにしました。
Window.requestAnimationFrame()
requestAnimationFrameは本来はアニメーションを実装するために使用するAPIです。
Window.requestAnimationFrame() - Web API | MDN
ブラウザにアニメーションを行いたいことを知らせ、指定した関数を呼び出して次の再描画の前にアニメーションを更新することを要求します。
ブラウザが画面を描画するたびにコールされ、そのタイミングで任意の関数を実行したい場合に使用するのが一般的かと思います。
今回は、ブラウザが画面を描画するたびにコールされることを利用して、描画1回あたりに要した時間を計測しています。 1回あたりに要した時間が分かれば、画面描画のfpsも計算することができます。 つまり、端末負荷が上がって画面がカクカクする状態になればfpsも下がることを利用して、端末負荷の指標としましました。
正常時の基準とべきfpsは60ですが、注意も必要です。
このコールバックの回数は、たいてい毎秒 60 回ですが、一般的に多くのブラウザーでは W3C の勧告に従って、ディスプレイのリフレッシュレートに合わせて行われます。
特にゲーミング用のディスプレイなどはリフレッシュレートが高く設定されている場合もあり、必ずしも60を基準にできるとは限りません。
そもそももっと直接計測する方法はないのか?
google meetはどうやって端末負荷を計測しているのか?
googleの公式ではないので確かではないですが、chrome extensions apiを使用していると思われます。
javascript - How is Google Meet able to show CPU usage? - Stack Overflow
試しにgoogle meetをchrome以外のブラウザでアクセスしてみると、cpu使用率のグラフが表示されなくなっています。 かなり有力そうです。
残念ながらextensionsのapiをfrontendのコードから呼び出すことはできないので断念しました。 googleは外部に公開していない内部apiを通じて取得しているのではなかろうかと推測されます。
実装
requestAnimationFrame()のコールバック関数の中で、再起的にrequestAnimationFrame()を呼び出しています。 60回ごとに平均を計算しています。(60はあくまで目安です)
ポイントはInfinityです。理由が分かっていないのですが、時々lastCalledTimeとcurrentTimeが一致してしまうことがあり、fpsがInfinityになってしまいます。
これを避けるために、Infinityを除外する処理を加えています。
const measureFps = () => { let lastCalledTime: number | undefined; let counter = 0; let fpsArray: number[] = []; const calcFps = () => { const currentTime = Date.now(); if (lastCalledTime === undefined) { lastCalledTime = currentTime; } else { const delta = (currentTime - lastCalledTime) / 1000; lastCalledTime = currentTime; const fps = 1 / delta; if (counter >= 60) { const sum = fpsArray.reduce((a, b) => a + b); const average = sum / fpsArray.length; counter = 0; fpsArray = [fps]; console.log({ average }); } else if (fps !== Infinity) { fpsArray.push(fps); counter++; } } requestAnimationFrame(calcFps); }; requestAnimationFrame(calcFps); };
参考: Simple way to calculate the FPS when using requestAnimationFrame · GitHub
それほど多くもないですが、Infinityを弾いた分は誤差になるので根本解決はしたいなと思ってます...
それにしてもネイティブアプリはいいなぁ。読んで頂きありがとうございました!
Goの整数型
最近goを勉強し始めたので、備忘録としてgoの整数型についてまとめます。
符号あり整数
| 型 | サイズ(bit) | 最小値 | 最大値 |
|---|---|---|---|
| int8 | 8 | -128 | 127 |
| int16 | 16 | -32768 | 32767 |
| int32(rune) | 32 | -2147483648 | 2147483647 |
| int64 | 64 | -9223372036854775808 | 9223372036854775807 |
確認するにはmath.MinXXX, math.MaxXXXを使います。
package main import ( "fmt" "math" ) func main() { fmt.Printf("%d, %d", math.MinInt8, math.MaxInt8) fmt.Printf("%d, %d", math.MinInt16, math.MaxInt16) fmt.Printf("%d, %d", math.MinInt32, math.MaxInt32) fmt.Printf("%d, %d", math.MinInt64, math.MaxInt64) }
符号なし整数
| 型 | サイズ(bit) | 最小値 | 最大値 |
|---|---|---|---|
| uint8(byte) | 8 | 0 | 255 |
| uint16 | 16 | 16 | 65535 |
| uint32 | 32 | 32 | 4294967295 |
| uint64 | 64 | 64 | 18446744073709551615 |
| uintptr | * | * | * |
uintptrはポインターの値を指します。
確認するにはmath.MaxXXXを使います。最小値は常に0なのでメソッドは存在しません。
注意点としては、戻り値は明示的に指定しない限りintであるということです。また型推論では常にintになります。
従って、math.MaxUint32まではintで表せますが、math.MaxUint64はintでは表せないので、変数に代入する場合は型を明示的に指定する必要があります。
fmt.Printlnなどで出力する場合は、型キャストが必要です。
package main import ( "fmt" "math" "reflect" ) func main() { fmt.Println(reflect.TypeOf(math.MaxUint16)) // => int fmt.Println(math.MaxUint32) // => 4294967295 i := math.MaxUint64 // => constant 18446744073709551615 overflows int var j uint64 = math.MaxUint64 fmt.Println(i) // => 18446744073709551615 fmt.Println(math.MaxUint64) // => constant 18446744073709551615 overflows int fmt.Println(uint64(math.MaxUint64)) // => 18446744073709551615 }
特別な理由がない限り、整数を定義するときはintにすべきです。
型推論ではintが採用されるのは前述の通りですが、intは実装系に依存し、32bit系ではint32, 64bit系ではint64と同じです。
浮動小数点
| 型 | サイズ(bit) | 最小値 | 最大値 |
|---|---|---|---|
| float32 | 32 | ||
| float64 | 64 |
複素数
| 型 | サイズ(bit) | 最小値 | 最大値 |
|---|---|---|---|
| complex64 | float32の実部と虚部から成る | ||
| complex128 | float64の実部と虚部から成る |
complex()で複素数を生成できる。
Rubyの引数いろいろ
昨年のクリスマスにRuby3.0がリリースされました。8年ぶりのメジャーバージョンアップで、その目玉として静的型付けが導入されたことはご存知の方も多いのではないでしょうか。
それ以外にも様々な変更がありましたが、そのうちの一つでキーワード引数の扱いが変更になりました。引数は種類が多くてややこしいですよね。私もよくわからなくなるのでまとめてみようと思います。
引数の書き方
Rubyは関数呼び出しや、定義の時に引数のカッコを省略可能です。
def greet name puts "Hello, #{name}!" end greet "Taro" #=> "Hello, Taro!"
と書いても、
def greet(name) puts "Hello, #{name}!" end greet("Taro") #=> "Hello, Taro!"
と書いてもOKです。私はカッコをつける派ですが、付けないコードもよく見かけます。
デフォルト引数
引数に初期値を持たせることができます。これによりメソッド呼び出し時に、引数が必須ではなくなります。
def greet(name="Taro") puts "Hello, #{name}!" end greet #=> "Hello, Taro!" greet("Jiro") #=> "Hello, Jiro!" def greet(name) puts "Hello, #{name}!" end greet #=> ArgumentError
デフォルト引数と普通の引数の両方を渡したい場合は、引数の個数からRubyが推測をしてくれます。
def greet(name="Taro", age) puts "#{name} is #{age}." end greet(10) #=> "Taro is 10." greet("Jiro", 10) #=> "Jiro is 10."
可変長引数
引数の個数が決まっていない場合に使用します。名前の通りですね。
def greet(*name) name.each do |name| puts "Hello, #{name}!" end end greet("Taro", "Jiro") #=> "Hello, Taro" "Hello, Jiro"
引数の前に*を付けます。これで複数の引数を配列として受け取れます。配列なので、ループ等で処理してあげます。
可変長引数と、普通の引数の両方を渡したい場合は、デフォルト引数の場合と同様、引数の数からRubyが推測してくれます。
def greet(*name, msg) name.each do |name| puts "#{msg}, #{name}!" end end greet("Taro", "Jiro", "Hello") #=> "Hello, Taro!" "Hello, Jiro!"
オプション引数
可変長引数に類似の引数です。引数にキーとバリューの組み合わせを指定したい場合に使用します。引数は可変長です。
def greet(**args) args.each do |arg| puts "#{arg[0].capitalize} is #{arg[1]}." end end greet(name: "Taro", age: 10) #=> "Name is Taro." "Age is 10."
引数の前に**を付けます。これで複数のキーとバリューの組み合わせをハッシュとして受け取れます。指定するキーはなんでも良いので使い道は限定されると思います。あらかじめキーだけは指定しておくキーワード引数の方が使用頻度は高いと思います。
キーワード引数
引数にキーとバリューの組み合わせを指定します。引数が複数あると、何をどの順に渡しているのか分かりにくくなります。そういう場合はキーワード引数を使います。呼び出す時は、キーとバリューをセットで渡します。オプション引数との違いは、キーはあらかじめ指定しておく必要があるということです。
def greet(name:, age:) puts "#{name} is #{age}." end greet(name: "Taro", age: 10) #=> "Taro is 10."
どの引数がどういう意味を持つのかが一目瞭然だと思います。
キーワード引数はデフォルト引数を渡すこともできます。
def greet(name: "Taro", age: 10) puts "#{name} is #{age}." end greet #=> "Taro is 10." greet(name: "Jiro") #=> "Jiro is 10."
キーワード引数はRuby2.0から導入された機能で、それ以前はハッシュを使ってキーワード引数を擬似的に実現していました。
def greet(options = {}) name = options[:name] age = options[:age] puts "#{name} is #{age}." end greet(name: "Taro", age: 10) #=> "Taro is 10."
引数にキーとバリューの組み合わせを指定する場合、明示的にハッシュで書いても、{}を省略した形で書いても暗黙的に双方向に変換されます。
# キーワード引数の場合 def greet(name: "Taro", age: 10) puts "#{name} is #{age}." end profile = {name: "Jiro", age: 20} greet(profile) #=> "Jiro is 20." greet(name: "Jiro", age: 20) #=> "Jiro is 20." # 普通の引数でハッシュを受け取る場合 def greet(options = {}) name = options[:name] age = options[:age] puts "#{name} is #{age}." end greet(profile) #=> "Jiro is 20." greet(name: "Jiro", age: 20) #=> "Jiro is 20."
しかしRuby3.0からは、この仕様が変更になっています。キーワード引数の扱いが独立し、キーワード引数に{}をつけた場合に動かなくなりました。(普通の引数でハッシュを受け取る場合は互換性が維持されています。)従って明示的に、ハッシュを展開する必要があります。**をつければOKです。
def greet(name: "Taro", age: 10) puts "#{name} is #{age}." end profile = {name: "Jiro", age: 20} greet(profile) #=> Ruby3.0からエラー greet(**profile) #=> "Jiro is 20."
*と**
*(Splat Operator)は配列を展開する演算子、**(Double Splat Operator)はハッシュを展開する演算子です。
Javascriptで言うところの...(Spread Operator)のような感じだと思います。
array = [1, 2, 3] hash = {a: 1, b: 2} [a, b, array, c] #=> [a, b, [1, 2, 3], c] [a, b, *array, c] #=> [a, b, 1, 2, 3, c] {A: 10, B: 11, hash, C: 12} #=> SyntaxError {A: 10, B: 11, **hash, C: 12} #=> {A: 10, B: 11, a: 1, b: 2, C: 12}
*や**を使うと配列やハッシュを展開できるので、逆も可能です。これを利用したのが可変長引数、オプション引数です。
メソッド定義の際に*nameのように引数を指定すると、呼び出すときに引数が配列に変換されるという仕組みです。
ブロック引数
引数にブロックを指定できます。定義する際は&を引数の前に書きます。メソッド内ではyieldでブロックを呼び出します。
def greet(&block) yield end greet { puts "hello" } #=> "hello"
ブロック引数にプロックを渡したい場合は、呼び出しの際も&を書いてやればOKです。
def greet(&block) puts block.yield("hello") end proc_obj = proc { |str| str.upcase } greet(&proc_ojb)
定義時に引数にブロックを指定していないメソッドにもブロックは渡すことができます。ただ、注意点としては呼び出し方はyieldのみ限定されます。callで呼び出すことはできません。
def greet if block_given? yield else puts "No block" end end proc_obj = -> do File.write("hoge.txt", "Hello, world") File.read("hoge.txt") end greet(&proc_obj) #=> "Hello, world" greet #=> "No block"
if block_given?で引数にブロックが渡ってきているか条件分岐が可能です。
ブロックの呼び出し方いろいろ
block.call block.yeild yeild
procはオブジェクトの作り方も、呼び出し方も何パターンもあるのでまた別に書きたいと思います。
参考
https://techlife.cookpad.com/entry/2020/12/25/155741
https://qiita.com/jnchito/items/74e0930c54df90f9704c
https://qiita.com/rtoya/items/33617078501776fdcad7
https://ryotatake.hatenablog.com/entry/2018/11/20/splat_operator
CORSをちゃんと理解する
今回はCORSについてまとめたいと思います。私はよくサーバー側の設定を忘れて、ブラウザに怒られたりしてますが、今ひとつ理解できず、コピペして終わってました。Railsのapiモードでアプリを作る時とかも、よくわからずgem rack-corsとか書いてました。最近、FetchAPIについて学ぶ機会があって、ブラウザからのhttp通信についてちゃんと調べようと思ったのがきっかけでCORSについてもまとめてみました。
corsとは?
CORS(Cross-Origin Resource Sharing)は異なるオリジン間での通信を許可する仕組み。CSRFなどのセキュリティ面から、XMLHttpRequestやFetchAPIなどのhttp通信は同一オリジンポリシーに従います。従ってオリジンが異なる場合はそれを許可するための、適切なcorsに関するヘッダーが必要になってきます。
オリジンとは?
オリジン = ドメイン + プロトコル + ポート番号
https://domain-a.comみたいな部分がオリジンですね。つまり、https://domain-a.comからhttps://domain-b.comへの通信はデフォルトでは許可されず、corsによる設定が必要になるという事です。
ではcorsの設定についてみていきます。
プリフライトリクエスト
後述する特定の条件以外の場合は、プリフライトリクエストと呼ばれるリクエストを最初に行い、あらかじめサーバーがリクエストに応答可能か確認を行います。httpのOPTIONSリクエストによって、これを行います。
リクエスト
OPTIONS / HTTP/1.1 Origin: https://domain-a.com Access-Control-Request-Method: POST Access-Control-Request-Headers: X-Custom-Header, Content-Type
通信を行いたいメソッドや、ヘッダーをサーバーに対して提示します。Access-Control-Request-Headersに関しては後述の単純リクエストで許可されるヘッダー以外を指定します。
レスポンス
HTTP/1.1 204 No Content Access-Control-Allow-Origin: http://domain-a.com Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-Custom-Header, Content-Type Access-Control-Max-Age: 86400
レスポンスには許可するオリジン、メソッド、ヘッダー情報が含まれます。Access-Control-Allow-Originに関してはワイルドカードの使用が可能なので、全てを許可する場合は、
Access-Control-Allow-Origin: *
と書くことが可能です。
Access-Control-Max-Ageは、プリフライトリクエストの結果をキャッシュしておける上限時間です。
単純リクエスト
以下のいずれかの条件を満たす場合は、プリフライトリクエストを送らずに直接http通信を行うことが可能です。
逆に言えば、この条件を満たさない場合は、全てまず、プリフライトリクエストを行わなければなりません。
以下のメソッドのいずれかである。
GET, HEAD, POST以下のヘッダーのみを含む。
Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, WidthContent-Typeヘッダーは以下のみ。
application/x-www-form-urlencoded, multipart/form-data, text/plainリクエストに使用される
XMLHttpRequestUploadにイベントリスナーが登録されていない。リクエストに
ReadableStreamオブジェクトが使用されていない。
資格情報(認証)
認証を行うためにCookieをヘッダーに含めて送信したい場合は多くあると思います。デフォルトではヘッダーにCookieは含めないので、リクエスト時に設定が必要です。
XMLHttpRequestの場合
const xhr = new XMLHttpRequest(); xhr.withCredentials = true;
FetchAPIの場合
fetch('https://domain-a.com', { mode: 'cors', credentials: 'include', });
これにより、リクエストヘッダーにCookieヘッダーが含まれるようになります。
レスポンスに次のヘッダーが含まれば、許可されたことになります。
Access-Control-Allow-Credentials: true
これはプリフライトリクエストの場合でも、実際のリクエストの場合にも(もちろん単純リクエストの場合も)含まれます。プリフライトリクエストの場合は、実際のリクエストにCookieを含められるかどうか、実際のリクエストの場合は、それをブラウザで利用して良いかを定めています。つまり、実際のリクエストでこれがtrueでなかった場合は、ブラウザによって無視されるということになります。
注意点としては、資格情報を送る場合はAccess-Control-Allow-Originにワイルドカードを設定できません。特定のオリジン名を指定する必要があります。
参考
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
http://www.tohoho-web.com/ex/cors.html
https://qiita.com/att55/items/2154a8aad8bf1409db2b
Socket.io + Express を試してみる
WebSocketとは?
双方向通信を可能にするプロトコルの一つです。リアルタイムで通信を行いたいチャットなどに利用されます。
普通のhttp/https通信では、クライエントからリクエストが送られる度にコネクションが張られ、サーバーからレスポンスが返ると、コネクションは切断されます。 つまり、通信の度にコネクションを張り直す必要があり、クライエントからのリクエストなしにサーバーからレスポンスを送信することができません。
これを解決するために、従来はロングポーリング(comet)という技術が使われていました。
一定の間隔でブラウザからリクエストを送り、サーバーは新着情報がなければレスポンスを一定時間保留にします。その間に新着があればそれを返し、なければ何もなかったというレスポンスを返します。サーバー側の保留なしの単純なポーリングよりリアルタイム性が高いのが特徴です。これによって擬似的に、リアルタイム通信が可能になりますが、以下の欠点があります。
- ヘッダーが冗長でオーバーヘッドが大きい
- タイムアウトがある
- httpコネクションを占有してしまう
一方でWebsocketは、最小2byte, 最大でも14byteと軽量かつ、HTTPをアップグレーデして、TCP上で双方向通信を実現します。 クライエントからの最初のリクエストのヘッダーは次のようになります。
GET / HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: upgrade Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: xxxxxxxxxxx== Sec-WebSocket-Version: 13
サーバーからのレスポンスは次のようになります。
HTTP/1.1 101 OK Upgrade: websocket Connection: upgrade Sec-WebSocket-Accept: yyyyyyyyyy=
なんとなく直感で理解できるのではないでしょうか。
Sec-Websocket-KeyとSec-Websocket-Acceptはそれぞれ対応しており、クライエントが自分のリクエストに対するレスポンスだと認識できるようになっています。
ちなみに、101は、Upgradeリクエストヘッダーが送られてきた場合に返却するSwitchinig Protocols レスポンスです。
この時点で、Websocketコネクションが確立されました。
Socket.ioとは?
手軽にwebsocketを実現するjavascriptライブラリで、フロント側と、node.jsで実装されたサーバー側から作られています。
WebSocketが繋がらなくなった時に、ロングポーリングやポーリングなどの代替手段を提供してくれる他、到達確認や認可、namespaceなどを提供してくれます。
チャットを作ってみる
まずexpressとsocket.ioをインストールします。
yarn add express socket.io
次のようにapp.jsを作成します。
// app.js const express = require('express'); const app = express(); const http = require('http').createServer(app); const PORT = process.env.PORT || 3000; app.get('/', (req, res) => { res.send('<h1>Hello world</h1>'); }); http.listen(PORT, () => { console.log(`Server listening. Port: ${PORT}`); });
expressに関する記述のみですが、動作確認をしておきましょう。
node app.js
ブラウザで、localhost:3000にアクセスします。Hello worldと表示されるはずです。
次に表示部分をhtmlファイルに分けていきます。
// app.js app.use(express.static(`${__dirname}/public`)); app.get('/', (req, res) => { res.send(`${__dirname}/index.html`); });
まず、app.jsでファイルを読み込みます。静的ファイルを分けたいので、staticを使っています。
<!-- public/index.html --> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>Socket.io</title> <link rel="stylesheet" href="index.css"> </head> <body> <div class="chat-field"> <div class="chat-area"> <ul id="messages"></ul> </div> <form class="chat-form" action=""> <input id="m" /> <button class="disabled">Send</button> </form> </div> <script src="/socket.io/socket.io.js"></script> <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> <script type="text/javascript" src="index.js"></script> </body> </html>
/* public/index.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font: 13px Helvetica, Arial; } .chat-field { margin: 10em auto; width: 40%; height: 50em; border-radius: 5px; box-shadow: 5px 5px 25px 0 rgba(0, 0, 0, .5); text-align: center; } .chat-area { margin: 5em auto; width: 90%; height: 40em; overflow: scroll; } .chat-area li { margin: 0.5em; padding: 0.7em; background-color: rgb(223, 223, 223); border-radius: 8px; list-style-type: none; } .chat-form input { margin-right: 0.3em; padding: 0.6em; width: 70%; border: 2px solid blueviolet; border-radius: 5px; } .chat-form button { padding: 0.6em; border: 2px solid blueviolet; border-radius: 5px; background-color: blueviolet; color: white; }
次にindex.jsを書いていきます。アクセス時に、websocketを張り、送信ボタンを押すとそれを通じて通信を行い、レスポンスを表示する仕様です。
// public/index.js $(() => { $('input').change(function() { let text = $(this).val(); if (text === null) { $('button').prop('disabled', true); } else { $('button').prop('disabled', false); }; }); const socket = io(); $('form').submit(e => { e.preventDefault(); socket.emit('post', $('#m').val()); $('#m').val(''); $('button').prop('disabled', true); return false; }); socket.on('msg', res => { $('#messages').append($('<li>').text(res)); }); });
socket.ioのインスタンスに対して、emitで送信、onで受信を行っています。第一引数は、送信側と受信側で同じキーを指定します。
ここまでで一度動作確認をしておきましょう。次のような画面になっていれば成功です。

続いてサーバー側にsocket.io部分を書いていきます。
// app.js const io = require('socket.io')(http); io.on('connection', socket => { console.log('user connected'); socket.on('post', post => { io.emit('msg', post); }); });
onで受信し、postをキーに持つメッセージを受け取ります。そして、受け取ったものをそのまま返却していますが、これはconnectionが張られている全てのユーザーに送信されます。動作確認してみてください。
これで簡単なチャットは作成できましたが、自分と相手のメッセージくらい区別できるようにしておきましょう。 具体的には、接続時にユーザー一意のトークンを発行し、ユーザーがリクエストの際にそのトークンを含めることで実装します。
トークン生成にはcryptoを使用し、socket.idでユーザーごとに割り振られたidを取得できるのでこれを元に作成します。
// app.js const crypto = require('crypto'); io.on('connection', socket => { console.log('user connected'); (() => { const token = makeToken(socket.id); io.to(socket.id).emit('token', { token: token }); })(); socket.on('post', post => { io.emit('msg', post); }); }); const makeToken = id => { return crypto.createHash('sha1').update(id).digest('hex'); };
全てのユーザーに送信者のトークンを返却してるのが相当イケてないですが今回はよしとしてください(笑)
最後にクライエント側で識別を行って、ラインのように左右に出し分けたいと思います。
// index.js const IAM = { token: null }; socket.on('token', data => { IAM.token = data.token; }); $('form').submit(e => { e.preventDefault(); socket.emit('post', { msg: $('#m').val(), token: IAM.token, }); $('#m').val(''); $('button').prop('disabled', true); return false; }); socket.on('msg', res => { if (res.token === IAM.token) { $('#messages').append($('<li class="mine">').text(res.msg)); } else { $('#messages').append($('<li class="others">').text(res.msg)); } });
まず、tokenを受け取り、IAMにセットしておきます。(tokenはconnectionが張られた際に、サーバーから送信されるものです。)
メッセージの送信時に、トークンを含めるように変更し、受け取り時はIAMと比較して自分が送信したものかどうか判定します。class属性を変えることで表示を分けます。
最後にindex.cssを変更します。
/* index.css */ .chat-area li { padding: 0.7em; background-color: rgb(223, 223, 223); border-radius: 8px; list-style-type: none; } .mine { margin: 0.5em; width: 50%; } .others { margin: 0.5em 0.5em 0.5em auto; width: 50%; }
これで完成です。ブラウザで複数タブを開いて、チャットをやりとりしてみてください。
最終コードはgithubにあげてます。

参考にさせて頂きましたm
WebSocketについて調べてみた。 - Qiita
socket.io が提供してくれているものは何か - from scratch
はじめてのSocket.io #2 チャット編「自分がemitした通信内容か判定する」