PR

VB.NET(VB2022)でTCPクライアント(タスク処理)を実装

VB.NET
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

VB.NET(Visual Basic 2022)のデスクトップアプリでTCP通信の接続要求を行うTCPクライアントをタスクを使用して行う方法を紹介します。タスクで接続待機や受信待機を行うためアプリが固まることなく他の処理が行えます。

TCPクライアントを非同期処理で行う方法を下記記事で紹介していますが、本記事は同期処理をタスクを生成して行う方法をまとめています。以下はVisual Basic 2022をVB2022とします。

VB.NET(VB2022)でTCPクライアント(非同期)を実装

外部機器として下記記事のArduino UNO(以下Arduinoとする)を使用しています。Arduinoはアプリの接続要求に対して応答するサーバーとして使用します。

ArduinoのEthernetライブラリでLAN通信する

VB.NET(VB2022)のデスクトップアプリで動作確認したことを下記リンクにまとめています。

VB.NET(VB2022)のデスクトップアプリ開発でできること

TCPクライアントの動作を確認するアプリ

本記事で作成するアプリの動作画面
本記事で作成するアプリの動作画面

TCP通信を行う接続先のIPアドレスとポートを入力して「接続」ボタンをクリックするとTCP通信の接続を行い、「送信」ボタンで横のテキストボックスに入力した文字列を送信します。

接続先からパケットを受信すると受信したパケットデータをテキストボックスに表示して確認できるようにします。

スポンサーリンク

コントロールの実装

コントロールの追加
コントロールの追加

各種コントロールの追加方法を下記記事にまとめています。本記事でも同様のコントロールを流用して使用します。

VB.NET(VB2022)のUDP通信を非同期処理で行う方法

ツールボックスからコントロールを追加し、一部のプロパティを変更します。コントロールの名称はコントロールの追加の通りの番号の配置とします。

パケットのデータの表示及びTCP接続の管理を行うためのタイマとしてTimer1を追加します。タイマの使用方法は下記記事にまとめています。

VB.NET(VB2022)のタイマーの実装とイベントの追加方法

変数の宣言

アプリで共通する変数を宣言する場合はフォームクラス内で変数を宣言する必要があります。VBの場合はDimでメモリに変数を割り当てます。

Imports System.Net
Imports System.Net.NetworkInformation
Imports System.Net.Sockets
Imports System.Text

Public Class Form1
    Dim ClientTCP As TcpClient
    Dim cts As Threading.CancellationTokenSource
End Class

Form1クラス直前でインポート(Imports)しているものはネームスペースを使用する意味です。7行目のTcpClientはSystem.Net.SocketsネームスペースのTcpClientクラスでインスタンス化する宣言になります。インポートしない場合はSystem.Net.Socketsと記述する必要がありますが、名前の簡素化に従って省略するとネームスペースが自動で追加されます。

例ではアプリ全体で使用するTcpClientをインスタンス化しています。クラス内で宣言した場合はアプリを終了しない限りメモリを割り当てた状態になります。TcpClientはTCP接続やTCPパケットの送受信に使用します。

タスク処理で使用する変数について説明します。8行目のctsはSystem.ThreadingネームスペースのCancellationTokenSourceクラスでインスタンス化する宣言になります。ctsはTCP接続や受信待ちのタスクをキャンセルする場合に使用します。

PR:RUNTEQ(ランテック )- マイベスト3年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール

TCP通信を行う方法

TCP通信の接続から切断までの流れ
TCP通信の接続から切断までの流れ

TCP通信はパソコンのクライアントからArduinoのサーバーに対して接続要求を出し、接続の応答を受けるとTCPパケットの双方向通信ができるようになります。

TCP通信は接続先と1対1の双方向通信を行うため、UDP通信のように1対Nの通信は対応していませんが、プロトコルでパケットの再送や送信順番などを管理するためパケットを正確に伝送できるのが特徴です。

クライアントまたはサーバーが切断要求を出すと接続中のソケットを破棄する処理が進みますが、瞬時に破棄せずに一定期間ソケットを維持します。クライアントでTCP通信のポートを固定している場合は切断したソケットが完全に破棄されるまで次の接続ができなくなるため注意が必要です。

TCP接続を行う

System.Net.Socketsのネームスペースを使用します。TextBoxに入力したIPアドレスやポートを使用して接続先と接続を行います。Button1のクリックイベントに処理を追加します。

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    If Button1.BackColor = Color.LightGreen Then
        remoteIp = TextBox1.Text & "." &
                   TextBox2.Text & "." &
                   TextBox3.Text & "." &
                   TextBox4.Text
        localPort = TextBox8.Text
        remotePort = TextBox5.Text
        ClientTCP = New TcpClient()
        Task.Run(
            Sub()
                Try
                    ClientTCP.Connect(remoteIp, remotePort)
                Catch ex As Exception
                End Try

                If ClientTCP.Connected Then
                    RecvTask()
                End If
            End Sub
        )
        clientstop = True
        Timer1.Enabled = True
    End If
End Sub

remoteIpは接続先のIPアドレスの文字列の生成に使用します。「192.168.11.2」のようにIPアドレスを生成するため文字列の”&”を使用して連結して生成しています。remotePortは接続先のポートの指定に使用します。

TcpClientクラスを初期化してインスタンス化します。インスタンス化したClientTcpを使用してTCP通信の管理を行います。引数に接続先のIPやポートを指定するとTCP接続が開始するため引数はしていません。

ローカルポートを指定して初期化する方法もありますが、TCP通信はプロトコルの仕様上切断してもソケット情報がしばらく破棄されずに残るためローカルポートを指定しない方法が良い場合もあります。指定しない場合は空いたポートが自動で割り当てられます。

TCP接続とパケットの受信はタスクを生成して別のスレッドで行います。タスクはSystem.Threading.tasks.taskクラスのRun()メソッドの引数に処理を追加して生成します。

Connect()メソッドでTCP接続要求を出します。第1引数に接続先のIPアドレス(ホスト名)、第2引数に接続先のポート番号を指定します。接続に失敗した時、アプリが強制終了しないようにTry Catch文で処理を行います。

接続が成功するとタスクのConnect()メソッド以下の処理に進みますが、接続に失敗した場合は受信処理ができないためConnectedプロパティで接続しているかを確認して自作の関数RecvTask()関数に遷移するようにします。

PR: わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジムPython入門講座の申込

TCPパケットを受信する

Private Sub RecvTask()
    Dim getByte(1000) As Byte
    Dim readcnt As Integer

    Invoke(
        Sub()
            TextBox7.Text = "スタート " & vbCrLf
        End Sub
    )
    taskstop = False
    cts = New Threading.CancellationTokenSource()

    While cts.Token.IsCancellationRequested = False
        If ClientTCP.Connected Then
            Try
                readcnt = ClientTCP.GetStream().Read(getByte, 0, getByte.Length)

                For i = 0 To readcnt - 1
                    RxRingSet(getByte(i))
                Next
            Catch ex As Exception

            End Try
        Else
            Exit While
        End If
    End While

    Invoke(
        Sub()
            TextBox7.Text &= vbCrLf & "接続停止"
        End Sub
    )
End Sub

RecvTask()関数はメイン処理とは別のタスク(スレッド)で動作します。そのためメイン処理のコントロールに直接アクセスすることができません。メイン処理のコントロールにアクセスする場合はInvoke()メソッドで生成したデリゲート(メイン処理とスレッドを繋げる門)を使用します。例ではTextBox7のTextプロパティに「スタート」と「接続停止」を指定しています。

Threading.CancellationTokenSource()メソッドでタスクのキャンセルに使用する変数(cts)をインスタンス化します。

タスクをキャンセルせずに次の接続を行うと終了していないタスクが残ってしまい不要なタスクが生成されたままになってしまいます。

TCPパケットの受信はWhile処理で繰り返して行います。タスクがキャンセルされない限りループを抜けずにパケットを受信するようにします。タスクのキャンセルやソケットの消失でアプリが強制終了しないようにTry catch文を使用して同期受信を行います。

パケットの受信はGetStream()のRead()メソッドを使用します。第1引数に読み込んだデータを格納する配列を指定します。第2引数にパケットの読み出しを開始位置を指定します。例ではパケットのデータをすべて取得するために0を指定しています。第3引数に読み出すデータ数を指定します。

戻り値にRead()メソッドで読み出したデータの数が格納されるため、表示用のデータに移し替えて格納します。表示用のデータに移し替えて格納しています。

Read()メソッドは同期受信するためメイン処理で使用するとアプリが占有されフリーズしたように固まってしまいますが、非同期のタスクで処理するためメイン処理に影響を与えることなくTCPパケットを受信することができます。

TCP接続を切断する

Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    If Button2.BackColor = Color.LightGreen Then
        If Button2.BackColor = Color.LightGreen Then
            cts.Cancel() 'タスクをキャンセル
        End If
    End If
End Sub

TCP接続はButton2をクリックして意図的に切断する場合とリンク不良などの意図せずに切断する場合について実装します。

TCP接続を切断する場合はタスクのキャンセルを行ってから行います。タスクのキャンセルはインスタンス化したctsのCancel()メソッドを使用します。

ctsのTokenプロパティはキャンセルの要求を受け付けるとIsCancellationRequestedがTrueになります。タスクの同期受信はタスクのキャンセルされない限り繰り返すようにしていますが、キャンセル要求によりTrueになるとループから抜け出しText7のTextプロパティに「接続停止」を表示してタスクを終了します。

Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick

    If cts.Token.IsCancellationRequested Then
        If taskstop = False Then
            taskstop = True
            ClientTCP.Close()
        End If
    End If

    If ClientTCP.Connected = False Then
        If clientstop Then
            clientstop = False
            Button2.BackColor = Color.LightBlue
            Button1.BackColor = Color.LightGreen
            cts.Cancel() 'タスクをキャンセル
            Timer1.Enabled = False
        End If
    End If
End Sub

ctsでタスクのキャンセルが受け付けられるとClientTCPオブジェクトのClose()メソッドでTCP接続を切断することができます。

ConnectedプロパティでTCP接続の状態を確認し、切断している場合は意図せずTCP接続が切断したと判断してタスクのキャンセルを行います。タスクのキャンセルが行われると上記の通りTCP接続を切断する処理を行います。

TCPパケットを送信する

Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
    Dim data() As Byte = Encoding.GetEncoding("utf-8").GetBytes(TextBox6.Text)
    Dim buf(data.Length + 1) As Byte

    For i = 0 To data.Length - 1
        buf(i) = data(i)
    Next
    'CRLFを付加する場合に追加する
    buf(data.Length) = &HD
    buf(data.Length + 1) = &HA

    If ClientTCP IsNot Nothing Then
        Try
            ClientTCP.GetStream().Write(buf, 0, buf.Length)
        Catch ex As Exception

        End Try
    End If
End Sub

Button3をクリックした時にTCP接続中のクライアントが存在している場合、TextBox6に入力している文字列を「UTF-8」でエンコーディングしてGetStream()Write()メソッドで送信します。

第1引数に送信するデータのアドレス(配列)、第2引数に送信するデータの先頭のオフセット値を指定します。例では配列をすべて送信するため0を指定しています。第3引数に送信するデータのサイズを指定します。

クライアントが存在しない場合、アプリが強制終了するため、クライアントが存在を確認してから切断処理を行います。

スポンサーリンク

動作確認(デバッグ)

動作確認の全体構成
動作確認の全体構成

パソコンとArduino UNO+Ehternet Shield Rev2をLANケーブル(クロス)で接続します。ArduinoのIPアドレスを192.168.11.2にしているのでパソコンのLANポートを同じクラスのIPアドレスとして192.168.11.100にしています。

Arduinoは電源を入れると5秒間隔で温湿度データを測定します。アプリでTCP接続するとサーバーのArduinoがクライアントを検出します。アプリからCRLF(0xD 0x0A)を送信するとArduinoは温湿度データを含めた文字列で応答します。アプリは応答した文字列をTextBox7に表示します。

動作確認のためデバッグを行います。デバッグの開始はエディタ上部の「▶開始」をクリックすると開始します。

アプリの動作確認の結果
アプリの動作確認の結果

アプリのComboBox1で使用可能なイーサーネットを選択します。接続先グループのTextBoxにArduinoのIPアドレス及びポートを指定します。Arduinoはポート80で接続待ちするのでポートは80にします。接続元のポートも80にしていますが自動で割り付けるため使用しません。

デバッグを繰り返し行う場合TextBoxのTextプロパティに直接IPアドレスやポート番号を指定しておくと入力する手間が省けるためスムーズにデバッグが行えます。

「接続」ボタンをクリックするとTCP接続を行います。接続に成功するとButton1の色がLightBlueになりButton2の色がLightGreenになります。タスクの生成の確認を行うためにTCP接続が成功した後でブレークでアプリを一時停止するとエディターのタスクの欄に生成中のタスクが表示されます。

タスクの生成を確認
タスクの生成を確認

「接続」、「切断」を繰り返し行ってもタスクのキャンセルによりタスクが1つになっていることもブレークによって確認できます。

TCP接続が継続している状態で、Arduinoに空のテキスト(実際はCRLFを付加している)を送信するとArduinoが温湿度データを含む文字列で応答しTextBox7に表示します。

次に、アプリからArduinoにTCPパケットを送信します。TextBox6に「TCP接続 タスク」を入力して「送信」ボタンをクリックします。

Arduinoのシリアルモニターの結果
Arduinoのシリアルモニターの結果

Arduinoのシリアルモニターを確認するとクライアントからパケットを受信したことを通知する文字列のnew clientを表示しTextBox6に入力した「TCP接続 タスク」が表示されているため、TCPパケットが送信できていることが分かります。

「切断」ボタンをクリックするとTCP接続を切断します。切断要求を通知するclient disconnectedを表示します。

コードのデバッグを行う場合は、コードエディタでブレークポイントを置くことで一時的にプログラムを停止させることができます。

ブレークポイントでデバッグする方法
ブレークポイントでデバッグする方法

ブレークポイントはコードエディターの最左部分をクリックして●マークが表示されれば設置できています。例ではButton1がクリックされた時に発生したイベントの先頭部分にブレークポイントを置いています。この状態でButton1をクリックするとブレークポイントでプログラムが一時停止します。

F11を押すとステップ実行になるため1行ずつ内容を確認しながらデバッグを行うことができます。上部の「▶続行(C)」をクリックするとブレークポイント状態から通常の動作に復帰します。

PR:(即戦力のスキルを身に着ける:DMM WEBCAMP 学習コース(はじめてのプログラミングコース))

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

Imports System.Net
Imports System.Net.NetworkInformation
Imports System.Net.Sockets
Imports System.Text

Public Class Form1

    Structure NET_INTERFACE
        Dim name As String
        Dim localip As IPAddress
    End Structure

    Const RXRGSZ As Integer = 2048

    Structure TYP_COMRING
        Dim rp As Integer
        Dim wp As Integer
        Dim dat() As Byte
    End Structure

    Dim ClientTCP As TcpClient
    Dim LocalNet() As NET_INTERFACE
    Dim localIp As IPAddress
    'Dim localPort As Integer
    Dim remoteIp As String = ""
    Dim remotePort As Integer
    Dim RxRing As TYP_COMRING '受信したデータを管理
    Dim taskstop As Boolean = False
    Dim clientstop As Boolean = False
    Dim cts As Threading.CancellationTokenSource

    '接続中のネットワークインターフェースからIPを取得
    Private Sub GetNetworkList()
        Dim netinterface = NetworkInterface.GetAllNetworkInterfaces
        Dim wk(netinterface.Length - 1) As NET_INTERFACE
        Dim i As Integer
        Dim j As Integer = 0
        Dim cnt As Integer = 0

        For Each netin In netinterface
            If netin.OperationalStatus = OperationalStatus.Up AndAlso
               netin.NetworkInterfaceType = NetworkInterfaceType.Ethernet Then

                wk(i).name = netin.Name
                cnt += 1

                Dim ipprperties As IPInterfaceProperties = netin.GetIPProperties

                If ipprperties IsNot Nothing Then
                    For Each ipadrs In ipprperties.UnicastAddresses
                        wk(i).localip = ipadrs.Address
                    Next
                End If
            End If

            i += 1
        Next

        ReDim LocalNet(cnt - 1)

        For i = 0 To wk.Length - 1
            If wk(i).name <> "" Then
                LocalNet(j) = wk(i)
                j += 1
            End If
        Next
    End Sub

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ComboBoxInit()
        RxRingInit()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        If Button1.BackColor = Color.LightGreen Then
            remoteIp = TextBox1.Text & "." &
                       TextBox2.Text & "." &
                       TextBox3.Text & "." &
                       TextBox4.Text

            'localPort = TextBox8.Text
            remotePort = TextBox5.Text

            ClientTCP = New TcpClient()
            Task.Run(
                Sub()
                    Try
                        ClientTCP.Connect(remoteIp, remotePort)
                    Catch ex As Exception

                    End Try

                    If ClientTCP.Connected Then
                        Button1.BackColor = Color.LightBlue
                        Button2.BackColor = Color.LightGreen
                        RecvTask()
                    End If
                End Sub
            )
            clientstop = True
            Timer1.Enabled = True
        End If
    End Sub
    'タスクの生成
    Private Sub RecvTask()
        Dim getByte(1000) As Byte
        Dim readcnt As Integer

        Invoke(
            Sub()
                TextBox7.Text = "スタート " & vbCrLf
            End Sub
        )
        taskstop = False
        cts = New Threading.CancellationTokenSource()

        While cts.Token.IsCancellationRequested = False
            If ClientTCP.Connected Then
                Try
                    readcnt = ClientTCP.GetStream().Read(getByte, 0, getByte.Length)

                    For i = 0 To readcnt - 1
                        RxRingSet(getByte(i))
                    Next
                Catch ex As Exception

                End Try
            Else
                Exit While
            End If
        End While

        Invoke(
            Sub()
                TextBox7.Text &= vbCrLf & "接続停止"
            End Sub
        )
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        If Button2.BackColor = Color.LightGreen Then
            If Button2.BackColor = Color.LightGreen Then
                cts.Cancel() 'タスクをキャンセル
            End If
        End If
    End Sub

    Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
        Dim data() As Byte = Encoding.GetEncoding("utf-8").GetBytes(TextBox6.Text)
        Dim buf(data.Length + 1) As Byte

        For i = 0 To data.Length - 1
            buf(i) = data(i)
        Next
        buf(data.Length) = &HD
        buf(data.Length + 1) = &HA

        If ClientTCP IsNot Nothing Then
            Try
                ClientTCP.GetStream().Write(buf, 0, buf.Length)
            Catch ex As Exception

            End Try
        End If
    End Sub

    Private Sub ComboBox1_MouseClick(sender As Object, e As MouseEventArgs) Handles ComboBox1.MouseClick
        ComboBoxInit()
    End Sub

    Private Sub ComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox1.SelectedIndexChanged
        localIp = LocalNet(ComboBox1.SelectedIndex).localip
    End Sub

    Private Sub ComboBoxInit()
        Dim i As Integer
        Dim str As String

        GetNetworkList()
        ComboBox1.Items.Clear()

        For i = 0 To LocalNet.Length - 1
            str = LocalNet(i).name & " " & "IP:" & LocalNet(i).localip.ToString
            ComboBox1.Items.Add(str)
        Next
    End Sub

    Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        Dim sz As Integer
        Dim i As Integer

        sz = RxRing.wp - RxRing.rp

        If sz < 0 Then
            sz += RxRing.dat.Length
        End If

        If sz > 0 Then
            For i = 0 To sz - 1
                TextBox7.Text &= Chr(RxRing.dat(RxRing.rp))
                RingRpAdd()
            Next
        End If

        If cts.Token.IsCancellationRequested Then
            If taskstop = False Then
                taskstop = True
                ClientTCP.Close()
            End If
        End If

        If ClientTCP.Connected = False Then
            If clientstop Then
                clientstop = False
                Button2.BackColor = Color.LightBlue
                Button1.BackColor = Color.LightGreen
                cts.Cancel() 'タスクをキャンセル
                Timer1.Enabled = False
            End If
        End If
    End Sub

    'RxRingの初期化を行う
    Private Sub RxRingInit()
        RxRing.wp = 0
        RxRing.rp = 0
        ReDim RxRing.dat(RXRGSZ - 1)
    End Sub

    'RxRingのwpを更新する
    Public Sub RxRingSet(dat As Byte)

        RxRing.dat(RxRing.wp) = dat
        RxRing.wp += 1
        If RxRing.wp = RxRing.dat.Length Then
            RxRing.wp = 0
        End If
    End Sub

    'RxRingのrpを更新する
    Private Sub RingRpAdd()

        RxRing.rp += 1
        If RxRing.rp >= RxRing.dat.Length Then
            RxRing.rp = 0
        End If
    End Sub

    Private Sub Button4_Click(sender As Object, e As EventArgs) Handles Button4.Click
        TextBox7.Clear()
    End Sub

End Class

TextBox1~4、5、8は数値以外の文字列を受け付けるため数値以外の入力を許可しないようにKeyPressイベントを追加実装しても良いと思います。KeyPressイベントについては下記記事を参考にしてください。

VB.NET(VB2022)のTextBoxの実装とイベントの追加方法

コントロールの番号やイベントハンドラーなどのプロパティ名などを変更している場合はソースコードのイベントハンドラーをお使いのイベントハンドラーに置き換えてください。

関連リンク

VB.NET(VB2022)のデスクトップアプリで動作確認したことを下記リンクにまとめています。

VB.NET(VB2022)のデスクトップアプリ開発でできること

マイクロソフトはVisual Studio以外にもVSCodeという便利なエディターを提供しています。下記リンクではVSCodeのインストールの仕方からC言語開発環境の作り方までをまとめています。

VSCodeをインストールしてC/C++の開発環境を作る

PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル

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

タイトルとURLをコピーしました