🔗 Node-RED TCP ノードガイド

📚 目次

📖 1. 概要

TCP ノードは、低レベルのTCP/IPソケット通信を行うためのノードです。HTTPやMQTTなどの高レベルプロトコルを使わず、直接TCPで通信したい場合に使用します。

📞 電話に例えると:TCPは「糸電話」のようなものです。相手と1対1で線をつなぎ、その線を通じてデータを送り合います。HTTPやMQTTはこの「糸電話」の上に決まった会話ルール(プロトコル)を載せたものです。

TCPの特徴

📥 TCP In

TCP In

役割: TCP接続を待ち受け、データを受信

サーバー/クライアント両モード対応

📤 TCP Out

TCP Out

役割: TCP接続でデータを送信

既存接続または新規接続に送信

🔄 TCP Request

TCP Request

役割: リクエスト送信→レスポンス受信

HTTP Requestのような使い方

⚙️ 2. TCP In ノード

動作モード

モード説明用途
Listen on 指定ポートでサーバーとして待ち受け TCPサーバーを構築
Connect to 指定ホスト:ポートにクライアントとして接続 外部TCPサーバーに接続

主要プロパティ

プロパティ説明
種類Listen on / Connect toListen on
ポートポート番号9000
ホスト接続先ホスト(Connect to時)192.168.1.100
出力出力形式stream / single / after silence

Output オプション

オプション説明用途
stream of 固定長バイト数で分割出力 バイナリプロトコル
single 接続ごとに1メッセージ 短いデータ
after silence of 指定秒の無通信後に出力 不定長データ
never - keep connection 接続維持、データごとに出力 ストリーム処理

受信時の msg プロパティ

プロパティ説明
msg.payload受信データ(Buffer または String)
msg._session接続セッション識別子
msg.ip接続元IPアドレス
msg.port接続元ポート番号

⚙️ 3. TCP Out ノード

動作モード

モード説明用途
Listen on サーバーとして待ち受け、接続したクライアントに送信 サーバーからクライアントへ送信
Connect to 指定ホストに接続して送信 クライアントとして送信
Reply to TCP TCP In で受信した接続に返信 リクエスト→レスポンス

主要プロパティ

プロパティ説明
種類動作モードReply to TCP
ポートポート番号9000
ホスト接続先ホスト192.168.1.100
接続を閉じる送信後に接続を閉じるafter each message

送信時に設定できる msg プロパティ

プロパティ説明
msg.payload送信データ(String または Buffer)
msg._session送信先セッション(Reply to TCP時に必要)

⚙️ 4. TCP Request ノード

TCP Request ノードは、リクエスト送信→レスポンス待機を1ノードで行います。HTTP Request に似た動作です。

主要プロパティ

プロパティ説明
ホスト接続先ホスト192.168.1.100
ポート接続先ポート9000
出力レスポンス出力条件after fixed number of chars

Return オプション

オプション説明
never - keep connection open接続維持、データ受信ごとに出力
after fixed number of chars指定バイト数受信後に出力
when character received特定文字(例: 改行)受信時に出力
after a fixed timeoutタイムアウト後に出力
immediately送信後すぐに出力(レスポンス待たない)

🔧 5. 実用的な使用パターン

📥 サンプルフローのインポート方法:

  1. 下のサンプルフローJSONをコピー
  2. Node-REDエディタで メニュー → 読み込み を選択
  3. JSONをペーストして「読み込み」をクリック

このサンプルフローには、以下で説明するパターンの実例が含まれています。

⚠️ テスト方法:

TCPのテストには以下のツールを使用できます:

netcat でのテスト例

# サーバーに接続(サンプルフロー実行後) nc localhost 9000 # メッセージを入力してEnter Hello Server! # サーバーからのレスポンスが表示される
📥 サンプルフローJSON(クリックで展開)
[ { "id": "tcp_sample_tab", "type": "tab", "label": "TCP サンプル", "disabled": false, "info": "" }, { "id": "tcp_comment1", "type": "comment", "z": "tcp_sample_tab", "name": "━━━ TCPサーバー(エコー) ━━━", "info": "ポート9000で待ち受け、受信データをそのまま返す", "x": 200, "y": 40, "wires": [] }, { "id": "tcp_in_server", "type": "tcp in", "z": "tcp_sample_tab", "name": "TCP Server :9000", "server": "server", "host": "", "port": "9000", "datamode": "stream", "datatype": "utf8", "newline": "", "topic": "", "trim": false, "base64": false, "tls": "", "x": 180, "y": 100, "wires": [["tcp_func_echo", "tcp_debug_received"]] }, { "id": "tcp_func_echo", "type": "function", "z": "tcp_sample_tab", "name": "エコー処理", "func": "// 受信データに情報を追加して返信\nvar received = msg.payload;\nvar clientInfo = msg.ip + \":\" + msg.port;\n\nnode.status({fill: \"green\", shape: \"dot\", text: clientInfo});\n\nmsg.payload = \"[Echo] \" + received + \" (from \" + clientInfo + \")\\n\";\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 390, "y": 100, "wires": [["tcp_out_reply"]] }, { "id": "tcp_out_reply", "type": "tcp out", "z": "tcp_sample_tab", "name": "Reply to Client", "host": "", "port": "", "beserver": "reply", "base64": false, "end": false, "tls": "", "x": 600, "y": 100, "wires": [] }, { "id": "tcp_debug_received", "type": "debug", "z": "tcp_sample_tab", "name": "受信データ", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "ip", "statusType": "msg", "x": 390, "y": 140, "wires": [] }, { "id": "tcp_comment2", "type": "comment", "z": "tcp_sample_tab", "name": "━━━ TCP Request(クライアント) ━━━", "info": "上記サーバーにリクエストを送信", "x": 210, "y": 220, "wires": [] }, { "id": "tcp_inject_request", "type": "inject", "z": "tcp_sample_tab", "name": "リクエスト送信", "props": [{"p": "payload"}], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "Hello TCP Server!", "payloadType": "str", "x": 180, "y": 280, "wires": [["tcp_request"]] }, { "id": "tcp_request", "type": "tcp request", "z": "tcp_sample_tab", "name": "TCP Request", "server": "localhost", "port": "9000", "out": "sit", "ret": "string", "splitc": " ", "newline": "", "trim": false, "tls": "", "x": 390, "y": 280, "wires": [["tcp_debug_response"]] }, { "id": "tcp_debug_response", "type": "debug", "z": "tcp_sample_tab", "name": "レスポンス", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "\"受信\"", "statusType": "str", "x": 590, "y": 280, "wires": [] }, { "id": "tcp_comment3", "type": "comment", "z": "tcp_sample_tab", "name": "━━━ JSONデータの送受信 ━━━", "info": "", "x": 190, "y": 360, "wires": [] }, { "id": "tcp_in_json_server", "type": "tcp in", "z": "tcp_sample_tab", "name": "TCP Server :9001", "server": "server", "host": "", "port": "9001", "datamode": "stream", "datatype": "utf8", "newline": "\\n", "topic": "", "trim": false, "base64": false, "tls": "", "x": 180, "y": 420, "wires": [["tcp_func_json_process"]] }, { "id": "tcp_func_json_process", "type": "function", "z": "tcp_sample_tab", "name": "JSON処理", "func": "try {\n // 受信データをJSONとしてパース\n var data = JSON.parse(msg.payload);\n \n // 処理結果を作成\n var response = {\n status: \"success\",\n received: data,\n processed: true,\n timestamp: new Date().toISOString()\n };\n \n msg.payload = JSON.stringify(response) + \"\\n\";\n} catch(e) {\n msg.payload = JSON.stringify({status: \"error\", message: \"Invalid JSON\"}) + \"\\n\";\n}\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 390, "y": 420, "wires": [["tcp_out_json_reply", "tcp_debug_json"]] }, { "id": "tcp_out_json_reply", "type": "tcp out", "z": "tcp_sample_tab", "name": "Reply JSON", "host": "", "port": "", "beserver": "reply", "base64": false, "end": false, "tls": "", "x": 590, "y": 420, "wires": [] }, { "id": "tcp_debug_json", "type": "debug", "z": "tcp_sample_tab", "name": "JSON処理結果", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 600, "y": 460, "wires": [] }, { "id": "tcp_inject_json", "type": "inject", "z": "tcp_sample_tab", "name": "JSONリクエスト", "props": [{"p": "payload"}], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "{\"command\":\"getData\",\"id\":123}", "payloadType": "str", "x": 180, "y": 500, "wires": [["tcp_request_json"]] }, { "id": "tcp_request_json", "type": "tcp request", "z": "tcp_sample_tab", "name": "TCP Request JSON", "server": "localhost", "port": "9001", "out": "char", "ret": "string", "splitc": "\\n", "newline": "", "trim": false, "tls": "", "x": 410, "y": 500, "wires": [["tcp_debug_json_response"]] }, { "id": "tcp_debug_json_response", "type": "debug", "z": "tcp_sample_tab", "name": "JSONレスポンス", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "\"JSON受信\"", "statusType": "str", "x": 620, "y": 500, "wires": [] }, { "id": "tcp_comment4", "type": "comment", "z": "tcp_sample_tab", "name": "━━━ 接続状態の監視 ━━━", "info": "", "x": 180, "y": 580, "wires": [] }, { "id": "tcp_in_monitor", "type": "tcp in", "z": "tcp_sample_tab", "name": "TCP Server :9002", "server": "server", "host": "", "port": "9002", "datamode": "stream", "datatype": "utf8", "newline": "", "topic": "", "trim": false, "base64": false, "tls": "", "x": 180, "y": 640, "wires": [["tcp_func_monitor"]] }, { "id": "tcp_func_monitor", "type": "function", "z": "tcp_sample_tab", "name": "接続管理", "func": "// クライアント情報を取得\nvar clientId = msg.ip + \":\" + msg.port;\nvar session = msg._session;\n\n// 接続リストを管理\nvar clients = flow.get('tcpClients') || {};\n\nif (msg.payload === '') {\n // 空データは切断を意味することがある\n delete clients[session];\n node.warn(\"Client disconnected: \" + clientId);\n} else {\n // 接続中のクライアントを記録\n clients[session] = {\n ip: msg.ip,\n port: msg.port,\n lastSeen: new Date().toISOString()\n };\n}\n\nflow.set('tcpClients', clients);\nnode.status({fill: \"blue\", shape: \"dot\", text: Object.keys(clients).length + \" clients\"});\n\nmsg.payload = \"Welcome! You are: \" + clientId + \"\\n\";\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 390, "y": 640, "wires": [["tcp_out_monitor"]] }, { "id": "tcp_out_monitor", "type": "tcp out", "z": "tcp_sample_tab", "name": "Reply", "host": "", "port": "", "beserver": "reply", "base64": false, "end": false, "tls": "", "x": 570, "y": 640, "wires": [] } ]

使用パターン

パターン1: TCPサーバー(エコー)

サンプルフローの「TCPサーバー(エコー)」を参照してください。

TCP In (Listen) 処理 TCP Out (Reply)

ポイント:

パターン2: TCP Request(クライアント)

サンプルフローの「TCP Request(クライアント)」を参照してください。

トリガー TCP Request 結果

ポイント:

パターン3: JSONデータの送受信

サンプルフローの「JSONデータの送受信」を参照してください。

JSONデータ JSON変換 TCP Out

ポイント:

パターン4: 接続状態の監視

サンプルフローの「接続状態の監視」を参照してください。

TCP In 接続管理 Welcome返信

ポイント:

📝 6. 演習問題

演習1: シンプルなTCPエコーサーバー 初級

📋 課題: ポート9100で接続を待ち受け、受信したデータをそのまま返すエコーサーバーを作成してください。

🎯 要求仕様:

✅ 成功の条件:

💡 ヒント

TCP In ノード:

  • Type: Listen on
  • Port: 9100
  • Output: stream of String

TCP Out ノード:

  • Type: Reply to TCP

TCP In の出力を直接 TCP Out に接続すれば、msg._session が保持されてクライアントに返信されます。

✅ 解答例フロー
[ {"id": "ex1_tab", "type": "tab", "label": "演習1"}, {"id": "ex1_tcp_in", "type": "tcp in", "z": "ex1_tab", "name": "TCP Server :9100", "server": "server", "host": "", "port": "9100", "datamode": "stream", "datatype": "utf8", "newline": "", "topic": "", "trim": false, "base64": false, "tls": "", "x": 180, "y": 100, "wires": [["ex1_tcp_out", "ex1_debug"]]}, {"id": "ex1_tcp_out", "type": "tcp out", "z": "ex1_tab", "name": "Reply", "host": "", "port": "", "beserver": "reply", "base64": false, "end": false, "tls": "", "x": 410, "y": 100, "wires": []}, {"id": "ex1_debug", "type": "debug", "z": "ex1_tab", "name": "受信ログ", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "ip", "statusType": "msg", "x": 410, "y": 140, "wires": []} ]

演習2: 時刻付きレスポンス 初級

📋 課題: 受信データに現在時刻を追加して返信するサーバーを作成してください。

🎯 要求仕様:

✅ 成功の条件:

💡 ヒント

Function ノード:

var timestamp = new Date().toISOString(); msg.payload = "[" + timestamp + "] " + msg.payload; return msg;
✅ 解答例フロー
[ {"id": "ex2_tab", "type": "tab", "label": "演習2"}, {"id": "ex2_tcp_in", "type": "tcp in", "z": "ex2_tab", "name": "TCP Server :9101", "server": "server", "host": "", "port": "9101", "datamode": "stream", "datatype": "utf8", "newline": "", "topic": "", "trim": false, "base64": false, "tls": "", "x": 180, "y": 100, "wires": [["ex2_func"]]}, {"id": "ex2_func", "type": "function", "z": "ex2_tab", "name": "時刻追加", "func": "var timestamp = new Date().toISOString();\nmsg.payload = \"[\" + timestamp + \"] \" + msg.payload + \"\\n\";\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 390, "y": 100, "wires": [["ex2_tcp_out"]]}, {"id": "ex2_tcp_out", "type": "tcp out", "z": "ex2_tab", "name": "Reply", "host": "", "port": "", "beserver": "reply", "base64": false, "end": false, "tls": "", "x": 570, "y": 100, "wires": []} ]

演習3: コマンド処理サーバー 中級

📋 課題: 簡単なコマンドを受け付けるTCPサーバーを作成してください。

🎯 要求仕様:

✅ 成功の条件:

💡 ヒント

Function ノード:

var cmd = msg.payload.trim().toUpperCase(); var parts = msg.payload.trim().split(' '); if (cmd === 'TIME') { msg.payload = new Date().toISOString(); } else if (cmd === 'PING') { msg.payload = 'PONG'; } else if (parts[0].toUpperCase() === 'HELLO') { var name = parts[1] || 'Guest'; msg.payload = 'Hello, ' + name + '!'; } else { msg.payload = 'Unknown command'; } msg.payload += '\n'; return msg;
✅ 解答例フロー
[ {"id": "ex3_tab", "type": "tab", "label": "演習3"}, {"id": "ex3_tcp_in", "type": "tcp in", "z": "ex3_tab", "name": "TCP Server :9102", "server": "server", "host": "", "port": "9102", "datamode": "stream", "datatype": "utf8", "newline": "", "topic": "", "trim": false, "base64": false, "tls": "", "x": 180, "y": 100, "wires": [["ex3_func"]]}, {"id": "ex3_func", "type": "function", "z": "ex3_tab", "name": "コマンド処理", "func": "var cmd = msg.payload.trim().toUpperCase();\nvar parts = msg.payload.trim().split(' ');\n\nif (cmd === 'TIME') {\n msg.payload = new Date().toISOString();\n} else if (cmd === 'PING') {\n msg.payload = 'PONG';\n} else if (parts[0].toUpperCase() === 'HELLO') {\n var name = parts[1] || 'Guest';\n msg.payload = 'Hello, ' + name + '!';\n} else {\n msg.payload = 'Unknown command';\n}\nmsg.payload += '\\n';\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 400, "y": 100, "wires": [["ex3_tcp_out"]]}, {"id": "ex3_tcp_out", "type": "tcp out", "z": "ex3_tab", "name": "Reply", "host": "", "port": "", "beserver": "reply", "base64": false, "end": false, "tls": "", "x": 590, "y": 100, "wires": []} ]

演習4: 2台のNode-RED間通信 上級

📋 課題: センサーデータを送信するクライアントと、受信して処理するサーバーを同一フロー内に作成してください。

🎯 要求仕様:

✅ 成功の条件:

💡 ヒント

クライアント側: Inject(5秒repeat)→ Function(JSON生成)→ TCP Request

サーバー側: TCP In → Function(JSON処理)→ TCP Out

改行文字(\n)を区切りとして使うと、JSONデータの境界が分かりやすくなります。

✅ 解答例フロー
[ {"id": "ex4_tab", "type": "tab", "label": "演習4"}, {"id": "ex4_comment1", "type": "comment", "z": "ex4_tab", "name": "【サーバー】", "info": "", "x": 130, "y": 40, "wires": []}, {"id": "ex4_tcp_in", "type": "tcp in", "z": "ex4_tab", "name": "TCP Server :9103", "server": "server", "host": "", "port": "9103", "datamode": "stream", "datatype": "utf8", "newline": "\\n", "topic": "", "trim": false, "base64": false, "tls": "", "x": 180, "y": 100, "wires": [["ex4_func_server"]]}, {"id": "ex4_func_server", "type": "function", "z": "ex4_tab", "name": "JSONパース", "func": "try {\n var data = JSON.parse(msg.payload);\n node.status({fill: \"green\", shape: \"dot\", text: data.sensor + \": \" + data.value});\n msg.payload = JSON.stringify({status: \"received\", data: data}) + \"\\n\";\n} catch(e) {\n msg.payload = JSON.stringify({status: \"error\"}) + \"\\n\";\n}\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 400, "y": 100, "wires": [["ex4_tcp_out", "ex4_debug_server"]]}, {"id": "ex4_tcp_out", "type": "tcp out", "z": "ex4_tab", "name": "Reply", "host": "", "port": "", "beserver": "reply", "base64": false, "end": false, "tls": "", "x": 590, "y": 100, "wires": []}, {"id": "ex4_debug_server", "type": "debug", "z": "ex4_tab", "name": "サーバー処理", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 610, "y": 140, "wires": []}, {"id": "ex4_comment2", "type": "comment", "z": "ex4_tab", "name": "【クライアント】", "info": "", "x": 140, "y": 200, "wires": []}, {"id": "ex4_inject", "type": "inject", "z": "ex4_tab", "name": "5秒ごと", "props": [{"p": "payload"}], "repeat": "5", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 150, "y": 260, "wires": [["ex4_func_client"]]}, {"id": "ex4_func_client", "type": "function", "z": "ex4_tab", "name": "センサーデータ生成", "func": "var temp = (20 + Math.random() * 10).toFixed(1);\nmsg.payload = JSON.stringify({\n sensor: \"temperature\",\n value: parseFloat(temp),\n timestamp: new Date().toISOString()\n}) + \"\\n\";\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 260, "wires": [["ex4_tcp_request"]]}, {"id": "ex4_tcp_request", "type": "tcp request", "z": "ex4_tab", "name": "TCP Request", "server": "localhost", "port": "9103", "out": "char", "ret": "string", "splitc": "\\n", "newline": "", "trim": false, "tls": "", "x": 590, "y": 260, "wires": [["ex4_debug_client"]]}, {"id": "ex4_debug_client", "type": "debug", "z": "ex4_tab", "name": "クライアント受信", "active": true, "tosidebar": true, "console": false, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "\"送受信完了\"", "statusType": "str", "x": 790, "y": 260, "wires": []} ]

✅ 7. まとめ

🎯 重要ポイント:

⚠️ 注意事項:

🔧 8. トラブルシューティング

症状原因解決方法
接続できないポートが使用中/ファイアウォール別のポート使用、FW設定確認
データが途中で切れるストリーム分割の問題区切り文字を設定、適切な Output 設定
返信が届かない_session が消えているReply to TCP を使用、msg._session を保持
文字化けエンコーディング不一致UTF-8 で統一
接続がすぐ切れるタイムアウトKeep-alive の実装を検討

🏭 9. 実務活用例

ケース1: 産業機器との通信

PLC(プログラマブルロジックコントローラ)やセンサー機器とTCPで直接通信

ケース2: Raspberry Pi 間の通信

複数のRaspberry Piでセンサーデータを収集し、中央のサーバーに送信

ケース3: レガシーシステム連携

独自プロトコルを使用する古いシステムとの連携

ケース4: カスタムプロトコルの実装

特定の要件に合わせた独自の通信プロトコルを実装

📚 Node-RED 公式ドキュメント - TCP

🏠