C#

【C#】 TCP通信を非同期ソケットで使う方法(サーバー編)

C#でTCP/IP通信を非同期で実装するのに苦戦したことがありました。原因は、TCP/IP通信についての理解が不十分だったからです。

前提知識が記事にまとめてあれば、スムーズに理解が進むと思うので、いろいろと調べたことを整理して記事にまとめてみました。

これからTCP/IP通信を実装する方には、特にオススメの記事となりますので、参考にしてみてください。

プログラミングを更に学びたい方必見

選抜された現役エンジニアから学べるプログラミングスクールで、現場や副業で役に立つノウハウやスキルを習得してみませんか?

オンラインに特化しているので場所に捉われず、自分のライフスタイルに合わせて受講することができます。

副業に興味がある方に人気の「はじめての副業コース」をはじめ、Web開発やシステム開発・アプリ開発などカテゴリ毎に受講ができ、初心者の方でも挫折することなく学ぶことができます。

まずはこの機会に現役のプロに無料相談をしてみましょう。

\ プログラミングスクールの詳細を確認する /

TCP/IP通信の理解

TCP/IPとは

TCP/IPは 「インターネット・プロトコル・スイート」 とも呼ばれ、現在でも 世界標準で利用されている通信規格 です。TCP/IPは機器やOSが異なっていても、共通のプロトコルを用いて通信を成立させることができます。例えば、パソコンとパソコン同士だけでなく、産業機器で使用されるPLC(プログラマブルコントローラー)と言った異なる機器同士で通信することができます。

特徴として、通信相手を確認して接続をしてから通信をするため、確実にデータを送受信することができます。確実にデータの送受信ができるので通信品質が高く、現在でも使用されている通信手段の1つです。

通信の特徴
  1. これから通信を始めることを確認した後で通信を開始する方式である
  2. 通信を開始する時点から通信相手に届くことが保証されている
  3. 通信終了時も、通信相手との間で通信を終了することを確認してから終える

この通信プロトコルをプログラムで利用するには ソケット を使って通信をします。

ソケットとは

ソケットは、TCP/IPをプログラムから利用する際に使用され、プログラムとネットワークを結びつける為のエンドポイント(通信の出入口)のことです。

エンドポイントは、IPアドレス と ポート番号 を指定して識別します。この中でポート番号の指定が特に重要です。

通信相手のIPアドレスが分かれば、そのIPアドレスにデータを送信することまではできますが、そのデータをどこに受け渡したらいいのか相手は判断ができません。
ポート番号があることで、通信を行うアプリケーションを特定することができ、データの受け渡しができます。

また、ポート番号は16ビットの数値なので、0~65535(0xFFFF)の範囲内で指定されます。

ポート番号の役割
ポート番号のタイプポート番号の範囲意味
ウェルノウンポート0~1023サーバーアプリケーション用に予約されているポート番号
(例:HTTPなら80、FTPなら20/21)
登録済みポート1024~49151よく利用されるアプリケーションのサーバー側ポート番号
ダイナミック 又は
プライベートポート
49152~65535クライアントアプリケーション用のポート番号

 

通信処理の流れ

上記の「TCP/IPとは」で説明した内容をフローチャートにすると以下になります。
この通信フローの順番でプログラムの処理を書くことになります。

今回のサンプルコードではサーバーとクライアント間で接続をして、クライアントから「Hello」を受信したら、「OK」を送信するプログラムを作成します。通信には非同期なソケット通信を使用します。

サーバー側の処理

サーバー側の処理を行う為に4つの関数を用意します。上図のフローチャート(通信処理の流れ)と比較しながら関数の役割を確認してみてください。

使用する独自関数
関数役割
StartListening指定されたIPアドレスとポートを、TCP/IP通信のソケットのエンドポイントとしてバインドする。非同期ソケットを開始して、接続を待機する。
AcceptCallbackクライアントとソケットで接続を確立し、データが送信されるを待ち受ける。
ReadCallbackクライアントからデータを受信し、受信したデータを蓄積する。蓄積データに終了タブ<EOF>があれば、受信を終了する。
SendCallbackクライアントへデータ送信を行い、ソケット通信を終了する。

 

サンプルソースコード

「tcpserver.cs」という新しい項目(ファイル)を作成して、プロジェクトに追加します。
ソケット関連で使用する名前空間 System.Net.Sockets System.Net を参照するために、usingディレクティブを用いてコードの先頭に記述します。

using System;
using System.Net.Sockets;
using System.Net;

 

StartListening関数

まずIPアドレスとポート番号を指定して、エンドポイントを設定します。IPアドレスは、  IPAddress.Any として、どんなIPアドレスも受け付けるようにします。ポート番号は引数で渡すことで変更できるようにしました。

作成したソケットにローカルエンドポイントをバインドして、TCP/IPの接続を待機します。

今回は while でクライアントと接続をし続けるので、接続待機状態はUI画面が操作できなくなります。そこで Task.Run() で非同期処理とし、別スレッドで while を実行します。

TcpServerの接続時の処理を非同期にする際は、 BeginAccept を用いて実装します。第1引数には、接続が確立された時に実行されるメソッド、第2引数には、ソケットを指定します。

 while ループさせ続けるため、 BeginAccept が何度も行われないように、System.ThreadingのManualResetEventクラスを用いて、接続が完了するまで待機します。

ManualResetEventに WaitOne() を呼び出しすると、シグナルを受け取るまでスレッドをブロックして待機し続けます。

 Set() を呼び出しすると、シグナル状態となり、待機中のスレッドが進行されます。シグナル状態になった後は、再利用するために Reset() を呼び出ししておきます。

//マニュアルリセットイベントのインスタンスを生成
public ManualResetEvent allDone = new ManualResetEvent(false);

//TCP/IPの接続開始処理
public async Task<bool> StartListening(int port)
{
    // IPアドレスとポート番号を指定して、ローカルエンドポイントを設定
    IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, port);

    // TCP/IPのソケットを作成
    Socket TcpServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    try
    {
        TcpServer.Bind(localEndPoint);  // TCP/IPのソケットをローカルエンドポイントにバインド
        TcpServer.Listen(10);           // 待ち受け開始

        await Task.Run(() = >
        {
            while (true)
            {
                // シグナルの状態をリセット
                allDone.Reset();

                // 非同期ソケットを開始して、接続をリッスンする
                Debug.WriteLine("接続待機中...");
                TcpServer.BeginAccept(new AsyncCallback(AcceptCallback), TcpServer);

                // シグナル状態になるまで待機
                allDone.WaitOne();
            }
        });

    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex.ToString());
    }

    return false;
}

 

AcceptCallback関数

クライアントと通信が確立されると、このメソッドの処理が実行されます。接続先のソケット情報を取得して、クライアントからのデータ受信を待ち受けます。

ここでも非同期処理にする為に、データ受信では BeginReceive を用いて開始をします。

public void AcceptCallback(IAsyncResult ar)
{
    // シグナル状態にし、メインスレッドの処理を続行する
    allDone.Set();

    // クライアント要求を処理するソケットを取得
    Socket TcpServer = (Socket)ar.AsyncState;
    Socket TcpClient = TcpServer.EndAccept(ar);

    // 端末からデータ受信を待ち受ける
    StateObject state = new StateObject();
    state.workSocket = TcpClient;
    TcpClient.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
}

 

ReceiveCallback関数

クライアントから送信されたデータを受信すると、このメソッドの処理が実行されます。

この処理では、受信したデータが0になるまでデータを蓄積します。データの区切り文字である<EOF>を受け取れば、「OK」を送信する処理へ移行します。それ以外の場合は、もう一度、 ReceiveCallback を行い受信を再開します。

データを送信する際も同様に非同期で行うので、データ送信では BeginSend を用いて開始をします。

public static void ReceiveCallback(IAsyncResult ar)
{
    var content = string.Empty;

    try
    {
        // 非同期オブジェクトからソケット情報を取得
        StateObject state = (StateObject)ar.AsyncState;
        Socket TcpClient = state.workSocket;

        // クライアントソケットからデータを読み取り
        int bytesRead = TcpClient.EndReceive(ar);

        if (bytesRead > 0)
        {
            // 受信したデータを蓄積
            state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));

            // 蓄積データの終端タグを確認
            content = state.sb.ToString();
            if (content.IndexOf("<EOF>") > -1)
            {
                // 終了タグ<EOF>があれば、読み取り完了
                Debug.WriteLine(string.Format("クライアントから「{0}」を受信", content));

                // ASCIIコードをバイトデータに変換
                byte[] byteData = Encoding.ASCII.GetBytes("OK");

                // クライアントへデータの送信を開始
                TcpClient.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), TcpClient);
            }
            else
            {
                // 取得していないデータがあるので、受信再開
                TcpClient.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
            }
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex.ToString());
    }
}

 

SendCallback関数

クライアントに対して、データの送信が完了すると、このメソッドの処理が実行されます。

送信と受信側のソケット通信をシャットダウンさせて終了させます。
private static void SendCallback(IAsyncResult ar)
{
    try
    {
        // 非同期オブジェクトからソケット情報を取得
        Socket TcpClient = (Socket)ar.AsyncState;

        // クライアントへデータ送信完了
        int bytesSent = TcpClient.EndSend(ar);
        Debug.WriteLine("「OK」をクライアントへ送信");

        //ソケット通信を終了
        Debug.WriteLine("接続終了");
        TcpClient.Shutdown(SocketShutdown.Both);
        TcpClient.Close();

    }
    catch (Exception e)
    {
        Debug.WriteLine(e.ToString());
    }
}

 

StateObjectクラス

ここでは非同期処理の際、ソケット情報(受信バッファと蓄積した受信データ)を保持するために使用します。

クライアントと接続が確立された時にソケットが保存され、受信時のコールバック関数により受信したデータをバッファのクラス変数に保存します。

// 非同期処理でソケット情報を保持する為のオブジェクト
public class StateObject
{
    // 受信バッファサイズ
    public const int BufferSize = 1024;

    // 受信バッファ
    public byte[] buffer = new byte[BufferSize];

    // 受信データ
    public StringBuilder sb = new StringBuilder();

    // ソケット
    public Socket workSocket = null;
}

サンプル使用例

上記で説明したサンプルソースコードを使用して、WPFでサーバーのアプリケーションを作成してみます。

ポート番号を入力して、受信開始ボタンをクリックすればクライアントと接続されるまで待機します。画面はマテリアルデザインを適用させてオシャレな画面に仕上げました。

マテリアルデザインの適用方法については、以下の記事で詳しく記載しています。

【WPF】Material Designでオシャレな画面デザインに変更する方法WPFでオシャレなUI画面にするならMaterial Design In XAML Toolkitをオススメします。Material DesignとはGoogleが提唱したデザインシステムで、自作のUI画面に簡単に適用できます。Nugetからパッケージをインストールして適用するまでの手順とデモアプリのインストール方法を紹介します。...
XAMLのソースは以下の記述になります。
<Window
    x:Class="TcpServer.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="TCP/IP (Server)"
    Width="500"
    Height="320"
    WindowStyle="None"
    mc:Ignorable="d">
    <Grid>
        <!--  ヘッダー  -->
        <materialDesign:ColorZone
            Height="50"
            Padding="12"
            Mode="PrimaryMid">
            <DockPanel>
                <materialDesign:PopupBox DockPanel.Dock="Right" PlacementMode="BottomAndAlignRightEdges">
                    <ListBox>
                        <ListBoxItem Content="Close" />
                    </ListBox>
                </materialDesign:PopupBox>
                <StackPanel Orientation="Horizontal">
                    <materialDesign:PackIcon
                        Width="26"
                        Height="26"
                        Kind="AccessPoint" />
                    <TextBlock
                        Margin="16,0,0,0"
                        VerticalAlignment="Center"
                        Text="TCP/IP SAMPLE" />
                </StackPanel>
            </DockPanel>
        </materialDesign:ColorZone>

        <Label
            Margin="15,61,0,0"
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            Content="TCP/IP通信のサーバー側のサンプル画面です。" />

        <!--  ポート番号入力欄  -->
        <Label
            x:Name="lblPort"
            Margin="15,98,0,0"
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            Content="ポート番号:" />
        <TextBox
            x:Name="txtPort"
            Width="185"
            Margin="90,85,0,0"
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            materialDesign:HintAssist.Hint="ポート番号を入力してください。"
            Style="{StaticResource MaterialDesignFloatingHintTextBox}" />

        <!--  受信開始ボタン  -->
        <Button
            x:Name="btnStart"
            Width="90"
            Height="30"
            Margin="0,92,10,0"
            HorizontalAlignment="Right"
            VerticalAlignment="Top"
            Click="BtnStart_Click"
            Content="受信開始"
            Foreground="Black"
            Style="{StaticResource MaterialDesignOutlinedButton}" />

        <!--  ログ出力欄  -->
        <TextBox
            x:Name="txtLog"
            Margin="10,140,10,10"
            AcceptsReturn="True"
            IsEnabled="{Binding ElementName=MaterialDesignOutlinedTextBoxEnabledComboBox}"
            Style="{StaticResource MaterialDesignOutlinedTextBox}"
            TextWrapping="Wrap"
            VerticalScrollBarVisibility="Auto" />
    </Grid>
</Window>
コードは以下の記述になります。
using System.Windows;

namespace TcpServer
{
    public partial class MainWindow : Window
    {
        tcpserver tcp = new tcpserver();

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void BtnStart_Click(object sender, RoutedEventArgs e)
        {
            btnStart.IsEnabled = false;
            if (!await tcp.StartListening(int.Parse(txtPort.Text)))
            {
                btnStart.IsEnabled = true;
            }
        }
    }
}

動作確認

サーバー側とクライアント側のアプリケーションをそれぞれ起動します。

【C#】ソケット通信(非同期)でクライアントの処理を記述する方法非同期でクライアント側のソケット通信を実装する方法について記載しています。本記事で紹介しているプログラム言語はC#です。ソケット通信について詳しく解説していきますので、参考にして下さい。...

サーバーアプリにポート番号を入力してから受信開始状態にし、クライアントアプリからサーバーへ接続要求をしてみましょう。

TCP/IP通信でデータの送受信ができていることが確認できました。

プログラミングを更に学びたい方必見

選抜された現役エンジニアから学べるプログラミングスクールで、現場や副業で役に立つノウハウやスキルを習得してみませんか?

オンラインに特化しているので場所に捉われず、自分のライフスタイルに合わせて受講することができます。

副業に興味がある方に人気の「はじめての副業コース」をはじめ、Web開発やシステム開発・アプリ開発などカテゴリ毎に受講ができ、初心者の方でも挫折することなく学ぶことができます。

まずはこの機会に現役のプロに無料相談をしてみましょう。

\ プログラミングスクールの詳細を確認する /

まとめ

この記事ではSoketクラスを使用してサーバー側の通信方法について記載しました。.NETではSoketクラスよりも簡単にTCP通信ができるように、TcpListenerクラスが用意されています。初心者の方には扱いやすいクラスですので、こちらの記事も参考にしてみてください。

【C#】TcpListenerでサーバーの通信処理を作成C#でTcpListenerクラスを使って、非同期でTCP通信するプログラム実装方法について記載しています。この記事ではサーバー側の通信サンプルについて解説していますので、参考にしてみてください。...

以上、最後まで読んでいただきありがとうございました。

プログラミングを学習したいなら…

プログラミングスキルを身に付けるなら、プログラミングを効率良く学べる
プログラミングスクール」がオススメです。

特にこんな方にオススメ!!
これからエンジニアを目指したい
プログラミングの専門性を高めたい
プログラミングを学んで副業をしたい
エンジニアに転職して年収をアップさせたい

プログラミングを触ったことがない未経験からでも、プログラミングスクールで学習すれば、エンジニアへ就職・転職することも可能です。

あなたの「行動力」と「やる気」で、あなたの人生を大きく変えるチャンスになることでしょう。

プログラミングスクールに興味がある方は是非チェックしてみてください。

> プログラミングを学ぶ <

COMMENT

メールアドレスが公開されることはありません。

CAPTCHA