none
TCP通信について RRS feed

  • 質問

  • 開発環境:VisualStadio 2013 Pro
    開発言語:Visual Basic

    上記環境にて、サーバーとTCP通信するソフトを開発しております。
    vb.netのソケット通信にて通信しております。

    通信開始と通信終了は問題なく出来ております。
    backgroundworkerにて
    通信開始⇒Read⇒通信終了のループでサーバーのデータを取得しております。

    ただ、通信していると何回かに1回正しくデータを取得できません。
    正しくデータを取得できないというのは
    受け取ったデータが、すべて0だったり、バラバラのデータを取得します。

    原因は、通信のReadに問題があるかと思われますが、
    正しく受信するプログラミングの仕方がわかりません。

    ただしくReadする方法をご教授いただけないでしょうか。

    下記に、ソースの一部抜粋をいたしました。

    #Region "=== 通信プロパティ ==="
        Dim client As TcpClient
        Dim stream As NetworkStream
        Dim strReader As System.IO.BinaryReader
        Dim strWriter As System.IO.BinaryWriter
        Dim _buffer() As Byte = New Byte() {}
        Dim command As String = Nothing
        'PLCのIPアドレス(サーバ)
        Dim ipPlc As String = "192.168.0.10"
        'PLCのポート(上位リンク)
        Dim PLCport As Integer = 8501
    #End Region

    #Region "   --- 通信開始 ---"
        Public Sub TcpConnect()
            client = New TcpClient()
            Try
                'TCP/IP接続を行う
                client.Connect(ipPlc, PLCport)
                'ストリームを取得する
                stream = client.GetStream()
                strWriter = New System.IO.BinaryWriter(stream)
                strReader = New System.IO.BinaryReader(stream)

            Catch ex As Exception
                MsgBox("TcpPort is closed")
                client.Close()
            End Try
        End Sub
    #End Region

    #Region "   --- 通信終了 ---"
        Public Sub TcpClose()
            Try
                strReader.Dispose()
                strReader.Close()
                stream.Dispose()
                stream.Close()
                client.Close()
                _SysMessage = "Close"
            Catch ex As Exception
                _SysMessage = ex.Message
            End Try
        End Sub
    #End Region

    #Region "   --- MRデータ Read 処理(M:内部リレー)ビット数:1000  ---"
        Public Sub MR_Read(ByVal MR_num As Integer)

            Erase _buffer
            _buffer = New Byte() {}
            command = Nothing
            Erase BitData
            BitData = New Byte(2108) {}
            buffer() = Nothing

            ' 送信するデータを作成
            Dim utf8 As Encoding = Encoding.UTF8

            'コマンド
            command = "RDS MR" & MR_num & " 1000"

            Dim sendBytes As Byte() = utf8.GetBytes(command & ControlChars.CrLf)

            ' データ送信
            strWriter.Write(sendBytes, 0, sendBytes.Length)

            ' データ受信
            Dim memoryStream As New MemoryStream()
            Dim resBytes As Byte() = New Byte(2108) {}
            Dim resSize As Integer = 0

            Do
                'resSize = stream.Read(resBytes, 0, resBytes.Length)

                resSize = strReader.Read(resBytes, 0, resBytes.Length)

                ' サーバから切断された
                If resSize = 0 Then
                    _Err_Read = True
                    Exit Do
                End If

                memoryStream.Write(resBytes, 0, resSize)
            Loop While stream.DataAvailable OrElse resBytes(resSize - 1) <> AscW(ControlChars.Lf)

            Select Case _Err_Read
                Case False '正常時
                    ' 受け取ったデータを加工
                    Dim resMsg As String = utf8.GetString(memoryStream.GetBuffer(), 0, CInt(memoryStream.Length))

                    '16進⇒10進
                    Convert16to10(MR_num)

                    '配列の初めの番号
                    Dim s As Integer = 1

                    'PCのPLCデータ配列に入れる
                    For i As Integer = 1 To 1000
                        '数値判定Flag:初期化
                        _flag = True

                        For Each _c In Mid(resMsg, s, 1)
                            '数字以外の文字が含まれているか調べる
                            If _c < "0"c OrElse "9"c < _c Then
                                _flag = False
                                Exit For
                            End If
                        Next

                        '文字判定
                        Select Case _flag
                            Case True '数値である
                                '16進数を10進に直した配列に入れる。
                                PLC_Data.MR_Array_Data_Set(_cv16to10) = Long.Parse(Mid(resMsg, s, 1))
                                _cv16to10 += 1
                                s += 2

                            Case False '数値でない
                                '文字の場合
                                Exit For
                        End Select
                    Next i
                Case True '異常時
                    _Err_Read = False
                    Exit Sub
            End Select
        End Sub
    #End Region

    2016年3月7日 5:35

回答

  • 某K社のシーケンサという前提での回答です。

    NetWorkStreamの終端をDataAvailableとvbLFで判定していますが、DataAvailableは通信の終端ではなく、その時点で読み取れるデータが無いことを示します。
    また、1000個のリレーを読み取ると2000バイト以上のデータになりますが、1パケットに収まらすに2個以上のパケットに分割されていると思われます。
    そのため、たとえば1個目と2個目のパケットの間に遅延などがあった場合、2個目を受け取る前にDataAvailableが0になりえます。
    そうなると途中までのデータしか受信していないのに処理が進んでしまうことになります。

    さらにTcpClientを開きっぱなしで使い続けた場合、前回に読み残したデータが混ざるために、想定していないデータを処理してしまうこともあるでしょう。

    Readで1バイトも読み出せなかったら終端であるという判定に変えてみたらどうでしょうか。

        Public Function MR_Read(ByVal MR_num As Integer, ByVal count As Integer) As List(Of Byte)
            Dim list As New List(Of Byte)
    
            If (count < 1 OrElse 9999 < count) Then
                Throw New ArgumentOutOfRangeException("count")
            End If
    
            ' 送信するデータを作成
            Dim utf8 As Encoding = Encoding.UTF8 'ASCIIの範囲しか使わないとは思うけ
            'コマンド
            command = String.Format("RDS MR{0} {1}", MR_num, count)
            Dim sendBytes As Byte() = utf8.GetBytes(command & ControlChars.CrLf)
            strWriter.Write(sendBytes, 0, sendBytes.Length) ' データ送信
    
            ' データ受信
            Dim memoryStream As New MemoryStream()
            Dim resBytes As Byte() = New Byte(2108) {}
            Dim resSize As Integer = 0
            Do
                resSize = strReader.Read(resBytes, 0, resBytes.Length)
                memoryStream.Write(resBytes, 0, resSize)
            Loop While resSize <> 0 'DataAvailableは終端ではない
    
            If (memoryStream.Position = 0) Then
                _Err_Read = True '1バイトも受信できなかった
            Else
                memoryStream.Position = memoryStream.Position - 1
                memoryStream.Read(resBytes, 0, 1)
                _Err_Read = resBytes(0) <> AscW(vbLf) '最後に格納されたのがLFではないなら読み出し失敗
            End If
    
            Select Case _Err_Read
                Case False '正常時
                    ' 受け取ったデータを加工
    
                    Dim resMsg As String = utf8.GetString(memoryStream.GetBuffer(), 0, CInt(memoryStream.Length))
                    resMsg = resMsg.TrimEnd(vbCr, vbLf) '末尾のCR,LFは捨てる
    
                    '正規表現で数字とスペースのみかどうかをチェック
                    Dim reg As New System.Text.RegularExpressions.Regex("^[01 ]+$")
                    If (Not reg.IsMatch(resMsg)) Then
                        '0/1とスペース以外が含まれている
                        _Err_Read = True
                    Else
                        Dim parts As String() = resMsg.Split(" "c)
                        If (parts.Count <> count) Then
                            _Err_Read = True '数が合わない
                        Else
                            Dim value As Integer = 0
                            Dim i As Integer = 0
                            For Each part As String In parts
                                value = value * 2 + Integer.Parse(part)
    
                                i += 1
                                If (i = 8) Then
                                    i = 0
                                    list.Add(value)
                                    value = 0
                                End If
                            Next
                            If (i <> 0) Then
                                list.Add(value) '8bitにまとまらない端数
                            End If
                        End If
                    End If
                Case True '異常時
                    _Err_Read = False
            End Select
            Return list
        End Function
    #k○-1000とRS232ユニットしか手元にないのでEthernet有りの実機では試せてないです

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 星 睦美 2016年3月8日 8:29
    • 回答としてマーク 星 睦美 2016年5月11日 7:35
    2016年3月7日 11:24

すべての返信

  •  「通信開始⇒Read⇒通信終了のループでサーバーのデータを取得しております。」ということですが、「通信開始⇒[Read のループ]⇒通信終了」で良いのではないでしょうか?
    え~っと、「通信開始/終了」は、ソケットの Open / Close ですよね?1度の Read 毎に、毎回 Open / Close しなければならないのでしょうか。プログラムが始まる、あるいはユーザによる通信開始指示により Open し、定期的に Read(コードを見ると、コマンドを送って、その戻り値を読み取る)、プログラムの終わりかユーザによる通信終了指示で Close とすると、良いのでは?

     あとは、LAN 上のデータを見て、本当に読み取り側が悪いのかどうかの確認とか。


     それと、重箱の隅ですが、× VisualStadio ○ Visual Studio


    Jitta@わんくま同盟

    2016年3月7日 11:10
  • 某K社のシーケンサという前提での回答です。

    NetWorkStreamの終端をDataAvailableとvbLFで判定していますが、DataAvailableは通信の終端ではなく、その時点で読み取れるデータが無いことを示します。
    また、1000個のリレーを読み取ると2000バイト以上のデータになりますが、1パケットに収まらすに2個以上のパケットに分割されていると思われます。
    そのため、たとえば1個目と2個目のパケットの間に遅延などがあった場合、2個目を受け取る前にDataAvailableが0になりえます。
    そうなると途中までのデータしか受信していないのに処理が進んでしまうことになります。

    さらにTcpClientを開きっぱなしで使い続けた場合、前回に読み残したデータが混ざるために、想定していないデータを処理してしまうこともあるでしょう。

    Readで1バイトも読み出せなかったら終端であるという判定に変えてみたらどうでしょうか。

        Public Function MR_Read(ByVal MR_num As Integer, ByVal count As Integer) As List(Of Byte)
            Dim list As New List(Of Byte)
    
            If (count < 1 OrElse 9999 < count) Then
                Throw New ArgumentOutOfRangeException("count")
            End If
    
            ' 送信するデータを作成
            Dim utf8 As Encoding = Encoding.UTF8 'ASCIIの範囲しか使わないとは思うけ
            'コマンド
            command = String.Format("RDS MR{0} {1}", MR_num, count)
            Dim sendBytes As Byte() = utf8.GetBytes(command & ControlChars.CrLf)
            strWriter.Write(sendBytes, 0, sendBytes.Length) ' データ送信
    
            ' データ受信
            Dim memoryStream As New MemoryStream()
            Dim resBytes As Byte() = New Byte(2108) {}
            Dim resSize As Integer = 0
            Do
                resSize = strReader.Read(resBytes, 0, resBytes.Length)
                memoryStream.Write(resBytes, 0, resSize)
            Loop While resSize <> 0 'DataAvailableは終端ではない
    
            If (memoryStream.Position = 0) Then
                _Err_Read = True '1バイトも受信できなかった
            Else
                memoryStream.Position = memoryStream.Position - 1
                memoryStream.Read(resBytes, 0, 1)
                _Err_Read = resBytes(0) <> AscW(vbLf) '最後に格納されたのがLFではないなら読み出し失敗
            End If
    
            Select Case _Err_Read
                Case False '正常時
                    ' 受け取ったデータを加工
    
                    Dim resMsg As String = utf8.GetString(memoryStream.GetBuffer(), 0, CInt(memoryStream.Length))
                    resMsg = resMsg.TrimEnd(vbCr, vbLf) '末尾のCR,LFは捨てる
    
                    '正規表現で数字とスペースのみかどうかをチェック
                    Dim reg As New System.Text.RegularExpressions.Regex("^[01 ]+$")
                    If (Not reg.IsMatch(resMsg)) Then
                        '0/1とスペース以外が含まれている
                        _Err_Read = True
                    Else
                        Dim parts As String() = resMsg.Split(" "c)
                        If (parts.Count <> count) Then
                            _Err_Read = True '数が合わない
                        Else
                            Dim value As Integer = 0
                            Dim i As Integer = 0
                            For Each part As String In parts
                                value = value * 2 + Integer.Parse(part)
    
                                i += 1
                                If (i = 8) Then
                                    i = 0
                                    list.Add(value)
                                    value = 0
                                End If
                            Next
                            If (i <> 0) Then
                                list.Add(value) '8bitにまとまらない端数
                            End If
                        End If
                    End If
                Case True '異常時
                    _Err_Read = False
            End Select
            Return list
        End Function
    #k○-1000とRS232ユニットしか手元にないのでEthernet有りの実機では試せてないです

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 星 睦美 2016年3月8日 8:29
    • 回答としてマーク 星 睦美 2016年5月11日 7:35
    2016年3月7日 11:24
  • ありがとうございます。すごく参考になりました。
    某K社のシーケンサです。

    1パケットに収まらすに2個以上のパケットに分割されています。
    なぜ受信しないのか、理由がわかりました。ありがとうございます。

    現在は、指摘事項を直し、修正いたしましたが、異常の数は減りましたが
    まだ稀に1回ほど、おかしく受信いたします。

    記載された判定すべて試し、データの数を1000⇒500に減らし、1パケットで受信できるようにしました。
    ですが、まだ判定が足りないようです。

    教えていただいた、'正規表現で数字とスペースのみかどうかをチェックですが
    どうしても最後にデバックモードでみるとバイナリ:2Eが入っていて
    とることが出来なく、従来の数値チェックにしました。すみません。

    また指摘されたTcpClientを開きっぱなしで使い続けた場合、前回に読み残したデータが混ざるということを
    考慮し、1回のReadごとにOpenClose処理を入れました。

    下記に、ソースの一部抜粋をいたしました。

    間違って、受信したデータの判定に何が足りないか
    ご教授いただけると助かります。

    #Region "   --- MRデータ Read 処理(M:内部リレー)ビット数:500 ---"
        Public Sub MR_Read(ByVal MR_num As Integer)

            ' 送信するデータを作成
            Dim ascii As Encoding = Encoding.ASCII 'ASCIIに変更いたしました。

           
            ' データ受信
            Dim memoryStream As New MemoryStream()
            Dim resBytes As Byte() = New Byte(1001) {}
            Dim resSize As Integer = 0


            Select Case stream.CanWrite
                Case True
                    'コマンド
                    command = String.Format("RDS MR{0} {1}", MR_num, 500)
                    Dim sendBytes As Byte() = ascii.GetBytes(command & ControlChars.CrLf)
                    'データ送信
                    strWriter.Write(sendBytes, 0, sendBytes.Length) ' データ送信

                    Select Case stream.CanRead
                        Case True
                            Do
                                resSize = strReader.Read(resBytes, 0, resBytes.Length)

                                If resSize <> 1001 Then
                                    _Err_Read = True
                                    Exit Do
                                End If

                                ' サーバから切断された
                                If resSize = 0 Then
                                    _Err_Read = True
                                    Exit Do
                                End If

                                memoryStream.Write(resBytes, 0, resSize)
                            Loop While resBytes(1000) <> 10 ’最後の値がLF バイナリでは10

                        Case False
                            _Err_Read = True
                    End Select
                Case False
                    _Err_Read = True
            End Select


            If (memoryStream.Position <> 1001) Then
                _Err_Read = True '正しく受信できなかった
            Else
                memoryStream.Position = memoryStream.Position - 1
                memoryStream.Read(resBytes, 0, 1)

                If resBytes(1000) = 10 Then '最後に格納されたのがLFではないなら読み出し失敗
                    _Err_Read = False
                Else
                    _Err_Read = True
                End If
            End If


            Select Case _Err_Read

                Case False '正常時

                    ' 受け取ったデータを加工
                    Dim resMsg As String = ascii.GetString(memoryStream.GetBuffer(), 0, CInt(memoryStream.Length))
                    resMsg = resMsg.TrimEnd(vbCr, vbLf, vbCrLf) '末尾のCR,LFは捨てる


                    Dim parts As String() = resMsg.Split(" "c)
                    If (parts.Count <> 500) Then
                        _Err_Read = True '数が合わない
                    Else

                        Dim i As Integer = 0
                        '数値判定Flag:初期化
                        _flag = True

                        'PCのPLCデータ配列に入れる   
                        For PLC_check As Integer = 0 To 498
                            For Each _c In parts(PLC_check)
                                '数字以外の文字が含まれているか調べる
                                If _c < "0"c OrElse "9"c < _c Then
                                    _flag = False
                                    Exit For
                                End If
                            Next
                        Next

                        '文字判定()
                        Select Case _flag
                            Case True '数値である
                                '16進⇒10進
                                Convert16to10(MR_num)

                                i = 0
                                If _Err_Read = False Then
                                    'PCのPLCデータ配列に入れる
                                    For PLC_input As Integer = 0 To 498
                                        '16進数を10進に直した配列に入れる。
                                        PLC_Data.MR_Array_Data_Set(_cv16to10) = Long.Parse(parts(PLC_input))
                                        _cv16to10 += 1
                                    Next

                                End If

                            Case False '数値でない
                                '_Word_Data = 999
                        End Select

                    End If

                Case True '異常時
                    _Err_Read = False
            End Select
            memoryStream.Dispose()
        End Sub
    #End Region

    2016年3月8日 8:21
  • 1回のReadで読み取ったバイト数が指定以外になった場合にエラーとしているようですが、Readした時点ですべて揃っている保証はありません。1回の読み取りではすべてを読み取れない前提で処理するようにしましょう。
    そのためには、Readの返り値が0になるかMemoryStreamに入れたバイト数が指定長さになったことで確認するようにしましょう。

    > 最後にデバックモードでみるとバイナリ:2E

    末尾にあるのであれば、TrimEnd(vbCr, vbLf, Chr(&H2E))で除去できます。
    それ以前に、K社からダウンロードしたマニュアルによれば&H2Eが入っていること自体が異常な気がします。(機種が判らないのでもしかしたら正しいのかもしれませんが)
    想定された値ではないのであれば、WireSharkなどをつかって実際に流れているパケットを確認するか、あるいはK社に確認した方がいいでしょう。

    あと、エラーとしてBoolean型の_Err_Readという変数でしかエラーを確認できないようになっていますが、せめてEnum型にしてどのような原因でエラーになったのかが判るようにしましょう。
    そうでないとどこまで正しくできているのかが判りません。
    また、稀におかしいのというのがどの程度の頻度を指しているのか不明ですが、TCPではCRCでチェックされていても100%完璧ではありません。正しくなさそうなデータが来たのであれば再度コマンドを送って再取得するなどして対応しましょう。

    処理は実機に合わせて実装する必要がありますが、私には実機がないので実機がどのような挙動をするのか知るすべがありません。ですから細かい挙動の調査についてはご自身で行っていただくしかありません。
    とはいえ、K社はサポートをよくしてくれるので、マニュアルとは違う挙動をしているのであればK社に確認した方がいいでしょう。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2016年3月8日 14:36
  • ありがとうございます。
    指摘事項を考慮し、Readの返り値が0の時を判定に使いました。
    以下のようにソースを変更いたしました。

              While (resSize = strReader.Read(resBytes, 0, resBytes.Length)) <> 0
                                If resSize <> 1000 Then
                                    _Err_Read = True
                                    Exit While
                                End If

                                ' サーバから切断された
                                If resSize = 0 Then
                                    _Err_Read = True
                                    Exit While
                                End If

                                memoryStream.Write(resBytes, 0, resBytes.Length)
                            End While
                            memoryStream.Write(resBytes, 0, resBytes.Length)

    上記処理を変更し、ご指摘いただいた、どのような原因でエラーになったのか。

    原因がどこで値が正しく受信できなくなるか、処理を区切り
    デバックモードで値を確認しながら、原因を探りました。

    PLCの値を変化させず、受信時に前の値と、現在の値が異なることがあれば、デバックのブレイクポイントに入るようにし、
    1つ1つチェックいたしました。

    その結果、値は正しく来ているのに
    文字コード変換が間にあわないということが判りました。

    ありがとうございます。

    Dim resMsg As String = ascii.GetString(memoryStream.GetBuffer(), 0, CInt(memoryStream.Length))

    ↑の処理が、間にあわなく変換できない文字は、異常になることがわかりました。
    ASCIIEncoding.GetStringは、16 進数 0x7F を超えるバイトは、Unicode 疑問符 ("?") にデコードされます。
    と書かれていたので、100個ごとにデータを区切り変換させたところ
    正しく処理ができました。

    ただ、100個ごとに区切る方法がわからなく、単純に以下のように
    配列をつくりFor文でデータを入れてデータを分けました。

             For i As Integer = 0 To 99
                        A1_byte(i) = resBytes(i)
                    Next

    最後に、配列にデータを入れる時、処理が間にあわなく、正しく配列に入らない現象があります。
    ご教授いただけると助かります。

    入れ方は以下のように、単純な方法です。

                                If _Err_Read = False Then
                                    'PCのPLCデータ配列に入れる
                                    For i As Integer = 0 To 499
                                        '16進数を10進に直した配列に入れる。
                                        PLC_Data.MR_Array_Data_Set(_cv16to10) = Long.Parse(parts(i))
                                        _cv16to10 += 1
                                        i += 1
                                    Next

                                End If

    2016年3月9日 8:29
  • ↑の処理が、間にあわなく変換できない文字は、異常になることがわかりました。
    ASCIIEncoding.GetStringは、16 進数 0x7F を超えるバイトは、Unicode 疑問符 ("?") にデコードされます。

    ASCII文字は7bitの範囲で表現されるために、&H00~&H7FまではASCII文字になるが&H80~&HFFまでは範囲外という意味であって、文字列の長さが128文字以上という意味ではありません。

    配列にデータを入れる時、処理が間にあわなく、正しく配列に入らない現象があります。

    間に合わないというのがシーケンサー自体の通信処理が遅いので間に合わないのか、他のことを指しているのかわからないです。
    とはいえ、おそらくBackgroundWorkerを使ってマルチスレッドになっているのに、同期処理をせずに変数の読み書きしているせいで、メインスレッドと処理がそろっていないだけなのではないかと思われます。
    #メインスレッドで読み書きする変数を、同時に別スレッドでも読み書きしたら、壊れた結果になってしまいます。

    まずは通信処理だけに専念するためにBackgroundWorkerを使わずにメインスレッドで一回だけの読み出し処理を行わせて正しく処理できるようにすることから始めましょう。
    その次には、メインスレッドのままでループさせて何度も読み出す処理の確認をしましょう。
    それらが正常に動くようになってからBackgroundWorkerなどのマルチスレッドに処理を移しましょう。(スレッドの同期をよく読んで、、lockステートメントを使って変数が同時に読み書きされないように保護しましょう。)


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2016年3月9日 14:18
  • PLCとやり取りしている処理は、backgroundworkerの1スレッドのみです。
    内部リレーの特定アドレスを見て、値を1にしてデバックモードで変化があったときデバックモードが起動するようにし、
    受信時ではなく、変換時でもなく、PC側で管理しているPLCの配列に値を入れるとき
    1であるはずの値が0の値で、入りました。

    backgroundworkerのdoworkで PLCとデータ通信する処理を書くのは間違いでしょうか。
    ご教授いただけると助かります。

    2016年3月9日 14:49
  • ご返信、ありがとうございます。

    色々と発想と考え方を変えて、処理を変えてみました。
    バイナリーデータでデータを取得しているので、生のデータを活用し、内部リレーは0か1しかないので
    Ascコード変換を掛けず
    'ascii 48→0
    'ascii 49→1
    であるから、それを利用し、それ以外はすべてエラーとして判定したところ
    上手に処理ができました。

    ワードデータも
    アスキーコード変換をselect文で作り、処理がうまくいきました。

    現在のところ、誤動作しないで処理が出来ています。
    色々と教えていただき、ありがとうございます。

    2016年3月10日 7:51