お世話になっています。
質問文が長くなってしまいましたので、聞きたい内容からです。
「TCPヘッダにあるウィンドウサイズを確認し、受信したメッセージデータがウィンドウサイズと一致していれば次のメッセージを待つ」
……という事をNode.jsのnetライブラリでどうすれば実現出来るか教えてください。
無理なら無理でも大丈夫です。
無理な場合、諦めて回避策を実施しようと考えています。
(一応下に共有しています)
状況説明
Node.jsでTCPサーバを構築しています。
開発は滞り無く進み結合テストまでいきました。
※本当はモジュール化されていますが、今回は質問の為に必要なコードを別のファイルから引っ張ってきて同じスコープ内にまとめています。
JavaScript
1const net = require('net') 2 3// receive_from_router :: Socket -> Buffer -> Undefined 4const receive_from_router = (socket, buf) => { 5 // バッファー文字列のバリデート 6 // 妥当なデータならばDBに問い合わせたり保存したり色々する 7 // また、動作の成否を元に接続したクライアントに結果を返す 8 socket.write(result, () => client.end()) 9} 10 11const server = net.createServer(socket => { 12 socket.on('data', data => receive_from_router(socket, data)) 13 socket.on('end', () => console.log(`${socket.remoteAddress}: disconnected.`) 14}) 15server.on('connection', socket => console.log(`${socket.remoteAddress}: connected.`)) 16server.on('error', err => console.error(err)) 17server.listen(45000)
問題発生
結合テスト中に、接続したクライアントが超長いバイト文字列を送りつけて来ました。
そのサイズ……実に2,576Byte!!
意外としょぼいですがTCPが一度に受け取れるパケットサイズは大したことなく、
1,440Byte目で区切られてしまい、2度data
イベントが発火している事が分かりました。
その結果、2つに分けられたメッセージはどちらもバリデートエラーで捨てられてしまいました。
- 1度目のメッセージ: データ量が足りず必須項目の入力エラー
- 2度目のメッセージ: 変に途中から始まっているので、メッセージ内のヘッダー情報が不正
connectListenerで作られるsocketを、data内部でconsole.logで出力してみました。
1回目はsocket._handle.bytesRead
の中身が1440で、2回目は2576でしたが、
他には使えそうな情報はなさそうです。
他のサイトの情報を元にsocket.bufferSize
も確認しましたが、dataイベントの中身では0が返ってきただけでした。
Socket { connecting: false, _hadError: false, _handle: TCP { bytesRead: 1440, _externalStream: [External], fd: 14, reading: true, owner: [Circular], onread: [Function: onread], onconnection: null, writeQueueSize: 0 }, _parent: null, _host: null, _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: false, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, domain: null, _events: { end: [ [Object], [Function] ], finish: [Function: onSocketFinish], _socketEnd: [Function: onSocketEnd], data: [Function] }, _eventsCount: 4, _maxListeners: undefined, _writableState: WritableState { objectMode: false, highWaterMark: 16384, finalCalled: false, needDrain: false, ending: false, ended: false, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 0, prefinished: false, errorEmitted: false, bufferedRequestCount: 0, corkedRequestsFree: { next: null, entry: null, finish: [Function: bound onCorkedFinish] } }, writable: true, allowHalfOpen: false, _bytesDispatched: 0, _sockname: null, _pendingData: null, _pendingEncoding: '', server: Server { domain: null, _events: { connection: [Array], error: [Function] }, _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: TCP { bytesRead: 0, _externalStream: [External], fd: 11, reading: false, owner: [Circular], onread: null, onconnection: [Function: onconnection], writeQueueSize: 0 }, _usingSlaves: false, _slaves: [], _unref: false, allowHalfOpen: false, pauseOnConnect: false, _connectionKey: '6::::40002', [Symbol(asyncId)]: 13 }, _server: Server { domain: null, _events: { connection: [Array], error: [Function] }, _eventsCount: 2, _maxListeners: undefined, _connections: 1, _handle: TCP { bytesRead: 0, _externalStream: [External], fd: 11, reading: false, owner: [Circular], onread: null, onconnection: [Function: onconnection], writeQueueSize: 0 }, _usingSlaves: false, _slaves: [], _unref: false, allowHalfOpen: false, pauseOnConnect: false, _connectionKey: '6::::40002', [Symbol(asyncId)]: 13 }, _peername: { address: '::ffff:118.238.220.66', family: 'IPv6', port: 61295 }, read: [Function], _consuming: true, [Symbol(asyncId)]: 118, [Symbol(bytesRead)]: 0 }
ゴールの定義
Bufferの結合は(パフォーマンスはともかく)これで行けそうです。
なので、素直に書けば下記のような感じのコードになるかと思います。
JavaScript
1// isFull :: Socket -> Boolean 2const isFull = (socket, buf) => true // メッセージの状態から完了か否かを判別する 3const server = net.createServer(socket => { 4 const buffers = []; 5 socket.on('data', data => { 6 buffers.push(data) 7 if (isFull(socket, data)) receive_from_router(socket, Buffer.concat(buffers)) 8 }) 9 client.on('end', () => console.log(`${socket.remoteAddress}: disconnected.`) 10})
問題はこのisFull
関数の判別ロジックをどうすれば良いのか?
状況だけ説明しておくと、クライアントは2メッセージ目を追撃で送信することはなく、レスポンスを待つだけです。
なのでnetライブラリのend
イベントで引っ掛ける事も難しいのが現状です。
(end
イベントはクライアントのFINに反応する仕様のようです)
TCPの仕様とNode.jsのnetライブラリの挙動は余り詳しくないので、
こうすれば検知出来るという答えがありましたら教えてください。
試行錯誤の跡
1,440Byteで区切られるなら、その文字列で切れば良いじゃないかと思ったのですがどうやらダメそうです。
一般的にはウィンドウ・サイズの初期値は数Kbytesから数十Kbytes程度である。この値は16bitの符号なし整数値で表現されるので、最大なら64Kbytesまで拡大することも可能であるが、現在ではTCP/IPの規格が拡張され、さらに大きなサイズにすることも可能となっている。
TCPの仕様でバッファーのオーバーフローを防ぐ為に可変式のウィンドウサイズというものが、
送受信されるパケットのヘッダー部にあり、それを元に最大文字列数が決定されているようです。
そこで「TCPヘッダにあるウィンドウサイズを確認し、受信したメッセージデータがウィンドウサイズと一致していれば待つ」が実現出来れば解決しそうだと考えているのですが、公式サイトのドキュメントから、このメッセージヘッダーやウィンドウサイズを確認する方法が分からず途方にくれています。
以下参考にしたサイト
- 第41回レイヤ4 TCP ウィンドウ - 3 Minutes NetWorking
- 第14回 信頼性のある通信を実現するTCPプロトコル(その1) (3/3)
- Net - Node.js v9.10.1 Documentation
- node/lib/net.js - GitHub
Node.js TCP server incoming buffer - スタックオーバーフロー
this.disconnected
でクライアントが切断するまで待ってる設計
今回のクライアントはメッセージを送った後、バリデート結果等のレスポンスを受け取りたいのでFINを発射せず待っている。
結局どうしたか
今回は急遽クライアントとの連絡で使うメッセージの仕様を拡張し、
「今から送信するメッセージ長は2,576Byteですよ」という自己申告を埋め込んでおき、
受信したメッセージ長が1,440Byteなら、「ああ、更に1,136Byteが後で届くのね」という作りにして対応しようと考えています。
TCPの仕様はお互いが好き勝手にパケットを投げつけあって、その結果どう動くかは好きにしろみたいな仕様だと思います。
そこから今回の対応のように「今から送信するメッセージ長は2,576Byteですよ」とするのが一番自然なのかも知れないなとも考えています。
でも、今回のメッセージ分割は特別な操作をせずにTCPの仕様に従って自然と行われた事なので、
実は私が見落としているだけでウィンドウサイズを確認してよしなに実行出来る何かがあるんじゃないか?と考えています。
以上、長くなりましたがよろしくおねがいします。
追記1
Node.jsのnetなので、TCPの仕様が原因なのではなく、
単純にNode.jsのStreamの仕様が原因なのかもしれません。
その辺もあまり詳しくないので、Node.jsの内部実装に詳しい方から見れば的外れな考察しているかも知れないと考えています。
回答2件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/03/31 14:57
2018/03/31 16:37