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は用意されていません。
そこでrequestAnimationFrame
apiを使用して画面の描画回数を測定することで、間接的に端末負荷の指標とすることにしました。
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, Width
Content-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した通信内容か判定する」