やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【書評】Elixirの歴史を辿るうちに「電話はなぜつながるのか」をふと読んでいた

なぜ読んだのか

個人的な興味があって、以前よりElixirというプログラミング言語を学んでいる。このElixirの動作環境がErlangVMというものであり、Erlangというプログラミング言語を実行するためのものだ。このElixirErlangは高い堅牢性(robustness)を備えており、古くから金融システムや電話交換機に用いられているという。また、このErlangという言語を作ったのはJoeを中心とした、エリクソン社という通信機器を扱うスウェーデンの企業であり、実装にはErlangが用いられていたようだ

つまり、Elixirの歴史を追う中でErlangに出会い、Erlangの歴史を追う中で、用途を調べた所、「電話」というワードに辿りついたわけだ。どのような実装がされているのかを理解するためには、電話にはどのような仕様が必要なのかを知りたいと思った

また、電話の仕組みは現代のインターネットのベースになっているのだという。インターネットの仕組みという現代においては抽象化された事象への理解を深めるためにもこの書籍を読んで見て背景を知ってみるのはアリだと思った

あとDr.Stoneで千空が携帯電話作ってたので、そんなこと出来るん?と気になり、気付いた時にはポチっていた

加入電話の仕組みってすげー

電話の仕組みは至ってシンプルだった。詳細はぜひこの書籍を読んでほしい。本当に面白い。電話の仕組みについて全く知らない自分でも、核になる部分の情報はインプット出来たと思う

ベースにあるのは、声、つまりは空気振動という現象を電波に変換して、電話線という導線を通じて、対象の相手側に伝えるという物理現象だ。また、声を電波に変換するということは声をバイト情報(0と1の組み合わせ)にして電波を利用して導線を通じて、相手側に送り出すという事と等しい。電波を利用して送り出したバイト情報を受け手側で復元することで音声を再生する。電話がこのような単純な仕組みで動いていることに驚きを隠せない。しかも、何十年も前にだ...

さらに関心させられたのは、どのようにして相手側を探索するのかという仕組みと、どのようにして接続を保証しているのかというところだ。相手を探すために、電話交換機という全国に設置されている接続を繋げるための媒体を巡る旅がスタートする。発信者のリクエストを元に電話交換機Aから電話交換機Bに。電話交換機Bから電話交換機Cにと、電話交換機自身に保持されている接続者情報を探し、対象の接続先を探していく。この経路を記録して、発信元に知らせる。このような作業は特定のバイトパターンをコマンドのように定めて行われている。例えば、「00001111」のバイト情報であれば「交換機Bで対象の電話番号を持つ接続者を発見しましたよ」というFINDというコマンドとして扱っているわけだ(これは例なので実際には存在しない)。これは現代のIP(インターネットプロトコル)と非常に似ているし、実際にベースになっているのはこの部分からだそうだ

そして、この探索した経路を元に通話回線(音声をやり取りするための回線)を繋げることで初めて、通話ができる状態になる。つまり、音声を届けるための回線と情報をやり取りするための回線は分かれており、通話をするために2本の回線が用意されている。

話すとまだまだ面白い話があるが、このぐらいにしておく。どのようにして、届けられる音声の順序を保っているかという話も非常に面白い

IP電話の時代になって

電話をするために従来の電話線を用いる方法からIP、すなわちインターネット回線を用いた方式に変化していく。しかしながら、ベースの仕組みは全く変わっていない。新たに通話のためのプロトコルをインターネット回線上で定めて、相手側を探すために互いにプロトコルをベースにデータ送り合うだけで、加入電話の時と行なっていたことはほとんど変化していない。IPアドレスをいかにして非公開にするかなど、加入電話の時よりもセキュリティに対する意識と注意が高まっており、どのように秘匿性を保持しているのかが面白い。今まで、曖昧な理解のままだったが、なぜ GatewayNATというものが必要なのかがよく分かる

全く同じ様な仕組みでIP電話が実装されていることに加入電話というものが、いかに洗礼されて設計されたいたかということに恐怖さえ感じる。仕事柄、第2部の話はすんなり頭に入ってくるので、第2部から読み進めて、第1部に戻るという読み方もありかもしれない

業務に直結しないものを読む価値はあるのか

ある。オススメしたい。直接的には役に立たないかもしれないが、インターネットの背景を知ることが出来るため、非常に勉強になる。内容も優しく読みやすかった。この本を読んでみて、「あれ、電話ってもしかして作れるんじゃね」と思った。まずは、振動を電波に変える装置から取り掛かろう。待ってろ、千空..

こんなすげー仕組みに使われていて、安全に動作しているErlang半端ないって

参考文献

websocketを使ったgolang産チャットアプリのやらかしスレッド設計をkubernetesでスケール可能に修正した

機能に関する詳細

現在、業務でwebsocketを使用したチャットアプリケーションの開発に関わっている。自分が担当しているのはサーバーサイドのみで、実装はgolangを使用している。websocketのライブラリはgorilla/websocketを採用している

github.com

サーバーサイドの websocketのメッセージのやりとりの部分はgorilla/websocketのexamplesにあったものをパクリスペクトしている。そりゃ、ライブラリ作った人たちが作ったサンプルに間違いはないでしょという風に考えた

github.com

このサンプルに実装されているのはだいたい以下の通り

  • client.go -> clientに関する情報(struct)を生成。メッセージの受信送信のためのスレッドの実装
  • hub.go -> チャネル経由で受け取ったメッセージを配信するための処理の実装
  • main.go -> サーバーを起動してwebsocketの接続を受け付ける(switching protocol)

これだけでも十分な実装だが、自分が担当している業務では追加の機能要件がある。1つはredispubsub機能を使用した購読済みの別サーバーへのメッセージのpublish機能の実装で、これは予想の通り、kubernetesを使用してサーバーがスケールする予定であるため必要になる
とは言いつつも、参加当時はkubernetesに関する知識は皆無だったので、どのようにスケールするかなんて考えていなかった。というか、考えられなかったので、後のやらかしポイントになる

合わせて、メッセージの送受には並行処理が欠かせない。1つのプロセス、もしくはスレッドで処理を占有してしまうと、チャットとは呼べない代物が出来上がる。メッセージがサーバーに受信して送信されるまで、誰もメッセージを送ることが出来ないようなものになる。接続数が少ないうちは何とかなるかもしれないが、接続数に比例してストレスを感じるようになるだろう

やらかしたこと

f:id:takamizawa46:20191216225158p:plain
開発初期当時は環境の準備や実装の遅れなどの問題で、シングルサーバーでの動作確認しか行なっておらず、上手く動いているものだと錯覚していた。実際に、複数サーバーにしてみて動作を検証したはずなのだが、見落としていた様だ

では実際に何をやらかしていたかという話に入る。サーバーでのwebsocketのメッセージの処理とgolangの並行処理をするためにgoroutinesでどのようにスレッドを用意しているかを簡単に説明していく。ちなみにチャンネルはpubsubのチャンネルを指しており、チャネルはgolangのスレッド間通信で扱うデータ構造を指しており、タイポではない

setup

  • サーバーを起動する。この時に合わせて、Hub(examplesのhub.go)をgoroutinesを使用して立ち上げて待機させる
  • redispubsub機能を利用して対象のチャンネルを購読(subscribe)して、publishされたメッセージを受信するためのスレッドを待機させる
  • ユーザーがブラウザ経由でwebsocketの新規セッションのrequestを送信する
  • サーバー側で接続を許可して、このユーザー専用のメッセージ読み込みスレッドとメッセージ書き込みスレッドを立ち上げる
  • 新規でユーザーが接続される毎に同様に専用のスレッドを立ち上げる(認証で新規ユーザーなのかを判定している)

この時点でユーザーが2人であるとfixすると存在しているスレッドの内訳は以下の様に7つになる

  • メインスレッド(httpのrequestを受け付ける)
  • Hub(メイン処理を実行)スレッド
  • pubsubの受信用スレッド
  • ユーザーA専用の読み取りスレッド
  • ユーザーB専用の読み取りスレッド
  • ユーザーA専用の書き込みスレッド
  • ユーザーB専用の書き込みスレッド

メッセージの受信と送信

f:id:takamizawa46:20191216225218p:plain

  • webscoketのセッションを張っているユーザーから新規のメッセージが送信される
  • ユーザー専用の読み取りスレッドでメッセージを読み込んでredispubsub機能で購読済みのチャンネルにメッセージをpublish
  • publishされたメッセージを受信用のスレッドで受け取り、チャンネルを購読しているサーバーにメッセージを配信してチャネルを通じてHubにメッセージを渡す
  • Hubで受け取ったメッセージを元に、処理を行い対象のユーザーに配信するために各ユーザー専用の書き込みスレッドにメッセージをチャネル経由で送る
  • 専用の書き込みスレッドで受け取ったメッセージをwebsocket経由でユーザーに送信する

これが概要。あっ....これをスケールすると...

コードはかなり、省略してアレンジしたものを載せておく。動作は保証出来ないがイメージが伝わればと思う
hub.go(読み取りスレッド部分)

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    :
    :
        message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
        // publishは自前実装の関数。メッセージをpubsubを利用して送信する
        publish(message)
    }
}

pubsub.go(pubsubのメッセージを受信してHubに渡すスレッド) hub自体は起動時に引数で受け取る

func SubscribeChannel(channelName string, h *Hub) {
    :
    :
    // メッセージの受信ループを作成
    for {
        switch val := psc.Receive().(type) {
        case redis.Message:
            var message *Message
            h.broadcast <- message
        }
    }
}

hub.go(Hub)

type Hub struct {
         // userを識別するためにstringをkeyに持つmapに変更
    clients map[string]*Client
    broadcast chan []byte
    register chan *Client
    unregister chan *Client
}

func (h *Hub) Run(m *database.Mongo) {
    for {
        select {
        // websocket経由で受信したメッセージを読み取るスレッドから受信
        case binaryMessage := <-h.Broadcast:
            switch message.Type {
            case "MESSAGE":
                h.Client[message.UserID].Send <- binaryMessage
            }
        }
    }
}

スケールした結果

何と同じメッセージがスケールしたサーバーの数(Pods)だけ、送信されているではないか。そりゃそうだ。読み取ったメッセージをpublishして、受け取り配信するためのスレッドと各サーバー毎に立ち上がっているのだから。ついでに処理もサーバー台数分だけ走るので、databaseの値もめちゃくちゃになっている。開発初期当時は、kubernetesのスケールへの理解が不足しており、redispubsubpublishを担当するスレッドも同様にスケールされるという意識が無かった

単純にpublishをどのタイミングですれば良いのかという判断が甘かった

改善した結果

f:id:takamizawa46:20191216225241p:plain
構成に手を加えて、スケールしても問題なく、処理が1度のみ走り、メッセージが複数回、送信されないようにした。setupの部分の更新はなく、全体の流れは以下の様になった。大きな変更としてはスレッド間で扱うチャネルを1つ追加した。元のexamplesに実装されているのはbroadCastというチャネルでこれは、読み取り専用プロセスからHubに渡すために使用するチャネルとして扱い、新規で追加したチャネルはpubsub経由で受信したメッセージを受信するためのチャネルとして用意をした

元のHubのstruct

type Hub struct {
    // Registered clients.
    clients map[*Client]bool

    // Inbound messages from the clients.
    broadcast chan []byte

    // Register requests from the clients.
    register chan *Client

    // Unregister requests from clients.
    unregister chan *Client
}

チャネルを追加

type Pubsub struct {
        // 送信先
        Target string
        Message []byte
}

type Hub struct {
         // userを識別するためにstringをkeyに持つmapに変更
    clients map[string]*Client
    broadcast chan []byte
    register chan *Client
    unregister chan *Client
    pubsub chan *Pubsub
}

メッセージの受信と送信

  • webscoketのセッションを張っているユーザーから新規のメッセージが送信される
  • ユーザー専用の読み取りスレッドでメッセージを読み込んでHubにチャネル経由でメッセージを渡す
  • メッセージを元に処理を行い、redispubsub機能で購読済みのチャンネルにメッセージをpublish
  • publishされたメッセージを受信用のスレッドで受け取り、チャンネルを購読しているサーバーにメッセージを配信して別のチャネルを通じてHubにメッセージを渡す
  • Hubで受け取ったメッセージから送信先のユーザーのwebsocketのセッションが自身のサーバーに存在するかを確認。存在している場合に、ユーザー専用の書き込みスレッドにチャネル経由でメッセージを渡す
  • 専用の書き込みスレッドで受け取ったメッセージをwebsocket経由でユーザーに送信する

このように変更することで無事に、スケールした状態でもメッセージが1通のみ送信されるようなり、冷や汗が引いた。並行処理プログラミングの経験も、スケールの経験も全く無かったので大変、勉強になったし、どのように考えれば良いのかが理解出来た

hub.go(読み取りスレッド部分)

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    :
    :
        message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
        // exampleの通りチャネルを経由してHubにメッセージを渡す
        c.hub.broadcast <- message
    }
}

hub.go(Hub)

func (h *Hub) Run(m *database.Mongo) {
    for {
        select {
        :
        :
        // websocket経由で受信したメッセージを読み取るスレッドから受信
        case binaryMessage := <-h.Broadcast:
            switch message.Type {
            case "MESSAGE":
                h.Client[message.UserID].Send <- binaryMessage
                // 配信に必要な情報をpubsub経由で渡す
                publish(&Pubsub{Target: "対象のユーザーを認識する値(eg: unique id)", Message: message})
            }
        }
    }
}

pubsub.go(pubsubのメッセージを受信してHubに渡すスレッド)

func SubscribeChannel(channelName string, h *Hub) {
    :
    :
    // メッセージの受信ループを作成
    for {
        switch val := psc.Receive().(type) {
        case redis.Message:
            var message *Message
            // 全体配信用のチャネルにメッセージを渡す
            h.pubsub <- message
        }
    }
}

hub.go(Hub)のpubsub用のメッセージの受信

func (h *Hub) Run(m *database.Mongo) {
    for {
        select {
        case publish := <-h.pubsub:
            if val, ok := h.Clients[publish.Target]; ok {
                val.Send <- publish.Message
            }
         :
         :
    }
}

こんな感じで落ち着いた。並行処理の設計は奥が深い...
draw.ioというサービスを構成図に書くのに初めて使ってみたが、かなり良いのでオススメ

おまけ Elixir/Erlangの勉強が意外なところで役に立った

並行処理と聞いて、反応せずにはいられない。ElixirErlangの得意とするところだ。実際に実装に使用しているのはgolangだが、このチャットアプリを実装するにあたって、「プログラミングElixir」や「プログラミング Erlang」で得たプロセスの設計方法やアクターモデルの考え方はgoroutinesを扱う上で非常に参考になった

スレッドの設計をし直す時にも「プログラミング Erlang」でJoeがどのように複数プロセスでの処理を考えるのか(最初にメッセージシーケンス図を書くらしい)を参考に、シーケンス図をそこそこ書いた
まさか、こんな形で役に立つとは思わなかったがElixirErlang万歳

参考文献

【現役エンジニアが考察】はじめてのプログラミング言語習得のコツ。挫折しないためにはどう学ぶべきか

この記事を書く理由

個人的にElixirというプログラミング言語の勉強会を主催したり、セミナーに登壇する中で多くのプログラミングの初学者や、現在、学習をしているが、何をしていいのか分からないなど多くの相談を受けた。毎度、回答する中で「参考になりました」と良い反応をもらうことが多くなってきたので、せっかくなので記事にして多くの方に情報を共有できればと思う

  • 挫折を防ぐため
  • 効率的にプログラミング学習を進めるため
  • 楽しくプログラミングの学習を続けるため etc...

にぜひ参考にして頂ければと思います

想定している読者

この中で1つでも自身に当てはまるものがあれば、この記事を読んでいただく価値はあるかと思います

  • これから新たにプログラミングを始めたい
  • プログラミングの学習は何からやっていいのか分からない
  • プログラミングの学習をしているがどこまで学習すればいいのか分からない
  • どのプログラミング言語を選べばいいのか分からない
  • 過去にプログラミングに挑戦したが挫折したことがある

著者について

twitter.com

OKB(岡部)と言います。今年度から新卒として名古屋のITベンチャーでエンジニアをしています。学部生の頃は土木と建築を学んでおり、挫折の結果、独学でプログラミング学習をして今に至ります。情報系の学部の出身ではないため、いわゆる未経験という状態からのスタートをしました。プログラミング自体は始めて2年未満になります
業務ではWebアプリケーション開発のバックエンド、機械学習案件の取り回し(まれに実装)に携わっています。経験言語は以下の通りです

ほかに気休め程度に触っているものもありますが、今のベースになっているのはこのあたりです。とは言いつつも、私自身、過去に一度、プログラミング学習に挫折をしたことがあります。「猫でもわかるC言語」という著書があるのですが、全く分かりませんでした。なので挫折した面と、それでも業務にありつけたという面の両面を知っているからからこそ見えてくる視点があると思っています

これから記述することは格闘技と同じように流派があり、必ずしも正解ではありません。私の考えであって世間の考えではありません。しかしながら、先ほど書いた通り、「挫折」と「業務にありつけた」という両面を知っているからこそ自信を持って、この情報を共有します

それでは順に説明をしていきます

プログラムを書くという意味を見失わないで

f:id:takamizawa46:20191208014733p:plain
あなたはなぜプログラミングを学習しようと思うのか、またしているのか。今、世間ではエンジニアになるというのが一種のブームになっている。自由な時間に勤務でき、高額な報酬が手に入る。そんなイメージだけが一人歩きしてしまっており、プログラミングを始める人の頭にあるのは上記のような理想ばかりだ
それを悪いことだとは思わないし、私自身も同じような理由でプログラミングを始めた。誰だって楽して儲けたいものだ。Pythonってのを覚えたら年収が増えるんか!!!」という学生の浅知恵で始めただけのことである。しかしながら、その思いだけでこの長く辛い、独学の道を走り続けるのは難しい

「なぜ挫折するのか?」

答えはシンプルで、お金儲けが目的になって、プログラムを書くという意味を無視して通り過ぎているからだ
プログラミングの根本にある考え方は「自動化・効率化・高速化」のような人間の惰性の解決である。同じような計算処理を何度も繰り返すのが面倒臭い、毎日、水を汲んで温めるのが面倒臭い...など

プログラムを書いてお金を貰うということは自身の、もしくは誰かの面倒ごとをプログラムを書くことで効率化した、自動化したという結果ということになる。プログラマーになりたいだけという考えは止めよう。きっと続かない


まとめ

【プログラムを書くことを楽しもう】
面倒ごとをどのようにプログラミングという方法で自動化、高速化、効率化するという所にプログラミングの面白さがある

ほとんどのプログラミング言語の習得の進め方は同じ

楽しくプログラミングをするためには「プログラミング言語の習得」は欠かせない。しかし、このステップで挫折をしてしまう人が多く非常にもったいない。向き不向きはあるが、興味を持っただけに悲しい結果となってしまった

人間誰しも1つは言語を習得して日々、話しては聞き、使いこなしているのになぜプログラミング言語の習得に挫折するのか
それは「自動化、高速化、効率化」するために、なぜプログラミングが必要なのかを理解していないからだと言える

実際にプログラムを書いて、入荷した100個の果物(りんごとオレンジ)を仕分けるプログラムを書くことになったとする。さて何から始めるか。ざっくり作業の流れを書き出してみると以下のようになるだろう

  • 箱から果物を1つ取り出す
  • 取り出した果物がりんごなのかオレンジなのかを判定する
  • りんごであればりんご用の箱に。オレンジであればオレンジ用の箱に
  • まだ箱に果物が残っていればこの作業を繰り返す。もう無ければ終了する

別になんら難しくない作業の流れだと思う。このようにプログラムを書く際には処理の流れを書き出すことが重要であるが、今重要なのはそこではない。この作業の流れに登場する処理は3つだけであり、基本的にはどの言語でも3つの処理が用意、もしくは記述可能であるということだ

ではその3つの処理とは何なのか。凄くシンプルだ  

  • 変数の宣言(値を用意する)
  • 繰り返し(同じ処理を何度も行う)
  • 条件分岐(条件によって行う処理を変える)

たったこれだけだ。作業の効率を無視すれば、どのような処理も上記3つの処理を組み合わせれば記述することが可能になる。まずは各言語に用意された上記3つの構文を覚えることがスタートだ。逆に言えば、3つの処理の書き方を知っていればほとんどの処理は記述することが可能になっているはずだ
それにプログラミングという作業をめちゃくちゃ抽象化してしまえば、データをAという状態からBという状態に変化させているだけだ。先ほどの3つの処理を用いて説明をすれば、変数(用意した値)を繰り返し処理や条件分岐を使って、別の状態にするという訳になる

しかしながら、手が動かないという場合はプログラミング言語の知識の他にOSネットワークの知識が不足している可能性がある。安心しよう。プログラミング言語を習得出来ていないからではない

配列オブジェクトマップなどのデータ構造やその他の構文は必要になった時、処理が面倒だと思った時に「こんなものが用意されているんだ」という流れで覚えた方が使用すべきケースも分かるし、理解も深まる

例えば「配列を覚えるタイミング」は変数の宣言を10個、100個...もしなくてはいけないようになった時。どう考えても面倒だし、効率化するためにプログラムを書いてるのに全く効率化されていない。そこで配列というものが登場する。配列を用意すれば1つの変数(配列)に100個の値を格納することが出来る

このように「あれ、効率化されてない?」と思ったタイミングが新たな学びのタイミングになる

まとめ

プログラミング言語で最も基本的な構文を覚える】
- 変数の宣言
- 繰り返し
- 条件分岐

どのプログラミング言語を学ぶべきか

最初のプログラミング言語を何にするかは、常に現役のエンジニア間でも議論が行われている

  • コンピューターサイエンスを学ぶために
  • 楽しくプログラムを書くため
  • お金儲けが出来る言語を選ぶべき

色々な考え方があるため、確定的な答えを出すことが難しいが、私はプログラムの根本にある「自動化、高速化、効率化」が達成できるものなら何でもいいと思っている。その一方で最初の言語は、やはり楽しくシンプルに学ぶことができるものを選んだ方が良い
初学者の状態で「メモリ管理」だの「変数のスコープ」だの、「静的型付け」だということを考えて覚える時間を費やすよりも、シンプルな構文と処理を覚えて実際にプログラムを書く時間を用意した方が良い

オススメはPythonRubyのどちらかだ
Pythonは構文がシンプルで覚えやすい。また、コミュニティも活発で分からないことがあっても調べたら何となる可能性も高い。また、Webに機械学習にと採用される範囲が広く書けて損はないだろう
Rubyも同様に構文がシンプルで覚えやすい。Rubyの一番の強みはやはり、日本産(まつもとゆきひろ先生)であり、豊富な日本語情報が用意されているという点にある。それにRuby on railsという強力なWebフレームワークがあり、Web業界の仕事に繋がりやすい

この2つの言語、もしくは他の言語に触ったのちに「プログラミングってどうやって動いているんだろう」とか「なぜ、配列というデータが扱えるのだろう」とか「なぜPythonは処理が遅いと言われているんだろう」という話に興味を持ち始めた時、それからC言語などを始めてみれば良い。ありがたさが分かるのはそれからで良い。まずは書いて楽しむことが重要だ


まとめ

【最初の言語のオススメ】
PythonRubyが良い。まずはシンプルな構文を覚えて実際にプログラムを書いてみよう。難しい話に触れるのはその後で十分だろう

プログラミング言語の学習は書籍でサクッとやればいい。最初の一冊は1週間あれば、全てに目が通せるようなもの(P200ぐらい)の物がオススメ。いきなり、難しい書籍を対象に選んで、途中で挫折するのが典型的な残念パターンだ

Rubyに関しては良書を知らないので、省略するが、Pythonの学習にはサクッと読める「スラスラ読めるPythonふりがなプログラミング」がオススメだ。初学者の高校時代の部活の後輩に読んでもらったが、非常に分かり易かったと高評価を頂けた

どんなレベルの情報でも良いのでアウトプットしよう

これからエンジニアとしてやっていこうと考えているのならば、アウトプットをする癖をつけておくと良い。なぜアウトプットをする必要があるのかをRubyの作者のまつもとゆきひろ先生がこう答えている

インプットは必要、でも差別化要因にならない
しかし、アウトプットすることで差別化になる

アウトプット、ブログの記事にしたりという作業は面倒臭いし、100%お金が貰えるというわけではない。ではなぜ、アウトプットをするのか。エンジニア視点では2つ理由があると考えている
1つはアウトプットすることで自分の学習になるからで、学習した内容をブログ記事にまとめようとすると半端な知識では不可能だ。順序よく理解している必要がある。それに言語化してまとめることで自分の再学習にもなる。そうすると簡単には忘れなくなる
もう1つは世間に自分の存在を知ってもらうことが出来るからだ。アウトプットしないエンジニアは世間からすれば、無名の選手と変わらない。どんなレベルの情報であれ、アウトプットをしているということはGoogle検索に自分の情報が現れるということになる。また、アウトプットした情報はそのままポートフォリになる上に、どういった事柄に興味があるかという証明書にもなる

これだけのメリットがありながらアウトプットを実行しているエンジニアは少ないのだ
私がアウトプットをこのブログでし続けて得られたものは非常に多い。ぜひ、興味があればこちらの記事で詳しい話をしているので、一度目を通して見て頂ければと思う

www.okb-shelf.work

反応を貰える状態を作る

結局、ブログにしろyoutubeにしろ続かない理由は凄くシンプルで反応がもらえないから、もしくは報酬が発生しないからのどちらかになる。例を挙げてみると、「誰も食べもしない料理を作り続ける(反応がもらえない)」、「無賃できつい仕事を続ける(報酬が発生しない)」というイメージだろうか

この状態を抜け出すために何かしらの工夫をすると良い。プログラミングのコミュニティに所属してみるのも良いし、ハッカソンに出場してみるのも良い。プログラミングのアルバイトを初めてみるのも良い戦略だ

独学する上で大切なのは「続けられる仕組みを作ること」だと考えている。何を選ぶかはあなた次第だが、決まった期間にやり切りたいというのであれば、プログラミングスクールに通ってみるというのも手だ。しかしながら、決して安い額ではないのでスクールの評判や得られるものは慎重に選んだ方が良い

最終的に伝えたいこと

長々と自分の考えを記述してきた。ここまで読んで頂いた方には感謝しかありません。現在、エンジニアになるということが一種のブームのようになり、悪質な情報や、デマカセが多く、多くの初学者を混乱させる原因となっている。しかしながら、プログラミングの根底にあるのは今回の記事で紹介したような内容であり、楽しいものだと伝われば嬉しい

twitterのDMなどでも相談を受けているので気軽に相談してもらっても問題ない
学習の際の参考に少しでもなればと思います

参考文献

【プログラミングErlang7章より】並行処理とアクターモデルの概念について

並行処理について

人間なら誰でも並行処理を理解している。
この世界は並列だ。
Erlangのプログラミングは人間の思考と対話をモデル化している。
人間はそれぞれメッセージを送ることによって通信し合う独立した存在として動く。
誰かが死ねば、他の誰かが気づいてくれる。

Joe Armstrong

プログラミングErlangより引用

1つのコンピューターで出来ることには限りがある。より高度な処理をしたいと思った時には並行処理や並列処理を行う必要がある。 並行? 並行?なにそれ、めちゃくちゃ難しそう…と感じてしまうが、プログラミングErlangを読む限り、そう焦る必要はないらしい
実際に我々、人間が日々、暮らしているこの世界でも並行処理が行われている

たとえば、誰かと話をしている時。自分が友人に昨日の愉快な出来事を語る。その話を友人は相槌を打ちながら聞いてくれる。この何気ない日常のやりとりでさえも並行に処理が行われていることに気づく

自分が話をするという動作をするのと同時に相手は話を聞いて相槌を打つという動作を行なっている。これを創造主の視点から見てみると、あたかも二人が同時に、すなわち並行に処理をしているように見える。というか、している。つまりは、我々人間が1つのプロセスという単位で動いていると見なすことが出来る

これが人間の日常生活は並行処理に溢れているという一例だ

並行処理と並列処理の違い

以下のブログにて詳細に解説がされていた。せっかくなので、自身の言葉で並行と並列について違いをまとめておこう
yohei-a.hatenablog.jp

違いを説明するのに5人の人間に命令を出すというケースを例にしてみる。5人の人間に高速で順次、あたかも同時に見えるような速度で命令を順次出していくような処理を並行処理。5人の人間に本当に同時に命令を出すような処理を並列処理と考える

さらに砕いた例で説明をすると、1つの脳みそで順次、5人の話を同時に聞く聖徳太子と5つの脳みそで同時に5人の話を聞く宇宙人をイメージしてもらえれば分かりやすいのではないだろうか

f:id:takamizawa46:20191207171942p:plain

Erlangにおける並行処理の方法

のちに解説をするがErlangでは並行処理のためにプロセスが採用されており、互いの内部状態はメッセージを送り合うことで更新する。このようなモデルをアクターモデルと呼ぶ

また、プロセスとは別に共有メモリを使用するスレッドという処理単位を用いて並行処理を行うものもある。知っているもので例をあげればgolangのgoroutineはスレッドを用いて処理を行う

ただ、どのようにしてアクターモデルという思想が精査され出来上がったのかが気になったので、調べてみた

アクターモデル

すでに既出でアクターモデルについて解説している優良な記事が多いので詳しい話はそちらに任せて、ざっくりと概要と説明が出来るレベルに内容をまとめておこう

アクターモデルにおいて、全てのものはアクターである。これはオブジェクト思考における、全てのものはオブジェクトであるという考え方と非常に似ている。そのため、アクターモデル並行指向オブジェクトプログラミングとも言われるようだ

アクター同士はメッセージを送り合う事が可能であり、逆に言えばやり取りの方法はメッセージを送り合う事のみに限定されている。なぜ互いのアクター同士でメッセージを送る事が可能なのかというと、それぞれのアクターはメールボックスアドレス(言うなればメールアドレス)を持っており、自分宛に受け取ったメッセージをメールボックスにスタックする
このメッセージを順次処理していくことになる。受け取ったメッセージをトリガーにして、様々な処理を行えばアクターモデルプログラミングが可能になる

またアクター自身は別のアクターを生成することが出来る
理論や歴史はwikipediaなどに情報が転がっているのでそちらを参照してほしい

ja.wikipedia.org

感想

このような概念が1970年代に、近未来に起こりうるであろう、分散コンピューティング、並行処理を予測して形作られてきていたということに驚きを隠せない。そして、一度は1990年代に衰退したアクターモデルが現代になって複雑になりすぎたデータを処理するため、再び注目されていると...。熱い...

それにしても、このプログラミングErlangが面白い。著者がErlangの作者のJoe Armstrongであるため、並行処理に関するテクニックや考え方が散りばめられていて、Erlangに興味がない人が読んでも勉強になる

また読み進めたい

引用

【第15回清流elixir+fukuoka.exもくもく勉強会】Elixirでチャットサーバーを作りながらNode間通信を試す

トピック

elixir-sr.connpass.com

Qiitaのアドベントカレンダーの執筆したり、業務が忙しすぎたり、PIDの闇にハマっていたりで、少し間が空いてしまいましたが第15回のレポートをまとめました
今回も前回同様に、kogaさんにお声がけ頂きましてfukuoka.exさんと合同でもくもく勉強会を開催しました
fukuokaex.connpass.com

僕はQiitaのアドベントカレンダーElixirNode間チャットアプリを作るってのを書こうと思って、前からやりたかったNode間通信に挑戦したので当日の内容と後日の作業をプラスでまとめたものを共有します

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 12 -> 4 コミュニティ参加人数 : 36 -> 37 update!
2019/12/07現在

第15回の勉強会について

Nodeの基本的な操作

ElixirでのNode間通信を行う基本的な操作はプログラミングElixirの第15章にまとめられている。しかし、ただまねるだけでは面白くないので以下の記事を参考にNode間でチャットを作る簡易的なアプリケーションを作ってみようと思う

dev.classmethod.jp

その前に基本的な Nodeに関するコマンド処理を覚えておこう。まず複数のNodeを識別する必要があるためNodeに名前を付けてあげよう。名前を付ける際はiexの起動時に--snameというオプションを付与して以下のように起動する。すると、立ち上がったプロンプトに付与したNode名が表示されていることが確認できる

$ iex --sname node1
:
iex(node1@okabe-y)1>

自身のNode名を確認したい時にはNode.self()とすることでアトム形式で自身のNode名を取得することが出来る

iex(node1@okabe-y)1> Node.self()
:"node1@okabe-y"

これでNodeへの名前付けと自身の名前の確認が出来るようになった。次にもう1つターミナルを用意して、別のNodeを立ち上げてみる。こちらにはnode2という名前を付与する

$ iex --sname node2
:
iex(node2@okabe-y)1>

次に、この用意した2つのNodeを実際に繋げてみる。といっても、やり方は凄く簡単で自身のNodeに接続させてやりたいNodeの情報をNode.connect()/1を通して渡してやるだけだ。今回はnode1に対してnode2を接続させてみよう。まずはnode2のターミナルで自身のNode情報を確認する

iex(node2@okabe-y)1> Node.self()
:"node2@okabe-y"

次に、node1のターミナルで先ほど確認したnode2の情報を用いて、実際にNodeを接続させる。接続に無事に成功するとtrueが返ってくる

iex(node1@okabe-y)2> Node.connect(:"node2@okabe-y")
true

ついでに自身のNodeに今、どのNodeが接続しているのかを確認しておこう。Node.list()とすることで接続されているNodeの一覧をリスト形式で取得する事が出来る

iex(node1@okabe-y)3> Node.list()
[:"node2@okabe-y"]

無事に、先ほどの接続が完了していることが確認出来た

チャットサーバーの用意

ベースに使用しているのは先ほど紹介させて頂いた記事だが、せっかくなのでチャットルームの概念をサーバーに用意してみた。少しコードが長くなってしまったので順に解説はしていく

defmodule ClientInfo do
  defstruct name: "GUEST", room: "roomA", pid: nil
end

defmodule ChatServer do
  def start() do
    pid = spawn(__MODULE__, :receiver, [[]])
    :global.register_name(:server, pid)
  end
  def receiver(clients) do
    receive do
      # 全体通知のメッセージ
      {sender, {:announce, body}} ->
        IO.puts("[ANNOUNCE] #{body}")
        send(sender, {:ok})
        receiver(clients)
      # ルーム単位のメッセージ
      {sender, {:message, room, body}} ->
        Enum.filter(clients, fn client ->
          client.room == room
        end)
        |> Enum.map(fn client ->
          IO.puts("[MESSAGE]: #{client.room}@#{client.name} #{body}")
        end)
        send(sender, {:ok})
        receiver(clients)
      # ルームへの新規参加
      {sender, {:join, room, name}} ->
        client = %ClientInfo{name: name, room: room, pid: sender}
        add = clients ++ [client]
        IO.puts("[JOIN]: #{name} joined the #{room}")
        send(sender, {:ok})
        receiver(add)
      # ルームから退室
      {sender, {:leave, room, name}} ->
        IO.puts("[LEAVE]: #{name} left the #{room}")
        send(sender, {:ok})
        receiver(Enum.filter(clients, fn client ->
          !(client.room == room and client.name == name)
        end))
      {sender, {:status}} ->
        IO.puts("[STATUS] send server status to client")
        send(sender, {:ok, :status, clients})
        receiver(clients)
    end
  end
end

defmodule Client do
  def server_pid(), do: :global.whereis_name(:server)
  def announce(body) do
    send(server_pid(), {self(), {:announce, body}})
    receiver()
  end
  def message(room, body) do
    send(server_pid(), {self(), {:message, room, body}})
    receiver()
  end
  def join(room, name) do
    send(server_pid(), {self(), {:join, room, name}})
    receiver()
  end
  def leave(room, name) do
    send(server_pid(), {self(), {:leave, room, name}})
    receiver()
  end
  def status() do
    send(server_pid(), {self(), {:status}})
    receiver()
  end
  def receiver() do
    receive do
      {:ok} -> IO.puts("success send message to server")
      {:ok, :status, data} ->
        Enum.map(data, fn d ->
          IO.puts("[INFO]: #{d.name} joined #{d.room}")
        end)
      _ -> IO.puts("invalid message")
      after 3000 ->
        IO.puts("not reply...")
    end
  end
end

まずはチャットサーバーで扱うクライアントのデータの構造をstructを用いて定義している。今回のチャットサーバーは凄くシンプルなもので、それぞれ以下を参照している

  • name -> クライアントの名前
  • room -> クライアントが入室している部屋(今回は入室は1室のみ)
  • pid -> クライアントのPID
defmodule ClientInfo do
  defstruct name: "GUEST", room: "roomA", pid: nil
end

次にメインとなるチャットサーバーの実装がChatServerモジュールに記述してある。基本的にやっていることは凄くシンプルでElixirのメッセージパッシングの構文receiveをパターンマッチングを使って複数種類のメッセージを受け取るようにして、それぞれのメッセージを受け取った際の処理を順に記述しているだけになる

  • announce -> 接続しているクライアント全員にメッセージを通知
  • message -> 指定したルームにいるクライアント全員にメッセージを通知
  • join -> 指定したルームに新規にクライアントを参加させる
  • leave -> 指定したルームからクライアントを退室(削除)させる
  • status(デバッグ用) -> 現在チャットサーバーに接続しているクライアントの情報(誰がどこのルームに参加しているか)を返す

また、start()を呼び出すことで、メッセージ受信用のプロセスを新規で立ち上げて、:global.register_nameを使って、接続しているNode間で立ち上げた新規のプロセス情報を共有する。これはメッセージをメッセージ受信用のプロセスに送信するために行なっている(sendPIDを指定する必要があるため)

そして、最後にクライアントがメッセージを送信するためのモジュールがClientになる。それぞれの関数はsend構文をwrapして、関数単位で処理が出来るようにしているだけなので、実装しなくても同様の処理は行えるが、楽にメッセージを送信するために実装した

メッセージを送ってみる

先ほど実装したコードを適当なファイル名を付けてiexを実行したパスに配置しよう。僕は適当にmessage_server.exという名前を付けた。このファイルをそれぞれのiex上からコンパイルしてモジュールを実行可能にする

iex(node1@okabe-y)4> c("message_server.ex")
[Client, ChatServer, ClientInfo]
iex(node2@okabe-y)2> c("message_server.ex")
[Client, ChatServer, ClientInfo]

node1をメッセージ受信に用いて、node2からメッセージを送ってみる。まず、node1でチャットサーバーを起動する。その前にNode.list()を確認して、node1にnode2が接続していることを確認しておこう。Nodeが接続されていないと、:global.whereis_nameが値(チャットサーバーのPID)が取得出来ないので、メッセージを送る事が出来ない
無事に、チャットサーバーが立ち上がると:yesというアトムが返ってくる

iex(node1@okabe-y)5> Node.list()
[:"node2@okabe-y"]


iex(node1@okabe-y)6> ChatServer.start()
:yes

次に、node2の方からメッセージを送ってみる

iex(node2@okabe-y)3> Client.join("Elixirを朝まで語る部屋", "okb")
success send message to server
:ok

node1のターミナルを確認すると受信したメッセージを元に作成したログを確認することが出来るだろう

[JOIN]: okb joined the Elixirを朝まで語る部屋

せっかくなのでもう1つNodeを追加して、よりチャットっぽさを出してみよう。新たにターミナルを立ち上げてnode3という名前でiexを用意して、node1に接続させる

$ iex --sname node3
:
iex(node3@okabe-y)1> c("message_server.ex")
[Client, ChatServer, ClientInfo]

node1のターミナル

iex(node1@okabe-y)7> Node.connect(:"node3@okabe-y")
true

無事に成功したようなので、node3からnode2と同じチャットルームにjoinさせて、適当にチャットしてみる

iex(node3@okabe-y)3> Client.join("Elixirを朝まで語る部屋", "にゃーん")
success send message to server
:ok

node1

[JOIN]: にゃーん joined the Elixirを朝まで語る部屋

チャットサーバーに接続しているクライアントの確認
node2

iex(node2@okabe-y)5> Client.status()
[INFO]: okb joined Elixirを朝まで語る部屋
[INFO]: にゃーん joined Elixirを朝まで語る部屋
[:ok, :ok]

では、メッセージを送ってみよう node2

iex(node2@okabe-y)6> Client.message("Elixirを朝まで語る部屋", "こんにちは、初めまして!")
success send message to server
:ok

node1

# 送信されたというログ(フロントを実装していないため、サーバーログで確認)
[MESSAGE]: Elixirを朝まで語る部屋@okb こんにちは、初めまして!
[MESSAGE]: Elixirを朝まで語る部屋@にゃーん こんにちは、初めまして!

node3

iex(node3@okabe-y)5> Client.message("Elixirを朝まで語る部屋", "こんにちは、okbさん")
success send message to server
:ok

node1

[MESSAGE]: Elixirを朝まで語る部屋@okb こんにちは、okbさん
[MESSAGE]: Elixirを朝まで語る部屋@にゃーん こんにちは、okbさん

nod2

iex(node2@okabe-y)12> Client.leave("Elixirを朝まで語る部屋", "okb")
success send message to server
:ok

node1

[LEAVE]: okb left the Elixirを朝まで語る部屋

node3

iex(node3@okabe-y)6> Client.message("Elixirを朝まで語る部屋", "まだ朝じゃないのにいなくなったww")
success send message to server
:ok

node1

[MESSAGE]: Elixirを朝まで語る部屋@にゃーん まだ朝じゃないのにいなくなったww

無事にやり取りが出来ていることが確認出来た

今後の発展

今回のNode間通信は同一のホストで行なっただけで、まだNode間通信の真髄を味わったわけではない。それにチャットサーバーもクライアント管理が適当であったり、認証がなかったり、データ構造がいまいちだったりと改善の余地が山ほどある。しかしながら、ElixirでのNode間通信の基礎を楽しく抑えるには十分だろう

次はDockerを使って別ホストとのNode間通信を行なったり、cloudのVMと接続したりということをやってみようと思う

参考文献