TCP の概要
重要
上級ユーザーには、TcpClient
と TcpListener
の代わりに Socket クラスを強くお勧めします。
伝送制御プロトコル (TCP) を扱うには、最大限の制御とパフォーマンスのために Socket を使用するか、TcpClient および TcpListener ヘルパー クラスを使用するという 2 つの選択肢があります。 TcpClient および TcpListener は System.Net.Sockets.Socket クラスの上に構築されており、簡単に使用できるようデータ転送の詳細を処理します。
プロトコル クラスでは、基になる Socket
クラスを使って、ネットワーク サービスへのシンプルなアクセスが提供されます。状態情報を維持する必要がなく、プロトコル固有のソケット設定に関する詳細を知る必要もありません。 非同期 Socket
メソッドを使用するには、NetworkStream クラスが提供する非同期メソッドを使用できます。 プロトコル クラスでは公開されない Socket
クラスの機能にアクセスするには、Socket
クラスを使用する必要があります。
TcpClient
と TcpListener
は、NetworkStream
クラスを利用するネットワークを表します。 GetStream メソッドを利用してネットワーク ストリームを返し、ストリームの NetworkStream.ReadAsync メソッドと NetworkStream.WriteAsync メソッドを呼び出します。 NetworkStream
にはプロトコル クラスの基盤となるソケットがありません。そのため、閉じてもソケットに影響はありません。
TcpClient
と TcpListener
を使用する
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];
ヒント
手動テストとデバッグには、通常は、Dns.GetHostName() 値の結果のホスト名と共に GetHostEntryAsync メソッドを使って、localhost 名を IP アドレスに解決できます。 次のコード スニペットを考えてみます。
var hostName = Dns.GetHostName();
IPHostEntry localhost = await Dns.GetHostEntryAsync(hostName);
// This is the IP address of the local machine
IPAddress localIpAddress = localhost.AddressList[0];
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 制御
TcpClient
と TcpListener
は両方とも内部的には 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 をスローします。 有効な値は次のとおりです。
- AddressFamily.InterNetwork: IPv4 ソケット用。
- AddressFamily.InterNetworkV6: IPv6 ソケット用。
- AddressFamily.Unknown: これは既定のコンストラクターと同様に、デュアルスタック ソケットの作成を試みます。
次の 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) コンストラクター
このコンストラクターは、既定のコンストラクターと同様のデュアル スタックを作成し、それを hostname
と port
のペアで定義されているリモート 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
における Connect
、ConnectAsync
、BeginConnect
、EndConnect
のオーバーロードのすべては、対応する 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() メソッドは、Socket
の Bind および 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 メソッド (Send、SendAsync、Receive および ReceiveAsync) を直接使用できます。
関連項目
.NET