ここでは、TCP(Transmission Control Protocol)サーバーとクライアントを作成する方法の基本的な事柄について説明します。非同期処理などのさらに高度な事柄については、「TCPを利用した複数クライアント接続可能なチャットアプリケーションを作る」で説明します。
TCPサーバーはTcpListenerクラスを、TCPクライアントはTcpClientクラスを使用して作成すると、比較的簡単(といっても難しいですが)にできます。
サーバーは指定されたポートを監視し、クライアントの接続要求を待ちます。クライアントからの接続要求があり、これをサーバーが受け付けると、接続が完了し、切断するまでの間、データの送受信が可能となります。
早速、TCPサーバーを作ってみましょう。大ざっぱな手順は次のようになります。
以下に示すコンソールアプリケーションは、ローカルIPアドレスのポート2001で動作し、まず接続してきたクライアントからデータ(文字列)を受信し、受信した文字列の長さを返信してから、クライアントを切断しています。その後サーバーはすぐに終了します。もし終了させずにListenを継続する場合は、リスナを閉じずに、再びAcceptTcpClientメソッドを呼び出してください(ループさせることになるでしょう)。
このサンプルでは、送信するデータの最後にはラインフィード文字を付けるという決まりにしています。このような決まりによって、データを受信する側がいつまで受信したらいいのか判断できるようになります。(それ以外の方法としては、はじめにデータのサイズを送信して知らせておくといった方法も考えられます。)
ここで紹介している方法は、Listenやデータの送受信を同期的に行なっているため、複数のクライアントと同時に接続してやり取りを行うことはできません。
Public Class Server Public Shared Sub Main() 'ListenするIPアドレス Dim ipString As String = "127.0.0.1" Dim ipAdd As System.Net.IPAddress = System.Net.IPAddress.Parse(ipString) 'ホスト名からIPアドレスを取得する時は、次のようにする 'Dim host As String = "localhost" 'Dim ipAdd As System.Net.IPAddress = _ ' System.Net.Dns.GetHostEntry(host).AddressList(0) '.NET Framework 1.1以前では、以下のようにする 'Dim ipAdd As System.Net.IPAddress = _ ' System.Net.Dns.Resolve(host).AddressList(0) 'Listenするポート番号 Dim port As Integer = 2001 'TcpListenerオブジェクトを作成する Dim listener As New System.Net.Sockets.TcpListener(ipAdd, port) 'Listenを開始する listener.Start() Console.WriteLine("Listenを開始しました({0}:{1})。", _ DirectCast(listener.LocalEndpoint, System.Net.IPEndPoint).Address, _ DirectCast(listener.LocalEndpoint, System.Net.IPEndPoint).Port) '接続要求があったら受け入れる Dim client As System.Net.Sockets.TcpClient = listener.AcceptTcpClient() Console.WriteLine("クライアント({0}:{1})と接続しました。", _ DirectCast(client.Client.RemoteEndPoint, System.Net.IPEndPoint).Address, _ DirectCast(client.Client.RemoteEndPoint, System.Net.IPEndPoint).Port) 'NetworkStreamを取得 Dim ns As System.Net.Sockets.NetworkStream = client.GetStream() '読み取り、書き込みのタイムアウトを10秒にする 'デフォルトはInfiniteで、タイムアウトしない '(.NET Framework 2.0以上が必要) ns.ReadTimeout = 10000 ns.WriteTimeout = 10000 'クライアントから送られたデータを受信する Dim enc As System.Text.Encoding = System.Text.Encoding.UTF8 Dim disconnected As Boolean = False Dim ms As New System.IO.MemoryStream() Dim resBytes As Byte() = New Byte(255) {} Dim resSize As Integer = 0 Do 'データの一部を受信する resSize = ns.Read(resBytes, 0, resBytes.Length) 'Readが0を返した時はクライアントが切断したと判断 If resSize = 0 Then disconnected = True Console.WriteLine("クライアントが切断しました。") Exit Do End If '受信したデータを蓄積する ms.Write(resBytes, 0, resSize) 'まだ読み取れるデータがあるか、データの最後が\nでない時は、 ' 受信を続ける Loop While ns.DataAvailable OrElse _ resBytes(resSize - 1) <> AscW(ControlChars.Lf) '受信したデータを文字列に変換 Dim resMsg As String = enc.GetString(ms.GetBuffer(), 0, CInt(ms.Length)) ms.Close() '末尾の\nを削除 resMsg = resMsg.TrimEnd(ControlChars.Lf) Console.WriteLine(resMsg) If Not disconnected Then 'クライアントにデータを送信する 'クライアントに送信する文字列を作成 Dim sendMsg As String = resMsg.Length.ToString() '文字列をByte型配列に変換 Dim sendBytes As Byte() = enc.GetBytes(sendMsg & ControlChars.Lf) 'データを送信する ns.Write(sendBytes, 0, sendBytes.Length) Console.WriteLine(sendMsg) End If '閉じる ns.Close() client.Close() Console.WriteLine("クライアントとの接続を閉じました。") 'リスナを閉じる listener.Stop() Console.WriteLine("Listenerを閉じました。") Console.ReadLine() End Sub End Class
using System; public class Server { public static void Main() { //ListenするIPアドレス string ipString = "127.0.0.1"; System.Net.IPAddress ipAdd = System.Net.IPAddress.Parse(ipString); //ホスト名からIPアドレスを取得する時は、次のようにする //string host = "localhost"; //System.Net.IPAddress ipAdd = // System.Net.Dns.GetHostEntry(host).AddressList[0]; //.NET Framework 1.1以前では、以下のようにする //System.Net.IPAddress ipAdd = // System.Net.Dns.Resolve(host).AddressList[0]; //Listenするポート番号 int port = 2001; //TcpListenerオブジェクトを作成する System.Net.Sockets.TcpListener listener = new System.Net.Sockets.TcpListener(ipAdd, port); //Listenを開始する listener.Start(); Console.WriteLine("Listenを開始しました({0}:{1})。", ((System.Net.IPEndPoint)listener.LocalEndpoint).Address, ((System.Net.IPEndPoint)listener.LocalEndpoint).Port); //接続要求があったら受け入れる System.Net.Sockets.TcpClient client = listener.AcceptTcpClient(); Console.WriteLine("クライアント({0}:{1})と接続しました。", ((System.Net.IPEndPoint)client.Client.RemoteEndPoint).Address, ((System.Net.IPEndPoint)client.Client.RemoteEndPoint).Port); //NetworkStreamを取得 System.Net.Sockets.NetworkStream ns = client.GetStream(); //読み取り、書き込みのタイムアウトを10秒にする //デフォルトはInfiniteで、タイムアウトしない //(.NET Framework 2.0以上が必要) ns.ReadTimeout = 10000; ns.WriteTimeout = 10000; //クライアントから送られたデータを受信する System.Text.Encoding enc = System.Text.Encoding.UTF8; bool disconnected = false; System.IO.MemoryStream ms = new System.IO.MemoryStream(); byte[] resBytes = new byte[256]; int resSize = 0; do { //データの一部を受信する resSize = ns.Read(resBytes, 0, resBytes.Length); //Readが0を返した時はクライアントが切断したと判断 if (resSize == 0) { disconnected = true; Console.WriteLine("クライアントが切断しました。"); break; } //受信したデータを蓄積する ms.Write(resBytes, 0, resSize); //まだ読み取れるデータがあるか、データの最後が\nでない時は、 // 受信を続ける } while (ns.DataAvailable || resBytes[resSize - 1] != '\n'); //受信したデータを文字列に変換 string resMsg = enc.GetString(ms.GetBuffer(), 0, (int)ms.Length); ms.Close(); //末尾の\nを削除 resMsg = resMsg.TrimEnd('\n'); Console.WriteLine(resMsg); if (!disconnected) { //クライアントにデータを送信する //クライアントに送信する文字列を作成 string sendMsg = resMsg.Length.ToString(); //文字列をByte型配列に変換 byte[] sendBytes = enc.GetBytes(sendMsg + '\n'); //データを送信する ns.Write(sendBytes, 0, sendBytes.Length); Console.WriteLine(sendMsg); } //閉じる ns.Close(); client.Close(); Console.WriteLine("クライアントとの接続を閉じました。"); //リスナを閉じる listener.Stop(); Console.WriteLine("Listenerを閉じました。"); Console.ReadLine(); } }
ListenするIPアドレスとしてIPAddress.Anyを指定する(つまり上記の例の場合は、ipAddをIPAddress.Anyにする)と、利用可能な全てのIPv4アドレスをListenします。また、IPAddress.IPv6Anyを指定すると、利用可能な全てのIPv6アドレスをListenします(.NET Framework 1.1以降)。
1つのTcpListenerオブジェクトで全てのIPv4とIPv6のアドレスをListenするには、IPAddress.IPv6Anyを使用し、さらにSocket.SetSocketOptionメソッドでIPv6Onlyを0にします。
以下にその例を示します。この例は、Listenを開始し、クライアントの接続を受け入れ、接続に使用しているIPアドレスとポート番号を表示する部分だけです。それ以外は上記のコードと同じで大丈夫です。
'IPv4とIPv6の全てのIPアドレスをListenする Dim listener As New System.Net.Sockets.TcpListener( _ System.Net.IPAddress.IPv6Any, 2001) 'IPv6Onlyを0にする listener.Server.SetSocketOption( _ System.Net.Sockets.SocketOptionLevel.IPv6, _ System.Net.Sockets.SocketOptionName.IPv6Only, _ 0) 'Listenを開始する listener.Start() '接続要求があったら受け入れる Dim client As System.Net.Sockets.TcpClient = listener.AcceptTcpClient() Console.WriteLine("IPアドレス:{0} ポート番号:{1})。", _ DirectCast(client.Client.LocalEndPoint, System.Net.IPEndPoint).Address, _ DirectCast(client.Client.LocalEndPoint, System.Net.IPEndPoint).Port)
//IPv4とIPv6の全てのIPアドレスをListenする System.Net.Sockets.TcpListener listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.IPv6Any, 2001); //IPv6Onlyを0にする listener.Server.SetSocketOption( System.Net.Sockets.SocketOptionLevel.IPv6, System.Net.Sockets.SocketOptionName.IPv6Only, 0); //Listenを開始する listener.Start(); //接続要求があったら受け入れる System.Net.Sockets.TcpClient client = listener.AcceptTcpClient(); Console.WriteLine("IPアドレス:{0} ポート番号:{1})。", ((System.Net.IPEndPoint)client.Client.LocalEndPoint).Address, ((System.Net.IPEndPoint)client.Client.LocalEndPoint).Port);
次はTCPクライアントを作成する方法です。手順は、次の通りです。
以下に示すコンソールアプリケーションは、ローカルIPアドレスのポート2001に接続した後、入力された文字列をサーバーに送信し(キーボードから文字列を入力し、Enterキーを押す)、サーバーから送られてくるデータを受信しています。
Public Class Client Public Shared Sub Main() 'サーバーに送信するデータを入力してもらう Console.WriteLine("文字列を入力し、Enterキーを押してください。") Dim sendMsg As String = Console.ReadLine() '何も入力されなかった時は終了 If sendMsg Is Nothing OrElse sendMsg.Length = 0 Then Return End If 'サーバーのIPアドレス(または、ホスト名)とポート番号 Dim ipOrHost As String = "127.0.0.1" 'Dim ipOrHost As String = "localhost" Dim port As Integer = 2001 'TcpClientを作成し、サーバーと接続する Dim tcp As New System.Net.Sockets.TcpClient(ipOrHost, port) Console.WriteLine("サーバー({0}:{1})と接続しました({2}:{3})。", _ DirectCast(tcp.Client.RemoteEndPoint, System.Net.IPEndPoint).Address, _ DirectCast(tcp.Client.RemoteEndPoint, System.Net.IPEndPoint).Port, _ DirectCast(tcp.Client.LocalEndPoint, System.Net.IPEndPoint).Address, _ DirectCast(tcp.Client.LocalEndPoint, System.Net.IPEndPoint).Port) 'NetworkStreamを取得する Dim ns As System.Net.Sockets.NetworkStream = tcp.GetStream() '読み取り、書き込みのタイムアウトを10秒にする 'デフォルトはInfiniteで、タイムアウトしない '(.NET Framework 2.0以上が必要) ns.ReadTimeout = 10000 ns.WriteTimeout = 10000 'サーバーにデータを送信する '文字列をByte型配列に変換 Dim enc As System.Text.Encoding = System.Text.Encoding.UTF8 Dim sendBytes As Byte() = enc.GetBytes(sendMsg & ControlChars.Lf) 'データを送信する ns.Write(sendBytes, 0, sendBytes.Length) Console.WriteLine(sendMsg) 'サーバーから送られたデータを受信する Dim ms As New System.IO.MemoryStream() Dim resBytes As Byte() = New Byte(255) {} Dim resSize As Integer = 0 Do 'データの一部を受信する resSize = ns.Read(resBytes, 0, resBytes.Length) 'Readが0を返した時はサーバーが切断したと判断 If resSize = 0 Then Console.WriteLine("サーバーが切断しました。") Exit Do End If '受信したデータを蓄積する ms.Write(resBytes, 0, resSize) 'まだ読み取れるデータがあるか、データの最後が\nでない時は、 ' 受信を続ける Loop While ns.DataAvailable OrElse _ resBytes(resSize - 1) <> AscW(ControlChars.Lf) '受信したデータを文字列に変換 Dim resMsg As String = enc.GetString(ms.GetBuffer(), 0, CInt(ms.Length)) ms.Close() '末尾の\nを削除 resMsg = resMsg.TrimEnd(ControlChars.Lf) Console.WriteLine(resMsg) '閉じる ns.Close() tcp.Close() Console.WriteLine("切断しました。") Console.ReadLine() End Sub End Class
using System; public class Client { public static void Main() { //サーバーに送信するデータを入力してもらう Console.WriteLine("文字列を入力し、Enterキーを押してください。"); string sendMsg = Console.ReadLine(); //何も入力されなかった時は終了 if (sendMsg == null || sendMsg.Length == 0) { return; } //サーバーのIPアドレス(または、ホスト名)とポート番号 string ipOrHost = "127.0.0.1"; //string ipOrHost = "localhost"; int port = 2001; //TcpClientを作成し、サーバーと接続する System.Net.Sockets.TcpClient tcp = new System.Net.Sockets.TcpClient(ipOrHost, port); Console.WriteLine("サーバー({0}:{1})と接続しました({2}:{3})。", ((System.Net.IPEndPoint)tcp.Client.RemoteEndPoint).Address, ((System.Net.IPEndPoint)tcp.Client.RemoteEndPoint).Port, ((System.Net.IPEndPoint)tcp.Client.LocalEndPoint).Address, ((System.Net.IPEndPoint)tcp.Client.LocalEndPoint).Port); //NetworkStreamを取得する System.Net.Sockets.NetworkStream ns = tcp.GetStream(); //読み取り、書き込みのタイムアウトを10秒にする //デフォルトはInfiniteで、タイムアウトしない //(.NET Framework 2.0以上が必要) ns.ReadTimeout = 10000; ns.WriteTimeout = 10000; //サーバーにデータを送信する //文字列をByte型配列に変換 System.Text.Encoding enc = System.Text.Encoding.UTF8; byte[] sendBytes = enc.GetBytes(sendMsg + '\n'); //データを送信する ns.Write(sendBytes, 0, sendBytes.Length); Console.WriteLine(sendMsg); //サーバーから送られたデータを受信する System.IO.MemoryStream ms = new System.IO.MemoryStream(); byte[] resBytes = new byte[256]; int resSize = 0; do { //データの一部を受信する resSize = ns.Read(resBytes, 0, resBytes.Length); //Readが0を返した時はサーバーが切断したと判断 if (resSize == 0) { Console.WriteLine("サーバーが切断しました。"); break; } //受信したデータを蓄積する ms.Write(resBytes, 0, resSize); //まだ読み取れるデータがあるか、データの最後が\nでない時は、 // 受信を続ける } while (ns.DataAvailable || resBytes[resSize - 1] != '\n'); //受信したデータを文字列に変換 string resMsg = enc.GetString(ms.GetBuffer(), 0, (int)ms.Length); ms.Close(); //末尾の\nを削除 resMsg = resMsg.TrimEnd('\n'); Console.WriteLine(resMsg); //閉じる ns.Close(); tcp.Close(); Console.WriteLine("切断しました。"); Console.ReadLine(); } }