QA@IT
«回答へ戻る

回答を投稿

ストリーム「型」ソケット、INET STREAMのストリームですね、ちょっとストリームデータと勘違いしていました。
いずれにせよ、以下の発言が気になります。

UDPを使用して送受信してしたメッセージの塊をTCPを使用して送受信した場合、その塊の区切りは判断できないと思っています。

通常、TCP(STREAM)の方が信頼がおける通信であり、UDP(DGRAM)の方が整合性を失いやすい通信です。なぜUDPだと可能なものがTCPだと無理だと思われたのでしょうか(普通は逆ですね)

C じゃないですが、python 3.4.2 で簡単なUDPとTCPの実装を書くと以下の様になります。
C のソケットライブラリを使っても流れは大体同じになるかと思います。
マジックコメントつけてないので試してみるときは全角文字をソースに含めないようにしてください。Win 8.1でためしましたが大したことしてないんでどのpython 3系ならどの環境でも動くかと思います。

※ 説明のためにソース書いたんですが、書きながら質問者さんの疑問がなにか考えながら直していたらソースはあまり関係なくなってしまいました。
まったく無関係にはなってないですが、ソース後のコメントだけ読んでもらって結構です。

TCP Server / Client

serv.py

import socket
import threading
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 41234)) ; s.listen(2)  # allow 2 connections

def listen_thread(s_sock, num):
    print("listen_thread No.{0} start.".format(num))
    (c_sock, addr) = s_sock.accept() 
    print("accept : No.{0} from {1}".format(num, addr))
    try:
        while True:
            data = c_sock.recv(10)
            if not data:
                continue
            print("recv: thread No.{0}: {1}".format(num, data))
            if data.decode('ascii') == b'end'.decode('ascii'):
                print("recv: receive terminate request. thread No.{0}".format(num))
                return
    finally:
        c_sock.close()

th1 = threading.Thread(target=listen_thread, name="th1", args=(s, 1))
th2 = threading.Thread(target=listen_thread, name="th2", args=(s, 2))
th1.start() ; th2.start()

try:
    while th1.isAlive() and th2.isAlive():
        time.sleep(1)
finally:
    s.close()

cli.py

import socket
import time
server_address = ('localhost', 41234)
sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock1.connect(server_address)
sock2.connect(server_address)
print("sock1 sockname = {0}".format(sock1.getsockname()))
print("sock2 sockname = {0}".format(sock2.getsockname()))

st = ed = time.clock()
try:
    while int(ed - st) < 3:
        sock1.sendall(b'a'*3) ; sock2.sendall(b'bbb')
        ed = time.clock() ; time.sleep(1)
finally:
    sock1.sendall(b'end')
    sock2.sendall(b'end')
sock1.close()
sock2.close()

実行結果(サーバー出力のみ)

listen_thread No.1 start.
listen_thread No.2 start.
accept : No.1 from ('127.0.0.1', 50144)
accept : No.2 from ('127.0.0.1', 50145)
recv: thread No.1: b'aaa'
recv: thread No.2: b'bbb'
~省略~
recv: thread No.1: b'end'
recv: receive terminate request. thread No.1
recv: thread No.2: b'end'
recv: receive terminate request. thread No.2

サーバーは最大 2接続を許可するようにしています(そのためスレッドも利用しています)。
クライアントはソケットを 2個作ってサーバーに接続しにいきます。

このときサーバーでacceptした段階で各クライアント接続用のソケット(c_sock)が作成されます。
2つ接続されるので、2つのc_sockが作成されそれぞれのクライアントの通信は区別されて通信できます。

UDP Server / Client

一方UDPではそういうことにはなりません。

udp_serv.py

import socket

udp_s_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_s_sock.bind(('', 41235))

while True:
    (data, addr) = udp_s_sock.recvfrom(10)
    print("recv: {0} from {1}".format(data, addr))
    if data and data.decode('ascii') == b'end'.decode('ascii'):
        print("recv: udp socket receive terminate request from {0}".format(addr))
        break

udp_s_sock.close()

udp_cli.py

TCPのクライアントの先頭 5行を以下に書き換えます(変更があるのは3,4,5行目)

import socket
import time
server_address = ('localhost', 41235)
sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

6行目以降は同じ。

実行結果(サーバー出力)

recv: b'aaa' from ('127.0.0.1', 61491)
recv: b'bbb' from ('127.0.0.1', 61492)
~省略~
recv: b'end' from ('127.0.0.1', 61491)
recv: udp socket receive terminate request from ('127.0.0.1', 61491)

※ こちらは先にendを受け取った段階でUDPソケットをクローズするので、2つ目のendは受信されない(さらにUDPだから受信確認も接続確認もないので2つ目のendがロストしていてもだれもエラーを出さない)。

UDPではサーバーは単純に待ち受けて飛んできたデータを順次処理していくだけです。
さらに、TCPの場合はサーバーが起動していないとクライアントはエラーとなりますが、UDPの場合はサーバーが起動していなくてもデータを送信します。受信されたかどうかの確認もされません。

と、こういった基本部分はご存知かもしれませんが、

UDPを使用して送受信してしたメッセージの塊をTCPを使用して送受信した場合、その塊の区切りは判断できないと思っています。

という点をみるところ、データグラム(つまり塊)を受け取るUDPに対して
TCPは小刻みに受け取るじゃないかという話なんでしょうか。

上記のプログラムの各クライアントにおいて
sock1.sendall(b'a'*3) という部分で 3byteの送信を行っています。
ではこれを
sock1.sendall(b'a'*65536)
とした場合何が起こると思いますか?

TCPは正常に送信できます。
UDPは…エラーとなります(正確には環境にもよりますが)。
うちのWindows環境では

OSError: [WinError 10040] データグラム ソケットで送信されたメッセージが、内部のメッセージのバッファーまたはほかのネットワークの制限を超えています。または、データグラムの受信に使われるバッファーがデータグラムより小さく設定されています。

というエラーになります。

UDPはデータグラムを送信します。イーサネットフレームのサイズなどIPフラグメンテーションが発生すれば分割しなければなりませんがそれはIPの仕事です。IPがデータグラムまでは戻してくれますがUDPから2回送信したデータの届いた順番が正しいのかはUDPもIPも知りません。

分けずに塊で送るから安心という考えなのだと思いますが、データグラムのサイズには 65515 だったか、16bit長からヘッダサイズを除いたサイズという限界があります。さらに環境によってはフラグメンテーションが許されずそもそも大きなデータグラムが送れないことがあり、その場合はUDPは分割できないのでエラーになります。

TCPの場合はストリーム通信で受取先で組み立てられるのでその制限はありません。
数GBでも転送できます。それを小分けに送ってもとの数GBまで組み立てるまでがTCPの仕事です。
塊を送れないのではなくて、塊をばらして送って塊に戻すところまでやってくれるのです。

ですから「UDPだと塊で送れて安心、TCPだと塊は送れない小分けでしか送れないからどうしよう」という発端であればそれがそもそも勘違いではないかと思うのですがどうでしょう。

ストリーム「型」ソケット、INET STREAMのストリームですね、ちょっとストリームデータと勘違いしていました。
いずれにせよ、以下の発言が気になります。

> UDPを使用して送受信してしたメッセージの塊をTCPを使用して送受信した場合、その塊の区切りは判断できないと思っています。

通常、TCP(STREAM)の方が信頼がおける通信であり、UDP(DGRAM)の方が整合性を失いやすい通信です。なぜUDPだと可能なものがTCPだと無理だと思われたのでしょうか(普通は逆ですね)

C じゃないですが、python 3.4.2 で簡単なUDPとTCPの実装を書くと以下の様になります。
C のソケットライブラリを使っても流れは大体同じになるかと思います。
マジックコメントつけてないので試してみるときは全角文字をソースに含めないようにしてください。Win 8.1でためしましたが大したことしてないんでどのpython 3系ならどの環境でも動くかと思います。

※ 説明のためにソース書いたんですが、書きながら質問者さんの疑問がなにか考えながら直していたらソースはあまり関係なくなってしまいました。
まったく無関係にはなってないですが、ソース後のコメントだけ読んでもらって結構です。


### TCP Server / Client

`serv.py`

```py
import socket
import threading
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 41234)) ; s.listen(2)  # allow 2 connections

def listen_thread(s_sock, num):
    print("listen_thread No.{0} start.".format(num))
    (c_sock, addr) = s_sock.accept() 
    print("accept : No.{0} from {1}".format(num, addr))
    try:
        while True:
            data = c_sock.recv(10)
            if not data:
                continue
            print("recv: thread No.{0}: {1}".format(num, data))
            if data.decode('ascii') == b'end'.decode('ascii'):
                print("recv: receive terminate request. thread No.{0}".format(num))
                return
    finally:
        c_sock.close()

th1 = threading.Thread(target=listen_thread, name="th1", args=(s, 1))
th2 = threading.Thread(target=listen_thread, name="th2", args=(s, 2))
th1.start() ; th2.start()

try:
    while th1.isAlive() and th2.isAlive():
        time.sleep(1)
finally:
    s.close()
```

`cli.py`

```py
import socket
import time
server_address = ('localhost', 41234)
sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock1.connect(server_address)
sock2.connect(server_address)
print("sock1 sockname = {0}".format(sock1.getsockname()))
print("sock2 sockname = {0}".format(sock2.getsockname()))

st = ed = time.clock()
try:
    while int(ed - st) < 3:
        sock1.sendall(b'a'*3) ; sock2.sendall(b'bbb')
        ed = time.clock() ; time.sleep(1)
finally:
    sock1.sendall(b'end')
    sock2.sendall(b'end')
sock1.close()
sock2.close()
```

実行結果(サーバー出力のみ)
```
listen_thread No.1 start.
listen_thread No.2 start.
accept : No.1 from ('127.0.0.1', 50144)
accept : No.2 from ('127.0.0.1', 50145)
recv: thread No.1: b'aaa'
recv: thread No.2: b'bbb'
~省略~
recv: thread No.1: b'end'
recv: receive terminate request. thread No.1
recv: thread No.2: b'end'
recv: receive terminate request. thread No.2
```

サーバーは最大 2接続を許可するようにしています(そのためスレッドも利用しています)。
クライアントはソケットを 2個作ってサーバーに接続しにいきます。

このときサーバーでacceptした段階で各クライアント接続用のソケット(c_sock)が作成されます。
2つ接続されるので、2つのc_sockが作成されそれぞれのクライアントの通信は区別されて通信できます。


### UDP Server / Client

一方UDPではそういうことにはなりません。

`udp_serv.py`

```py
import socket

udp_s_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_s_sock.bind(('', 41235))

while True:
    (data, addr) = udp_s_sock.recvfrom(10)
    print("recv: {0} from {1}".format(data, addr))
    if data and data.decode('ascii') == b'end'.decode('ascii'):
        print("recv: udp socket receive terminate request from {0}".format(addr))
        break

udp_s_sock.close()
```

`udp_cli.py`

TCPのクライアントの先頭 5行を以下に書き換えます(変更があるのは3,4,5行目)

```py
import socket
import time
server_address = ('localhost', 41235)
sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

```
6行目以降は同じ。

実行結果(サーバー出力)
```
recv: b'aaa' from ('127.0.0.1', 61491)
recv: b'bbb' from ('127.0.0.1', 61492)
~省略~
recv: b'end' from ('127.0.0.1', 61491)
recv: udp socket receive terminate request from ('127.0.0.1', 61491)
```

※ こちらは先にendを受け取った段階でUDPソケットをクローズするので、2つ目のendは受信されない(さらにUDPだから受信確認も接続確認もないので2つ目のendがロストしていてもだれもエラーを出さない)。


UDPではサーバーは単純に待ち受けて飛んできたデータを順次処理していくだけです。
さらに、TCPの場合はサーバーが起動していないとクライアントはエラーとなりますが、UDPの場合はサーバーが起動していなくてもデータを送信します。受信されたかどうかの確認もされません。

と、こういった基本部分はご存知かもしれませんが、

> UDPを使用して送受信してしたメッセージの塊をTCPを使用して送受信した場合、その塊の区切りは判断できないと思っています。

という点をみるところ、データグラム(つまり塊)を受け取るUDPに対して
TCPは小刻みに受け取るじゃないかという話なんでしょうか。

上記のプログラムの各クライアントにおいて
`sock1.sendall(b'a'*3) ` という部分で 3byteの送信を行っています。
ではこれを
`sock1.sendall(b'a'*65536) ` 
とした場合何が起こると思いますか?

TCPは正常に送信できます。
UDPは…エラーとなります(正確には環境にもよりますが)。
うちのWindows環境では
> OSError: [WinError 10040] データグラム ソケットで送信されたメッセージが、内部のメッセージのバッファーまたはほかのネットワークの制限を超えています。または、データグラムの受信に使われるバッファーがデータグラムより小さく設定されています。

というエラーになります。

UDPはデータグラムを送信します。イーサネットフレームのサイズなどIPフラグメンテーションが発生すれば分割しなければなりませんがそれはIPの仕事です。IPがデータグラムまでは戻してくれますがUDPから2回送信したデータの届いた順番が正しいのかはUDPもIPも知りません。

分けずに塊で送るから安心という考えなのだと思いますが、データグラムのサイズには 65515 だったか、16bit長からヘッダサイズを除いたサイズという限界があります。さらに環境によってはフラグメンテーションが許されずそもそも大きなデータグラムが送れないことがあり、その場合はUDPは分割できないのでエラーになります。

TCPの場合はストリーム通信で受取先で組み立てられるのでその制限はありません。
数GBでも転送できます。それを小分けに送ってもとの数GBまで組み立てるまでがTCPの仕事です。
塊を送れないのではなくて、塊をばらして送って塊に戻すところまでやってくれるのです。

ですから「UDPだと塊で送れて安心、TCPだと塊は送れない小分けでしか送れないからどうしよう」という発端であればそれがそもそも勘違いではないかと思うのですがどうでしょう。