DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

TCPクライアント・サーバープログラムを作成する

ここでは、TCP(Transmission Control Protocol)サーバーとクライアントを作成する方法の基本的な事柄について説明します。非同期処理などのさらに高度な事柄については、「TCPを利用した複数クライアント接続可能なチャットアプリケーションを作る」で説明します。

TCPサーバーはTcpListenerクラスを、TCPクライアントはTcpClientクラスを使用して作成すると、比較的簡単(といっても難しいですが)にできます。

TCPサーバーの作成

サーバーは指定されたポートを監視し、クライアントの接続要求を待ちます。クライアントからの接続要求があり、これをサーバーが受け付けると、接続が完了し、切断するまでの間、データの送受信が可能となります。

早速、TCPサーバーを作ってみましょう。大ざっぱな手順は次のようになります。

  1. TcpListenerクラスのStartメソッドによりListen(監視)を開始し、クライアントからの接続要求を待機します。
  2. 接続要求があれば、AcceptTcpClientメソッドにより、クライアントの接続要求を受け入れます。
  3. AcceptTcpClientメソッドで返されるTcpClientオブジェクトのGetStreamメソッドにより、NetworkStreamを取得します。データの送受信には、このNetworkStreamを使用します。
  4. Listenを続けるのであれば、再びAcceptTcpClientメソッドで接続を待機します。Listenを終了するのであれば、Stopメソッドを呼び出します。

以下に示すコンソールアプリケーションは、ローカルIPアドレスのポート2001で動作し、まず接続してきたクライアントからデータ(文字列)を受信し、受信した文字列の長さを返信してから、クライアントを切断しています。その後サーバーはすぐに終了します。もし終了させずにListenを継続する場合は、リスナを閉じずに、再びAcceptTcpClientメソッドを呼び出してください(ループさせることになるでしょう)。

このサンプルでは、送信するデータの最後にはラインフィード文字を付けるという決まりにしています。このような決まりによって、データを受信する側がいつまで受信したらいいのか判断できるようになります。(それ以外の方法としては、はじめにデータのサイズを送信して知らせておくといった方法も考えられます。)

ここで紹介している方法は、Listenやデータの送受信を同期的に行なっているため、複数のクライアントと同時に接続してやり取りを行うことはできません。

VB.NET
コードを隠すコードを選択
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
C#
コードを隠すコードを選択
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();
    }
}

すべてのIPアドレスをListenする

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アドレスとポート番号を表示する部分だけです。それ以外は上記のコードと同じで大丈夫です。

VB.NET
コードを隠すコードを選択
'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)
C#
コードを隠すコードを選択
//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クライアントの作成

次はTCPクライアントを作成する方法です。手順は、次の通りです。

  1. TcpClientクラスのコンストラクタ(あるいはConnectメソッド)により、サーバーと接続します。
  2. サーバーとの接続に成功したところで、TcpClientクラスのGetStreamメソッドにより、NetworkStreamを取得し、データの送受信を行います。
  3. Closeメソッドにより、サーバーとの接続を終了します。

以下に示すコンソールアプリケーションは、ローカルIPアドレスのポート2001に接続した後、入力された文字列をサーバーに送信し(キーボードから文字列を入力し、Enterキーを押す)、サーバーから送られてくるデータを受信しています。

VB.NET
コードを隠すコードを選択
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
C#
コードを隠すコードを選択
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();
    }
}
  • 履歴:
  • 2011/3/1 NetworkStream.Closeを呼び出すように修正。
  • 2013/6/28 クライアントが切断した時にCloseが呼び出されない不具合を修正。Dns.Resolveの代わりにDns.GetHostEntryを使うように変更など。
  • 2013/8/4 「すべてのIPアドレスをListenする」を追加など。
  • 2015/3/16 送信するデータの最後に\nを付けるようにした。IPアドレスを指定してListen、接続をするようにした。タイムアウト時間を設定するようにしたなど。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • このサイトで紹介されているコードの多くは、例外処理が省略されています。例外処理については、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。