TCP の概要

重要

上級ユーザーには、TcpClientTcpListener の代わりに Socket クラスを強くお勧めします。

伝送制御プロトコル (TCP) を扱うには、最大限の制御とパフォーマンスのために Socket を使用するか、TcpClient および TcpListener ヘルパー クラスを使用するという 2 つの選択肢があります。 TcpClient および TcpListenerSystem.Net.Sockets.Socket クラスの上に構築されており、簡単に使用できるようデータ転送の詳細を処理します。

プロトコル クラスでは、基になる Socket クラスを使って、ネットワーク サービスへのシンプルなアクセスが提供されます。状態情報を維持する必要がなく、プロトコル固有のソケット設定に関する詳細を知る必要もありません。 非同期 Socket メソッドを使用するには、NetworkStream クラスが提供する非同期メソッドを使用できます。 プロトコル クラスでは公開されない Socket クラスの機能にアクセスするには、Socket クラスを使用する必要があります。

TcpClientTcpListener は、NetworkStream クラスを利用するネットワークを表します。 GetStream メソッドを利用してネットワーク ストリームを返し、ストリームの NetworkStream.ReadAsync メソッドと NetworkStream.WriteAsync メソッドを呼び出します。 NetworkStream にはプロトコル クラスの基盤となるソケットがありません。そのため、閉じてもソケットに影響はありません。

TcpClientTcpListener を使用する

TcpClient クラスは、TCP を使ってインターネット リソースにデータを要求します。 TcpClient のメソッドとプロパティは、TCP を使ったデータの要求と受信のための Socket の作成に関する詳細を抽象化します。 リモート デバイスへの接続はストリームとして表されるため、.NET Framework のストリーム処理技術を使用してデータの読み取りと書き込みを行うことができます。

TCP プロトコルは、リモート エンドポイントとの接続を確立してから、その接続を使用してデータ パケットを送受信します。 TCP は、データ パケットをエンドポイントに送信し、受信時に正しい順序で構成されるようにする処理を担当します。

IP エンドポイントを作成する

System.Net.Sockets を使うときは、ネットワーク エンドポイントを IPEndPoint オブジェクトとして表します。 IPEndPoint は、IPAddress とそれに対応するポート番号を使って構築されます。 Socket を使って会話を始める前に、アプリとリモートの通信先との間にデータ パイプを作成します。

TCP/IP はネットワーク アドレスとサービス ポート番号を使用して、サービスを一意に識別しています。 ネットワーク アドレスは、特定のネットワーク通信先を示します。ポート番号は、そのデバイス上の接続先である特定のサービスを示します。 ネットワーク アドレスとサービス ポートの組み合わせはエンドポイントと呼ばれ、.NET では EndPoint クラスによって表されます。 EndPoint の子孫は、サポートされるアドレス ファミリごとに定義されます。IP アドレス ファミリの場合、クラスは IPEndPoint です。

Dns クラスは、TCP/IP インターネット サービスを使うアプリにドメインネーム サービスを提供します。 GetHostEntryAsync メソッドは、DNS サーバーのクエリを実行して、ユーザー フレンドリなドメイン名 ("host.contoso.com" など) を数値のインターネット アドレス (192.168.1.1 など) にマップします。 GetHostEntryAsync は、要求した名前のアドレスとエイリアスの一覧を待機した後に含む Task<IPHostEntry> を返します。 ほとんどの場合、AddressList 配列で返された最初のアドレスを使用できます。 次のコードは、サーバー host.contoso.com の IP アドレスを含む IPAddress を取得します。

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("host.contoso.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];

ヒント

手動でのテストとデバッグには、通常、GetHostEntryAsync メソッドを使って Dns.GetHostName() の値を取得し、localhost 名を IP アドレスに解決できます。

Internet Assigned Numbers Authority (IANA) により、一般的なサービス用のポート番号が定義されています。 詳細については、IANA: 「Service Name and Transport Protocol Port Number Registry (サービス名とトランスポート プロトコル ポート番号の登録)」を参照してください。 他のサービスが、1,024 から 65,535 の範囲内でポート番号を登録している可能性があります。 次のコードは、host.contoso.com の IP アドレスとポート番号を組み合わせて、接続のためのリモート エンドポイントを作成します。

IPEndPoint ipEndPoint = new(ipAddress, 11_000);

リモート デバイスのアドレスを決定し、接続に使用するポートを選択すると、アプリはそのリモート デバイスとの接続を確立できます。

認証要求の処理に使用する TcpClient

TcpClient クラスは、Socket クラスより高い抽象化レベルで TCP サービスを提供します。 TcpClient は、リモート ホストへのクライアント接続を作成するために使われます。 IPEndPoint を取得する方法がわかったら、目的のポート番号とペアにする IPAddress があるとしましょう。 TCP ポート 13 でタイム サーバーに接続するように TcpClient を設定する例を次に示します。

var ipEndPoint = new IPEndPoint(ipAddress, 13);

using TcpClient client = new();
await client.ConnectAsync(ipEndPoint);
await using NetworkStream stream = client.GetStream();

var buffer = new byte[1_024];
int received = await stream.ReadAsync(buffer);

var message = Encoding.UTF8.GetString(buffer, 0, received);
Console.WriteLine($"Message received: \"{message}\"");
// Sample output:
//     Message received: "📅 8/22/2022 9:07:17 AM 🕛"

前述の C# コードでは、次のことが行われます。

  • 既知の IPAddress とポートから IPEndPoint を作成します。
  • 新しい TcpClient オブジェクトのインスタンスを作成します。
  • TcpClient.ConnectAsync を使って、ポート 13 のリモート TCP タイム サーバーに client を接続します。
  • NetworkStream を使って、リモート ホストからデータを読み取ります。
  • 1_024 バイトの読み取りバッファーを宣言します。
  • stream から読み取りバッファーにデータを読み取ります。
  • 結果を文字列としてコンソールに書き込みます。

クライアントはメッセージが小さいことがわかっているため、1 回の操作でメッセージ全体を読み取りバッファーに読み取ることができます。 さらに大きいメッセージ、または長さが不確定のメッセージでは、クライアントはバッファーをより適切に使って、while ループで読み取る必要があります。

重要

メッセージを送受信するときは、サーバーとクライアントの両方が事前に Encoding を知っている必要があります。 たとえば、サーバーが ASCIIEncoding を使って通信しているのに、クライアントが UTF8Encoding を使おうとすると、メッセージの形式が正しくなくなります。

認証要求の処理に使用する TcpListener

TcpListener 型を使って受信した要求の TCP ポートを監視し、クライアントへの接続を管理する Socket または TcpClient を作成します。 Start メソッドでリッスンが有効になり、Stop メソッドでポートのリッスンが無効になります。 AcceptTcpClientAsync メソッドは受信接続要求を受け取り、TcpClient を作成して要求を処理します。AcceptSocketAsync メソッドは受信接続要求を受け取り、Socket を作成して要求を処理します。

TcpListener を使用して TCP ポート 13 を監視するネットワーク タイム サーバーを作成する例を次に示します。 受信接続要求が受け取られると、タイム サーバーはホスト サーバーの現在の日時で応答します。

var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
TcpListener listener = new(ipEndPoint);

try
{    
    listener.Start();

    using TcpClient handler = await listener.AcceptTcpClientAsync();
    await using NetworkStream stream = handler.GetStream();

    var message = $"📅 {DateTime.Now} 🕛";
    var dateTimeBytes = Encoding.UTF8.GetBytes(message);
    await stream.WriteAsync(dateTimeBytes);

    Console.WriteLine($"Sent message: \"{message}\"");
    // Sample output:
    //     Sent message: "📅 8/22/2022 9:07:17 AM 🕛"
}
finally
{
    listener.Stop();
}

前述の C# コードでは、次のことが行われます。

  • IPAddress.Any とポートを使って、IPEndPoint を作成します。
  • 新しい TcpListener オブジェクトのインスタンスを作成します。
  • Start メソッドを呼び出して、ポートのリッスンを始めます。
  • AcceptTcpClientAsync メソッドからの TcpClient を使って、受信した接続要求を受け入れます。
  • 現在の日付と時刻を、文字列メッセージとしてエンコードします。
  • NetworkStream を使って、接続されているクライアントにデータを書き込みます。
  • 送信されたメッセージをコンソールに書き込みます。
  • 最後に、Stop メソッドを呼び出して、ポートでのリッスンを停止します。

Socket クラスを使用した有限の TCP 制御

TcpClientTcpListener は両方とも内部的には Socket クラスに依存しています。つまり、これらのクラスを使用してできることは、ソケットを直接使用して実現できます。 このセクションでは、いくつかの TcpClient および TcpListener のユース ケースを、機能的には同等な Socket の対応するユース ケースと合わせて説明します。

クライアント ソケットを作成する

TcpClient の既定のコンストラクターは、Socket(SocketType, ProtocolType) コンストラクターを介して "デュアルスタック ソケット" の作成を試みます。 このコンストラクターは、IPv6 がサポートされている場合は、デュアルスタック ソケットを作成し、それ以外の場合は、IPv4 にフォール バックします。

次の TCP クライアント コードについて考えてみましょう。

using var client = new TcpClient();

前述の TCP クライアント コードは機能的には次のソケット コードと同等です。

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

TcpClient(AddressFamily) コンストラクター

このコンストラクターは 3 つの AddressFamily 値のみを受け取り、それ以外の場合は ArgumentException をスローします。 有効な値は次のとおりです。

次の TCP クライアント コードについて考えてみましょう。

using var client = new TcpClient(AddressFamily.InterNetwork);

前述の TCP クライアント コードは機能的には次のソケット コードと同等です。

using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

TcpClient(IPEndPoint) コンストラクター

ソケットを作成すると、このコンストラクターは指定されたローカルIPEndPoint にもバインドします。 IPEndPoint.AddressFamily プロパティは、ソケットのアドレス ファミリを決定するために使用されます。

次の TCP クライアント コードについて考えてみましょう。

var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var client = new TcpClient(endPoint);

前述の TCP クライアント コードは機能的には次のソケット コードと同等です。

// Example IPEndPoint object
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);

TcpClient(String, Int32) コンストラクター

このコンストラクターは、既定のコンストラクターと同様のデュアル スタックを作成し、それを hostnameport のペアで定義されているリモート DNS エンドポイントに接続することを試みます。

次の TCP クライアント コードについて考えてみましょう。

using var client = new TcpClient("www.example.com", 80);

前述の TCP クライアント コードは機能的には次のソケット コードと同等です。

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

サーバーに接続する

TcpClient における ConnectConnectAsyncBeginConnectEndConnect のオーバーロードのすべては、対応する Socket メソッドと機能的に同等です。

次の TCP クライアント コードについて考えてみましょう。

using var client = new TcpClient();
client.Connect("www.example.com", 80);

上記の TcpClient のコードは、次のソケット コードと同等です。

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);

サーバー ソケットを作成する

生の Socket の対応物と機能的な同等性を持つ TcpClient インスタンスとちょうど同じように、このセクションは TcpListener コンストラクターを対応するソケット コードにマップします。 考慮すべき最初のコンストラクターは TcpListener(IPAddress localaddr, int port) です。

var listener = new TcpListener(IPAddress.Loopback, 5000);

前述の TCP リスナー コードは機能的には次のソケット コードと同等です。

var ep = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

サーバーのリッスンを開始する

Start() メソッドは、SocketBind および Listen() 機能を組み合わせるラッパーです。

次の TCP リスナー コードについて考えてみましょう。

var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start(10);

前述の TCP リスナー コードは機能的には次のソケット コードと同等です。

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);
try
{
    socket.Listen(10);
}
catch (SocketException)
{
    socket.Dispose();
}

サーバー接続を受け入れる

内部では、受信 TCP 接続は、受け入れられると常に新しいソケットを作成しています。 TcpListener は、(AcceptSocket() または AcceptSocketAsync() を介して) Socket インスタンスを直接受け入れるか、(AcceptTcpClient() および AcceptTcpClientAsync() を介して) TcpClient を受け入れることができます。

次の TcpListener コードについて考えてみましょう。

var listener = new TcpListener(IPAddress.Loopback, 5000);
using var acceptedSocket = await listener.AcceptSocketAsync();

// Synchronous alternative.
// var acceptedSocket = listener.AcceptSocket();

前述の TCP リスナー コードは機能的には次のソケット コードと同等です。

var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
using var acceptedSocket = await socket.AcceptAsync();

// Synchronous alternative
// var acceptedSocket = socket.Accept();

NetworkStream を作成してデータの送受信を行う

TcpClient では、GetStream() メソッドを使用して NetworkStream をインスタンス化して、データを送受信できるようにする必要があります。 Socket では、NetworkStream の作成を手動で行う必要があります。

次の TcpClient コードについて考えてみましょう。

using var client = new TcpClient();
using NetworkStream stream = client.GetStream();

これは、次のソケット コードと同等です。

using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

// Be aware that transferring the ownership means that closing/disposing the stream will also close the underlying socket.
using var stream = new NetworkStream(socket, ownsSocket: true);

ヒント

コードで Stream インスタンスを操作する必要がない場合は、NetworkStream を作成する代わりに、Socket の Send/Receive メソッド (SendSendAsyncReceive および ReceiveAsync) を直接使用できます。

関連項目