|
サーバー側でこのキーを検証するには、キーに特定の文字列を追加して sha1 操作を実行し、結果を Base64 に変換して送り返すことを意味します | var crypto = require(' crypto' ); var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; require('net').createServer(function(o) { var key; o.on('data ', function(e) { if(!key) { // 送信された KEY を取得 key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1 ] ; // 文字列 WS を接続し、sha1 操作を実行し、最後に Base64 に変換します key = crypto.createHash('sha1').update(key+WS).digest('base64'); // クライアントに返されたデータを出力します。これらのフィールドは必須です o.write('HTTP/1.1 101 Switching Protocolsrn'); o.write('Upgrade: websocketrn'); o.write ( 'Connection: Upgradern'); // このフィールドにはサーバーによって処理された KEY が含まれます o.write('Sec-WebSocket-Accept: '+key+'rn'); // 空行を出力します。 HTTPヘッダーの終了 o.write('rn'); } }); }).listen(8888); | このようにしてハンドシェイク部分が完了しました。データフレームによる分析と生成が生きています まずは公式のフレーム構造図を見てみましょう 簡単な紹介 FINは終了したかどうかの指標です RSVは予約されたスペース、0です opcode は、データタイプ、分割されたスライスであるかどうか、バイナリ解析であるかどうか、ハートビートパケットなどを識別します。 opcode に対応するマップを提供します MASK がマスクを使用するかどうか Payload len と次の extend payload lengthはデータの長さを表し、これが最も厄介です PayloadLenはわずか7ビットで、符号なし整数型に変換すると0から127までの値しか持ちません。 もちろん、そのような小さな値ではより大きなデータを記述することはできませんが、そのため、データ長が 125 以下の場合にのみデータ長の記述として使用できると規定されています。この値が 126 の場合、次の 2 バイトはデータ長を格納するために使用されます。 127 の場合、次の 8 バイトはデータ長の格納に使用されます マスキングキーマスク データフレームを解析するためのコードは以下に掲載されています | function decodeDataFrame(e) { var i = 0, j,s, frame = { FIN: e[i] >> 7, オペコード: e[i++ ] & 15, マスク: e[i] > > 7, PayloadLength: e[i++] & 0x7F }; if(frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e [i++]; } if(frame.PayloadLength === 127) { i += 4; frame.PayloadLength = (e[ i++] << 24) + (e[i++] << 16) + (e[i++] << 8) + e[i++]; } if(frame.Mask) { frame.MaskingKey = [e[i++], e[ i++], e[i++], e[i++]]; for(j = 0, s = []; j s.push(e[i+j] ^ Frame.MaskingKey[j%4]); } } else { s = e.slice(i, i+frame.PayloadLength); } s = 新しいバッファー; if(frame.Opcode === 1) { s = s.toString(); } frame.PayloadData = s; return Frame ; } | その後、データフレームは次のようになります生成された | function encodeDataFrame(e) { var s = [], o = new Buffer(e.PayloadData), l = o.length; s.push((e .FIN << 7) + e.Opcode); if(l < 126) { s.push(l); } else if(l < 0x10000) { s.push (126, (l&0xFF00) >> 8, l&0xFF); } else { s.push(127, 0, 0, 0, 0, (l&0xFF000000) >> 24, (l&0xFF0000) > > 16, (l&0xFF00) >> 8, l&0xFF); } return Buffer.concat([new Buffer(s), o]) ; } |
これらはすべてフレーム構造図に従って処理されます。ここでは詳しく説明しません。興味のある方は、Web Technology Research Institute にアクセスしてください。 3. WebSocket の写真と WebSocket の音声チャット ルームの転送 この記事のメイン部分は、WebSocket の使用シナリオをいくつか示すことです 1. 写真を転送する手順について考えてみましょう まず、サーバーはクライアントのリクエストを受信し、それを読み取り、バイナリ データをクライアントに転送します。クライアントはそれをどのように処理しますか?もちろん、FileReader オブジェクトを使用します 最初にクライアント コードを指定してください var ws = new WebSocket("ws://xxx.xxx.xxx.xxx:8888"); | ws.onopen = function (){ console.log("ハンドシェイクが成功しました"); }; ws.onmessage = function(e) { var Reader = new FileReader(); reader.onload = function(event ) { var content = events.target.result; var a = new Image(); a.src = content; document.body.appendChild(a); } reader.readAsDataURL ( e.data); }; メッセージを受信し、次に readAsDataURL を実行し、画像 Base64 をページに直接追加します | サーバー側のコードに移動します fs.readdir( "スカイランド" 、 function(err, files) { | if(err) { throw err; } for(var i = 0; i < files.length; i++) { fs.readFile ('skyland /' + files[i], function(err, data) { if(err) { throw err; } o.write(encodeImgFrame(data)); }); } }); function encodeImgFrame(buf) { var s = [], l = buf.length, ret = []; s.push((1 <<< ; 7) + 2); if(l < 126) { s.push(l); } else if(l < 0x10000) { s.push(126, (l&0xFF00) >) ;> 8, l&0xFF); } else { s.push(127, 0, 0, 0, 0, (l&0xFF000000) >> 24, (l&0xFF0000) >> 16, (l&0xFF00) >> ; 8, l&0xFF); } return Buffer.concat([new Buffer(s), buf]); } 注 | s.push((1 << 7) + 2) この文は、オペコードを 2 に直接書き込むのと同じです。バイナリ フレームの場合、クライアントはデータを受信するときに toString を実行しようとしません。そうでない場合は、エラーが報告されます~ コードは非常に単純です。ここで共有します WebSocket による写真の転送速度を見てみましょう 多くの写真、合計 824 万枚をテストしました 通常の静的リソース サーバーは約 20 秒かかります (サーバーが遠い) cdn約 2.8 秒かかります WebSocket メソッドについてはどうでしょうか? ? ! 答えは、これも約 20 秒かかります。 とてもがっかりしませんか? 送信速度が遅いのは、サーバーがローカル マシン上の同じ画像リソースを読み込むことができるということではありません。約1秒...データフローができないようです 距離制限を突破して通信速度を上げることはできません WebSocketの別の使い方を見てみましょう~ WebSocketを使用して音声チャットルームを構築します まずボイスチャットルームの機能を説明します ユーザーがチャンネルに入った後、マイクから音声を入力すると、それがバックグラウンドに送信されてチャンネル内の他の人に転送され、他の人がメッセージを受信して再生します それ問題は 2 か所にあるようです。1 つ目はオーディオ入力で、2 つ目は再生用のデータ ストリームの受信です まず話しましょうオーディオ入力については、ここでは HTML5 の getUserMedia メソッドが使用されていますが、注意してください。このメソッドはオンラインになると大きな落とし穴になります 最後に、最初にコードを投稿します if (navigator.getUserMedia) { | navigator .getUserMedia( { audio: true }, function ( stream) { var rec = new SRecorder(stream); recorder = rec; }) } first パラメータは {audio: true} で、オーディオのみを有効にしてから、 SRecorder オブジェクト。以降の操作は基本的にこのオブジェクトに対して実行されます。この時点で、 | コードがローカルで実行されている場合 、ブラウザはマイク入力を有効にするかどうかを尋ねるメッセージを表示します。確認すると、コードが起動します 次に、SRecorder コンストラクターとは何かを見て、重要な部分を説明します。 var SRecorder = function(stream) { | …… var context = new AudioContext(); var audioInput = context.createMediaStreamSource(stream); var Recorder = context.createScriptProcessor ( 4096、1、1);
AudioContext はオーディオ コンテキスト オブジェクトです。サウンド フィルタリングを行ったことがある学生は、「オーディオの一部が再生のためにスピーカーに到達する前に、途中でインターセプトされるため、オーディオ データを取得します。このインターセプトは window.AudioContext によって実行されます」ということを知っているはずです。これを行うには、オーディオに対するすべての操作がこのオブジェクトに基づいています。」 AudioContext を通じてさまざまな AudioNode ノードを作成し、フィルターを追加して特別なサウンドを再生できます 録音原理は同じですが、AudioContext を使用する必要もありますが、マイクのオーディオ入力を受信するには、通常のように ajax を使用してオーディオ ArrayBuffer オブジェクトを要求し、それをデコードすることでオーディオを処理する代わりに、マイクは createMediaStreamSource メソッドを使用する必要があります。このパラメーターは 2 番目のパラメーターであることに注意してください。 getUserMedia メソッドのパラメータ createScriptProcessor メソッドについて話しましょう。その公式の説明は次のとおりです: JavaScript 経由で直接オーディオ処理に使用できる ScriptProcessorNode を作成します。 —————— 要約すると、このメソッドは次のようになります。 JavaScript を使用して音声収集操作を処理することです いよいよ音声を収集します。勝利は目前です! 次にマイク入力をオーディオキャプチャに接続しましょう | audioInput.connect(recorder); recorder.connect(context.destination); | context.destination説明は以下の通り AudioContext インターフェイスの destination プロパティは、コンテキスト内のすべてのオーディオの最終的な宛先を表す AudioDestinationNode を返します。 —————— context.destination は、環境内のオーディオを表す最終的な宛先を返します。 この時点では、オーディオの収集を監視するイベントがまだ必要です | recorder.onaudioprocess = function (e) { audioData.input(e.inputBuffer.getChannelData(0)); } | audioData はインターネットで見つけたオブジェクトです。メインの encodeWAV メソッドは何度も実行されています。これは最後に完全なコードと一緒に掲載されます この時点で、 ユーザー全体がチャンネルに入った後、マイクから音声を入力する ことが完了しました。 次に、音声ストリームをサーバーに送信します。これは少し面倒ですが、先ほど述べたように、WebSocket はさまざまなオペコードを通じて返されたデータがテキストであるかバイナリ データであるかを示すことができ、onaudioprocess への入力は配列であり、サウンドを再生するために最終的に必要なものは Blob です。 、 {type: 'audio/wav'} のオブジェクトなので、送信する前に配列を WAV blob に変換する必要があります。このとき、上記の encodeWAV メソッドが使用されます サーバーは非常に単純であるようです。転送してください ローカルテストは確かに可能です ただし、シンクホールは発生します わかりました! サーバー上でプログラムを実行するときに getUserMedia メソッドを呼び出すと、安全な環境にある必要があること、つまり https が必要であることを求めるプロンプトが表示されます。つまり、ws も wss に置き換える必要があります。つまり、サーバー コードは独自のカプセル化されたハンドシェイク、解析、エンコードを使用します。コードは次のとおりです | var https = require('https'); var fs = require('fs'); var ws = require ('ws'); var userMap = Object.create(null); var options = { key: fs.readFileSync('./privatekey.pem'), cert: fs.readFileSync(' ./certificate.pem') } ; var server = https.createServer(options, function(req, res) { res.writeHead({ 'Content-Type' : 'text/html' ) }); fs.readFile( './testaudio.html', function(err, data) { if(err) { return ; } res.end(data); }); }); var wss = new ws.Server({server: サーバー}); wss.on('接続', function(o) { o.on('メッセージ', function(message) { if(message .indexOf('user') === 0) { var user = message.split(':')[1]; userMap[user] = o; } else { for(var u in userMap) { userMap[u].send(message); } } }); }); server.listen(8888) ;
|
コードはまだ非常に単純です。https モジュールを使用し、最初に説明した ws モジュールはシミュレートされたチャネルであり、転送のコア機能のみを実装します。 ws モジュールは実装が非常に便利なので使用されます。 wss と https はロジック コード 0 と競合します https の確立については、主に秘密キー、CSR 証明書の署名、および証明書ファイルが必要です。興味のある学生はそれについて学ぶことができます。現在のネットワーク環境では getUserMedia を使用できません ……) 以下は完全なフロントエンド コードです | var a = document.getElementById('a'); var b = document .getElementById('b'); var c = ドキュメント .getElementById('c'); navigator.getUserMedia = navigator.webkitGetUserMedia || var gRecorder = null; var audio = ドキュメント.querySelector('audio'); var ドア = false ; var ws = null; b.onclick = function() { if(a.value === '') { alert( 'ユーザー名を入力してください'); return false; } if(!navigator.getUserMedia) { alert('申し訳ありませんが、お使いのデバイスはボイスチャットできません'); return false; } SRecorder .get(function (rec) { gRecorder = rec; }); ws = new WebSocket("wss://x.x.x.x:8888"); ws.onopen = function() { console .log('ハンドシェイク成功'); ws .send('user:' + a.value); }; ws.onmessage = function(e) { receive(e.data); }; document.onkeydown = function(e ) { if(e.keyCode === 65) { if(!door) { gRecorder.start(); door = true; }} }; document .onkeyup = function(e) { if(e.keyCode === 65) { if(door) { ws.send(gRecorder.getBlob( )); gRecorder.clear(); gRecorder.stop(); door = false; } } } } c.onclick = function() { if (ws) { ws.close(); } } var SRecorder = function(stream) { config = {}; config.sampleBits = config.smapleBits || 8; config.sampleRate = config.sampleRate || (44100 / 6); var context = new AudioContext(); var audioInput = context.createMediaStreamSource(stream); var Recorder = context.createScriptProcessor(4096, 1, 1); var audioData = { size : 0 // 録音ファイルの長さ , バッファ: [] // 録音バッファ , inputSampleRate: context.sampleRate // 入力サンプリング レート , inputSampleBits: 16 to to config.sampleRate //出力サンプリング レート , oututSampleBits: config.sampleBits //出力サンプル ビット 8、16 , clear: function() { this.buffer = []; this.size = 0 ; } 、入力: 関数 (データ) { this.buffer.push(new Float32Array(data)); this.size += data.length; } 、圧縮: 関数 ( ) { //マージと圧縮 //マージ var data = new Float32Array(this.size); var offset = 0; for (var i = 0; i data.set(this.buffer[i], offset); offset += this.buffer[i].length; } //Compression var Compression = parseInt(this. inputSampleRate / this.outputSampleRate); var length = data.length / COMPLEX; var result = new Float32Array(length); var Index = 0, j = 0; while (index < length) { result[index] = data[j]; j += 圧縮; index++; } return result; } , encodeWAV: function () { var sampleRate = Math. min( this.inputSampleRate, this.outputSampleRate); var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits); var bytes = this.compress(); var dataLength = bytes.length * (サンプルビット / 8); var バッファ = new ArrayBuffer(44 + dataLength); var data = new DataView(buffer); var channelCount = 1;//モノラル var offset = 0; var writeString = function (str) { for (var i = 0; i data.setUint8(offset + i, str.charCodeAt(i)); } }; // リソース交換ファイル識別子 writeString('RIFF'); offset += 4; // 次のアドレスからファイルの終わりまでの合計バイト数、つまりファイルサイズ -8 data.setUint32( offset, 36 + dataLength, true); offset += 4; //WAV ファイル フラグ writeString('WAVE'); //Wave フォーマット フラグ writeString('fmt ') ; offset += 4; //フィルターバイト、通常は 0x10 = 16 data.setUint32(offset, 16, true); //フォーマットカテゴリ (PCM フォーマット)サンプリングデータ) data.setUint16(offset, 1, true); offset += 2; // チャンネル数 data.setUint16(offset, channelCount, true);
//サンプリング レート、1 秒あたりのサンプル数は、各チャンネルの再生速度を表します data.setUint32(offset,sampleRate, true); offset += 4; //波形データ転送速度 (平均バイト数) 2 番目の数値) モノラル チャンネル モノラル チャンネルごとに 1 つのサンプルが占めるバイト数 × サンプルごとのデータ ビット数を調整します/8 data.setUint16(offset, channelCount * (sampleBits / 8), offset += 2); // サンプルごとのデータビット数 data.setUint16(offset, sampleBits, true); offset += 2; //データ識別子 writeString('data'); //サンプルデータの総数、つまりデータの総サイズ -44 data.setUint32(offset, dataLength, true); offset += 4; // サンプルデータを書き込む if (sampleBits == = 8) { for (var i = 0; i
var s = Math.max(-1, Math.min(1, bytes[i])); var = s
val = parseInt(255 / (65535 / (val + 32768))); data.setInt8(offset, val, true); } } else { for (var i = 0; i
var s = Math.max(-1, Math.min(1, bytes[i])); data.setInt16(offset, s
} } return new Blob([data], { type: 'オーディオ/wav' }); } }; this.start = function () { audioInput.connect(recorder); recorder.connect(context.destination); } this.stop = function () { recorder .disconnect(); } this.getBlob = function () { return audioData.encodeWAV(); } this.clear = function() { audioData.clear(); } recorder.onaudioprocess = function (e) { audioData.input(e.inputBuffer.getChannelData(0)); } }; SRecorder。 get = function (コールバック) { if (コールバック) { if (navigator.getUserMedia) { navigator.getUserMedia( { audio: true }, function (stream) { var rec = new SRecorder(stream); callback(rec) ; }) } } } function accept(e) { audio.src = window.URL.createObjectURL(e); } 注: ライブで a キーを押して話し、a キーを放して送信します ボタンを押さずにリアルタイムで会話しようとして、setInterval を通して送信しましたが、ノイズがこれには、encodeWAV カプセル化の別の層と、環境ノイズを除去するためのより多くの機能が必要です
| この記事では、まず WebSocket の将来について展望します。仕様に従って自分たちでデータ フレームを解析して生成することを試み、WebSocket についてより深く理解しました 最後に、2 つのデモを見ていきます。WebSocket の可能性に関して言えば、音声チャット ルームのデモは幅広い範囲をカバーしています。 AudioContext オブジェクトに慣れていない学生は、まず AudioContext について理解する必要があります この記事はここで終わります~ アイデアや質問があれば、一緒に議論したり探索したりすることを歓迎します ~ 著者: TAT に感謝します。 vorshen 原文: http://www.alloyteam.com/2015/12/websockets-ability-to-explore-it-with-voice-pictures/
|
|