質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

TCP

TCP(Transmission Control Protocol)とは、トランスポート層のプロトコルで、コネクション型のデータサービスです。

Q&A

解決済

2回答

1843閲覧

TCPサーバ(net)でバッファーのオーバフローを検知したい

miyabi-sun

総合スコア21158

Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

TCP

TCP(Transmission Control Protocol)とは、トランスポート層のプロトコルで、コネクション型のデータサービスです。

4グッド

3クリップ

投稿2018/03/30 13:08

編集2018/03/30 13:33

お世話になっています。

質問文が長くなってしまいましたので、聞きたい内容からです。
「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ヘッダにあるウィンドウサイズを確認し、受信したメッセージデータがウィンドウサイズと一致していれば待つ」が実現出来れば解決しそうだと考えているのですが、公式サイトのドキュメントから、このメッセージヘッダーやウィンドウサイズを確認する方法が分からず途方にくれています。

以下参考にしたサイト


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の内部実装に詳しい方から見れば的外れな考察しているかも知れないと考えています。

umaru, defghi1977, ikapy👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

Node.jsを含め、一般的にアプリケーション側のソケットプログラミングにおいて、TCPのウィンドウサイズを意識して受信側データの終端を認識する、と言うようなことはしません。

ウィンドウサイズを意識するのは、パケットの解析や、パフォーマンス改善の為にカーネルのチューニングをするようなケースだけだと思います。

アプリケーションの開発側としては、メッセージの終端の扱いをどうするかは、独自に定義したプロトコル仕様に沿うかたちになります。

例えばTCPの既知のプロトコルであるHTTPでも、末端が改行コード\r\n\r\nと連続すれば終わり、と認識する場合もあれば、最初のヘッダー部に Content-Length: 1234\r\n" とのようにテキストでContent部のデータ長をセットし、受信側でそれをもってデータ長と認識し、処理を続けるケースもあります。

簡単なアプリケーションでは、メッセージを「行」のイメージで扱い、末端を\r\nとするケースもあります。

では結局、どうしたらいいのか、と言う点については、

今回は急遽クライアントとの連絡で使うメッセージの仕様を拡張し、
「今から送信するメッセージ長は2,576Byteですよ」という自己申告を埋め込んでおき、
受信したメッセージ長が1,440Byteなら、「ああ、更に1,136Byteが後で届くのね」という作りにして対応

基本的にこれで良いのではないでしょうか。ストリーム指向で受信するにしても、受信すべきバイト数に達するまで繰り返し受信し続けるだけです。

Node.jsでウィンドウサイズを取得できるかどうかは分かりませんが、低レベルのソケット関連のAPIか、お使いのOSのカーネルのシステムコールやAPIに依存するはずです。取得できたとしても、よほどパフォーマンスのチューニングが必要でない限り、意識する必要は無いはずです。言い換えると、一般的なソケットプログラミングのアプリケーションでは、TCPのウィンドウサイズなどと、低レベルな部分を意識して作るべきではありません。(知っていて損はありませんが)


追記しました:2018/03/31 11:31

そもそものご質問が「TCPウィンドウサイズを取得するには」と、手段を問うものでもあったと思いますので、個人的興味もあり、少し調べてみました。補足させていただきます。

Node.jsでウィンドウサイズを取得できるかどうかは分かりませんが、

※我ながら「分からないなら回答するな。」と自分で思ってしまったのも大きいですが。

github のnodejs/nodeのリポジトリからソースをcloneし、軽く見てみました。読み込みが足りないかもしれませんが、ソケットに関する低レベルな設定を行っている部分は見当たりませんでした。そこで、Linuxのカーネルの方に視点を向けてみます。

こと、Linuxに限って言えばTCPのウィンドウサイズは/procファイルシステム中に現れるtcp_rmemファイルで参考となる情報を得られると思います。Ubuntu 17.04(x86)で試した例です。

$ uname -a Linux vmubuntu17 4.10.0-38-generic #42-Ubuntu SMP Tue Oct 10 13:24:28 UTC 2017 i686 i686 i686 GNU/Linux $ cat /proc/sys/net/ipv4/tcp_rmem 4096 87380 6183360

tcp_rmem の内容は、Man page of TCPtcp_rmemによると、並びがmin default max とあります。このファイルをNodeのプログラムにおいてネイティブコールで読み出せば、参考程度にはなります。しかしながらTCPウィンドウサイズは常に一定と言うわけではなく、OSによって動的に変わるもののようなので、そもそもリアルタイムに取得することは難しい(あるいは不可能)とも取れます。リアルタイムに現在の値を取得できないと言うことは、「TCPウィンドウサイズを意識したプログラミング自体、不可能」という結論にも繋がるかと思います。

参考ですが、Nodeのソケットはnet.Socketのメンバーであるfdがネイティブのソケットのディスクリプターに対応していると推察します(未確認です)
net.Socketからは逸脱しますが、このfdの値を引数としてネイティブのsocket関連のシステムコールを使えば、ソケットを介して低レベルな情報のやり取りもやってできないことはなさそうです。

ヒント 3. 帯域幅遅延積に対して TCP ウィンドウを調整する

更に参考として、Windows。レジストリに書き込むことでTCPウィンドウサイズの範囲を変えられるようです。が、デフォルトでは存在しないのでここから取得することは確実性が低いと言えます。
TcpWindowSize
WindowsVista/7のウインドウサイズ

Windows APIやWindowsドライバーを利用した取得方法は無いものかと軽く探してみたのですが、見当たりませんでした。Windowsに関してはかなり昔に同じトピックで一度探してみたことがあるのですが、依然としてプログラムインターフェース上でも公開されていないものと思われます。
※Windowsドライバーに関しては、丁寧にリファレンスを探すと見つかるのかもしれません。

投稿2018/03/30 23:13

編集2018/03/31 02:32
dodox86

総合スコア9183

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

miyabi-sun

2018/03/31 14:57

私としてはどう設計するべきかに関してが本当に聞きたい内容だったと思います。 ですので、前半・後半共にとても参考になりました! HTTPの実装を絡めた説明は非常に腑に落ちるものでした。 TCPの仕様上、追撃メッセージが来るか来ないかなんて分かるはずがないので、クライアントとサーバ間で規約作って何処がメッセージの終わりかちゃんと定義しておくべきですね。 そしてOSを絡めた説明も非常に参考になりました! どう見てもNode.jsが用意しているnetライブラリが用意しているインスタンスのプロパティやメソッドが貧弱である事から不思議に思ってたのですが、 そこからOSの実装を調べる所が私にとっては完全に未知の領域で完全にあきらめてました…… > TCPウィンドウサイズを意識したプログラミング自体、不可能 技術面でもシンプルな結論ありがとうございます。 回答文中のリンクを幾つか参考にしましたが、 どれも速度面の都合でウィンドウサイズは単なる参考値であり、 アルゴリズムが更に速いパケットサイズがあればそれで送るように書いてありますね…… > fdがネイティブのソケットのディスクリプターに対応していると推察します fd: 14というNumber型が得られるだけなんで、I/Oが作ったセッションファイルみたいなものを開いて読みに行くとまた時間かかりそうですね… 仰る通りやる気になればやってやれないことはないかも知れませんが、Node.jsだと速度もかなり劣化しそうですし、間違いなくやるべきではないが結論になるでしょうね。 素晴らしい回答ありがとうございました!
dodox86

2018/03/31 16:37

コメントありがとうございます。お役に立てたようでよかったです。私もNode.jsに絡め、改めて勉強になりました。
guest

0

2576byte送ってきて分割される、というのが問題なら、分割されたのを結合して処理すればいいと思いますが、そこらへんどうでしょうか。

そもそもTCPではデータがどういう単位で送られてくるかは不定ですね

#送信側で1440byte送っても実際には1byteを1440回送信されても文句は言えない

投稿2018/03/30 22:06

編集2018/03/30 22:11
y_waiwai

総合スコア87719

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問