以前、システムコールを利用して簡単なTCPサーバーを実装しました。
hello world
という文字列が返るだけのジンプルな実装になっていましたが、他にも音声や動画といったデータをTCPを通じて送れるのか気になりました(...送れると思います)。
現代ではYouTubeやSpotifyといった音声・動画を配信するサービスが多くなりましたが、同じようなことが自分の実装で可能なのでしょうか。
今回は実装したTCPサーバーを拡張して音声ファイルを配信してみようと思います。
できたもの
コード量が増えてきたのでGithubにて公開しました。
前回と同じようにサーバー側とクライアント側で順に紹介していきます。
サーバー側
意外にもあっさりと実装できました。
前回はhello world
という文字列からバイナリ型の値を作成して配信したのですが、この箇所を読み取った音声ファイル(.mp3)のバイナリを配信するようにしたのみです。ただ、一度に音声ファイルのデータをまとめて送るとパケットの容量が肥大化してしまいますし、TCPのデータ容量の最大値を超過してしまう可能性が考えられます。
そのため、取得したファイルのポインター経由で少しずつ(1024byte)バイナリを読み取って、複数回に分割して配信するようにしました。
func handleClient(clientFd int) { buffer := make([]byte, 1024) soundFile, err := os.Open("./static/birdland1.mp3") if err != nil { fmt.Println("[ERROR] Failed Open file: ", err) os.Exit(1) } defer soundFile.Close() for { n, err := soundFile.Read(buffer) if err == io.EOF { break } if err != nil { fmt.Println("[ERROR] Failed Read buffer: ", err) return } if _, err := syscall.Write(clientFd, buffer[:n]); err != nil { fmt.Println("[ERROR] Failed Write buffer to file descriptor: ", err) return } } if _, err := syscall.Write(clientFd, []byte("EOF")); err != nil { fmt.Println("[ERROR] Failed Write 'EOF' to file descriptor: ", err) os.Exit(1) } syscall.Close(clientFd) }
socket-challenge/main.go at send-mp3 · okabe-yuya/socket-challenge · GitHub
クライアント側で読み取り終了を判定するために、最後にEOF
というバイナリを送るようにしました。
また、本当はサーバー起動時に一度だけ音声ファイルを読み込んで、配信する方がメモリに優しいとは思うのですが複数クライアントが接続してくることを考慮する必要があり、Mutexなどを利用して排他制御をしてSeekさせるのが面倒だったので、Accept後に起動するgoroutineの内部でファイルを読み込む方式を採用しました。
悔しい...。
クライアント側
サーバー側の変更に伴ってクライアント側も変更しました。
前回とは異なり、音声ファイル全体を受信する必要があるためEOF
の値が取得されるまで、繰り返し受信するようにしました。
取得したバイナリは文字例として結合させています。
# frozen_string_literal: true require 'socket' require 'securerandom' port = 8001 s = TCPSocket.open('127.0.0.1', port) buffer = '' puts ':::Start receive binary data' binary = s.gets until binary == 'EOF' binary = s.gets break if binary.nil? buffer += binary end s.close
音声の再生
このままバイナリから音声を再生できれば嬉しいのですが、プログラムからバイナリを読み込んで音声を再生させるのは非常に難しいです。ライブラリを使うことも検討しましたが、以前、マクドナルドのポテトのティロリ音を再生する際に使用したafplay
コマンドを使用しました。
一度、バイナリをファイルに書き出してafplay
を実行後、ファイルを削除するようになっています。
本当はファイルに書き出したくないのですが諦めました。
puts ':::Received binary data' file_name = "#{SecureRandom.uuid}_received_se.mp3" File.open(file_name, 'w') do |f| f.write(buffer) end puts ':::Play sound' system('afplay', file_name) File.delete(file_name)
socket-challenge/client.rb at send-mp3 · okabe-yuya/socket-challenge · GitHub
実行結果
サーバー側を起動しておきます。
$ go run main.go Server is listening on 0.0.0.0:8001
この状態で、クライアント側を起動します。
$ ruby client.rb hello world hello world hello world hello world hello world
音声が再生されていることを確認するため、実行結果を録画しました。
無事に音声が再生されました。すごい!楽しい!
最後に
今回の実装を通して個人的には非常に驚かされました。
というのも、分割して受信したバイナリをクライアント側で結合したものが、音声として問題なく再生できたという点です。
つまり、基本的にバイナリとして送信できれば、ファイルの形式は問わないということでしょうか。
音声ファイルを受信・再生できたので、次は動画ファイルを受信・再生できるように挑戦しようと思います。
少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。