画像ファイル
画像ファイル  C#、VB2005 でSocket通信
Socket通信
1C#、VB2005 でSocket通信
2サーバー 複数接続

VB.NET C#全般
1羊の皮を着た狼 VB.NET
2Form1、Form2の相互参照
3Form1、Form2の相互参照 2
4VB.NET C# データ型の基本
5VB.NET C# 文字列
6VB.NET タイマー精度
7BackgroundWorkerの魅力1..
8BackgroundWorkerの魅力2..
9VB6のタイマー
10コントロールの配列をインデクサ..
11コントロールの配列はジェネリク..
12インデクサ(C#、VB.NET)
13インデクサでBit操作
14Unicode 入門
15デリゲート入門
16マルチスレッド入門
17イベント入門
18デリゲートとイベント
18インターフェースの基本

RichTextBox関係
1RichTextBoxの不思議
2テキスト色付け高速化計画
3VB.NET RichTextBox1
4VB.NET RichTextBox 2

RS-232C関係
1RS-232Cの基礎
2RS-232Cの何が変わった..
3SerialPortクラス
4Unicode(ユニコード)の壁
5マルチスレッドの壁
6RS-232C サンプルコード
7RS-232CのHEXモニタ
8RS-232C 送信モジュール
9RS-232Cのループテスト
10RS-232Cのピンチェンジ..

プロセス間通信
1プロセス間通信(送信側)
2プロセス間通信(受信側)

質問、意見はこちらに
画像ファイル

ブログ 本だけ持って
画像ファイル


C#、VB2005のソケット通信はVB6程簡単ではない
VB6ではWinSockと言うコントロールがあり、ソケット通信がいとも簡単に行なえた。
VB2005、C#ではいかがであろうか?
実はVB2005にもC#にもWinSockの相当するコントロールは無いので有る。
つまりVB2005やC#でソケット通信を行なおうとすると、それ相応の覚悟がいるので有る。
今回は、ホストにもクライアントにもなるソケット通信のプログラムを作ってみよう。

仕様
さて仕様であるが、下の画像を御覧頂きたい。
サーバー(ホスト)とクライアントをボタンで切り替え、スターとボタンを押すと、通信モードとなる。
もちろん相手の切断にも柔軟に対応する。
更に切断エラーなどのログを表示する。

画像ファイル

Framework2.0 Socketクラス
今回はFramework Socketクラスの中のTcpListenerTcpClientを使用する。
TcpListenerはサーバーで使用してクライアントからの接続を確立し、 streamを作成して、送られてきたデータを読み込む。
ユニコード
ここでも又「ユニコードの壁」が有る、従来のSoket通信で使われた文字は、 Sift-Jisが多いと思われる。
Frameworkは全てUnicode(utf-32)である。
ここでは、送信は全てUnicode(utf-32)からSift-Jisに変換して送信する。
又受信は全てSift-Jisが来るものと仮定して、Sift-JisからUnicode(utf-32)に変換して 表示するものとする。
もちろんこのプログラムどうしの送受信場合はFrameworkのUnicodeどうしとなり変換の必要は無いが、 一方が従来のSocketでも正常に受信出来る事を考えてShift-Jisどうしの送受信とする。
マルチスレッド
他のサンプルコードなどを見てTcpListenerTcpClientはポーリングでデータを受信すると思われる方も居るかもしれないが、実はポーリングでは無い。
確かにデータを受信する部分はLoopの中に有るが、あるコードのところでデータが受信するまで 待機しているのである。
従って文字が送られてこない限りCPUの負荷にはならないので有るから安心しても良い。
ただしデータを検知するまではスレッドを止めて(スレッドが固まって)待機するのであるから、 コンソールプログラム以外はマルチスレッドで作らなければならないのである。
すなわちサーバー側もクライアント側も受信は別スレッドで行なうことになる。
マルチスレッドに関して自信の無い方は 「マルチスレッド入門」 を参照されたし。
それではコードを見ていこう。

コードの説明
クラスの参照の部分を下に示す。
参照
C#のコード
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;

VB2005のコード
Option Strict On
Imports System.Text
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading


続いてクラス共通の部分である。
宣言
C#のコード
//***********************************************************
//初期設定宣言
//***********************************************************
//受信は必ずshift-jisと仮定している
//他の文字の場合はここを変える事
Encoding ecUni = Encoding.GetEncoding("utf-16");
Encoding ecSjis = Encoding.GetEncoding("shift-jis");

//サーバー設定
TcpClient server = null;
//サーバーのリスナー設定
TcpListener listener = null;
//サーバーのセカンドスレッドの設定
Thread threadServer = null;

//クライアント設定
TcpClient client = null;
//クライアントのセカンドスレッドの設定
Thread threadClient = null;


//**********************************************************
//デリゲート宣言
//**********************************************************
//別スレッドからメインスレッドのテキストボックスに書き込むデリゲート
delegate void dlgWriteText(string text);

//引数を持たない汎用のデリゲート
//ストップボタンを押す等に使用
delegate void dlgMydelegate();

VB2005のコード
    ' //***********************************************************
' //初期設定宣言
' //***********************************************************
' //受信は必ずshift-jisと仮定している
' //他の文字の場合はここを変える事
Dim ecUni As Encoding = Encoding.GetEncoding("utf-16")
Dim ecSjis As Encoding = Encoding.GetEncoding("shift-jis")

'//サーバー設定
Dim server As TcpClient = Nothing
'//サーバーのリスナー設定
Dim listener As TcpListener = Nothing
'//サーバーのセカンドスレッドの設定
Dim threadServer As Thread = Nothing

'//クライアント設定
Dim client As TcpClient = Nothing
'//クライアントのセカンドスレッドの設定
Dim threadClient As Thread = Nothing
System.Text.EncodingクラスでSift-JisとEuc-16(Unicode)のインスタンスを2つ作って、 受送信時の変換を行なう。
又TcpClientクラスのサーバーとクライアントのインスタンスとセカンドスレッドのインスタンスを作る。
サーバーとクライアントの違いはサーバーはクライアントが接続要求をしてきた時に接続を作成する、 TcpListenerのインスタンスを持つことである。
listener.Start();としておくと、サーバーはここでクライアントの接続要求を待つことになる。
いずれにしろこれらはセカンドスレッドで行なわれる。
C#のコード
delegate void dlgWriteText(string text);
VB2005のコード
Delegate Sub dlgWriteText(ByVal text As String)
デリゲートはセカンドスレッドからメインの受信のテキストボックスやLogファイルの書き出しのテキストボックスへの 書き込み用である。
2つ目のデリゲートは汎用であり(デリゲートは普通汎用であるが)引数が無いデリゲートは これを使用する。
実際は別スレッドからメインスレッドのボタン等を操作している。

スタートボタン押下
C#のコード
//ソケット通信開始
private void StartSock()
{
//チェックボックスを見て sub
//サーバー又はクライアントスタート
if (radioServer.Checked)
{
ServerStart();
picIndicator.BackColor = Color.Green;
}
else
{
ClientStart();
picIndicator.BackColor = Color.LightGreen;
}
//ボタンのenableを変える
butStart.Enabled = false;
butStop.Enabled = true;
}

VB2005のコード
    '//ソケット通信開始
Private Sub StartSock()
'//チェックボックスを見て
'//サーバー又はクライアントスタート
If radioServer.Checked Then
ServerStart()
picIndicator.BackColor = Color.Green
Else
ClientStart()
picIndicator.BackColor = Color.LightGreen
End If
'//ボタンのenableを変える
butStart.Enabled = False
butStop.Enabled = True
End Sub
スタートボタンを押すとStartSock()が呼び出されて、ラジオボタンのサーバーとクライアントの選択に 分かれて処理を開始する。
ただしサーバーは先ほど書いたようにlistener.Start()でクライアントの接続を待つことになる。
接続要求がクライアントから来るとサーバーは接続を作成して、セカンドスレッドをつくり、 其の中でデータ受信を待つのである。
下はセカンドスレッドを作成して、実行するコードで有る。

セカンドスレッドの作成とサーバースタート
C#のコード
//***********************************************************
//セカンドスレッドの作成とサーバーのスタート
//***********************************************************
private void ServerStart()
{
//TcpListenerを使用してサーバーの接続の確立
try
{
//lisenerが無い場合
if (listener == null)
listener = new TcpListener(IPAddress.Any, int.Parse(textBoxPortNo.Text));

//クライアント接続要求の受付開始
listener.Start();

//ログの書き込み
writeLog("サーバを開始しました。");

//スレッドの作成と開始
threadServer = new Thread(new ThreadStart(ServerListen));
threadServer.Start();

}
catch (Exception ex)
{
//エラーが起きた
writeLog("サーバ接続エラー:" + ex.Message.ToString());
listener.Stop();
picIndicator.BackColor = Color.Navy;
}
}

VB2005のコード
    '//***********************************************************
'//セカンドスレッドの作成とサーバーのスタート
'//***********************************************************
Private Sub ServerStart()
'//TcpListenerを使用してサーバーの接続の確立
Try

'//lisenerが無い場合
If listener Is Nothing Then
listener = New TcpListener(IPAddress.Any, _
Int32.Parse(textBoxPortNo.Text))
End If
'//クライアント接続要求の受付開始
listener.Start()

'//ログの書き込み
writeLog("サーバを開始しました。")

'//スレッドの作成と開始
threadServer = New Thread(New ThreadStart(AddressOf ServerListen))
threadServer.Start()

Catch ex As Exception
'//エラーが起きた
writeLog("サーバ接続エラー:" + ex.Message.ToString())
listener.Stop()
picIndicator.BackColor = Color.Navy
End Try
End Sub

下のコードは最も肝心な受信の部分である、長くなるのでLoopの部分は省略して別に説明する。
接続を待つ
C#のコード
//***********************************************************
//別スレッドで実行されるサーバ側の処理
//***********************************************************
private void ServerListen()
{

if (server == null)
return;
//クライアントの要求があったら、接続を確立する
//クライアントの要求が有るまでここで待機する
server = listener.AcceptTcpClient();

//ログの書き込みをデリゲートで実行
this.textBoxLog.BeginInvoke(new dlgWriteText(writeLog)
, new object[] { "クライアントが接続しました。" });

//インジケータの色を変える
//バックカラーの変更はデリゲートの必要は無し
picIndicator.BackColor = Color.LightGreen;

//クライアントとの間の通信に使用するストリームを取得
NetworkStream stream = server.GetStream();

//受信文字が入る。
//この数字は少なくても可、少ない場合はLoopが何回か実行される
Byte[] bytes = new Byte[1000];

//受信文字を書き込むデリゲートを作成。
dlgWriteText dlgText = new dlgWriteText(WriteReadText );
while (true)
{
// 別に表示 }
}

VB2005のコード

'//***********************************************************
'//別スレッドで実行されるサーバ側の処理
'//***********************************************************
Private Sub ServerListen()


If server Is Nothing Then
Return
End If
'//クライアントの要求があったら、接続を確立する
'//クライアントの要求が有るまでここで待機する
server = listener.AcceptTcpClient()

'//ログの書き込みをデリゲートで実行
Me.textBoxLog.BeginInvoke(New dlgWriteText(AddressOf writeLog) _
, New Object() {"クライアントが接続しました。"})

'//インジケータの色を変える
'//バックカラーの変更はデリゲートの必要は無し
picIndicator.BackColor = Color.LightGreen

'//クライアントとの間の通信に使用するストリームを取得
Dim stream As NetworkStream = server.GetStream()

'//受信文字が入る。
'//この数字は少なくても可、少ない場合はLoopが何回か実行される
Dim bytes(1000) As Byte

'//受信文字を書き込むデリゲートを作成。
Dim dlgText As dlgWriteText = New dlgWriteText(AddressOf WriteReadText)

While (True)

'別に表示
End While
End Sub
何回も書いているが上のコードは別スレッド上で行なわれているもので、 この中からメインスレッドのテキストボックス等に書き込む場合はデリゲートでアクセスする必要が有る。
各コードの説明は、コードの中に出来るだけ詳しく説明しているのでそれを御覧頂きたいのであるが、 ここでは注意点だけ説明しよう。
C#のコード
Byte[] bytes = new Byte[1000];
VB2005のコード
Dim bytes(1000) As Byte
これは受信文字をstreamから取得するバッファを確保しているのだが、このバッファが所得文字より 小さい場合はどうなるのであろうか?
実はこの時にWhileをぐるぐる回り全ての文字を取得することになる。
つまり、バッファは適当な大きさで良いことになる。
受信
C#のコード
while (true)
{
try
{
//受信が無い場合はここで待機する
//文字受信が有った場合とクライアントが接続を切った場合に
//次のステップに進む
int intCount = stream.Read(bytes, 0, bytes.Length);

if (intCount != 0)
{
//intStrCountが0より大きい場合は文字受信有り

//受信部分だけ切り出す
Byte[] getByte=new byte[intCount];
for (int i = 0; i < intCount; i++)
getByte[i] = bytes[i];

byte[] uniBytes;
//'S-Jisからユニコードに変換
uniBytes = Encoding.Convert(ecSjis, ecUni, getByte);

//バイト配列から文字列に変換する
string strGetText = ecUni.GetString(uniBytes);
//受信文字を切り出す

//メインスレッドのテキストボックスに書き込む
textBoxWrite.Invoke(dlgText, strGetText);
}
else
{
//intStrCount=0の場合はクライアントが接続を切った
//デリゲートからログを書き込む
this.textBoxLog.BeginInvoke(new dlgWriteText(writeLog)
, new object[] { "クライアントが切断されました。" });

//サーバの切断と再接続がタイマーを使用して行なわれる
//タイマーはデリゲートで呼ばれる
this.BeginInvoke(new dlgMydelegate(setStartTimer)
, new object[] { });

//ループを抜ける
return;
}

}
catch (System.Threading.ThreadAbortException)
{
//Stopボタンを押すとこのスレッドが削除されこのエラーが起きる。
//それを回避する為のエラー処理。
//切断されたのでループを抜ける
return;
}
catch (Exception ex)
{
//切断で起こるスレッドエラー以外のエラーはここに来る
this.textBoxLog.BeginInvoke(new dlgWriteText(writeLog)
, new object[] { "受信エラー  " + ex.Message.ToString() });
this.butStart.BeginInvoke(new dlgMydelegate(StartSock ),
new object[] { });
//ループを抜ける
return;
}
}

VB2005のコード
        While (True)
Try
Dim intCount As Int32 = stream.Read(bytes, 0, bytes.Length)
If intCount <> 0 Then

'//受信部分だけ切り出す
Dim getByte(intCount - 1) As Byte
For i As Integer = 0 To intCount - 1
getByte(i) = bytes(i)
Next
Dim uniBytes() As Byte
'//'S-Jisからユニコードに変換
uniBytes = Encoding.Convert(ecSjis, ecUni, bytes)
Dim strGetText As String = ecUni.GetString(uniBytes)
textBoxWrite.Invoke(dlgText, strGetText)
Else
'//サーバーが切断された
Me.textBoxLog.BeginInvoke(New dlgWriteText(AddressOf writeLog) _
, New Object() {"ホストが切断されました。"})
Me.butStart.BeginInvoke(New dlgMydelegate(AddressOf StopSock) _
, New Object() {})
Return
End If
Catch e As ThreadAbortException
'//何もしません
Return

Catch ex As Exception
Me.textBoxLog.BeginInvoke(New dlgWriteText(AddressOf writeLog) _
, New Object() {"受信エラー  " + ex.ToString()})
Return
End Try
End While
さてループの説明であるが、 肝心なのは下のコードである、このループは文字の受信が来るまでひたすら、下のコードの場所で 待つのである。
文字が受信できると、このメソドは受信文字のバイト数を返して実行がそれ以降のコードに移るのである。
C#のコード
int intStrCount = stream.Read(bytes, 0, bytes.Length);

VB2005のコード
Dim intStrCount As Integer = stream.Read(bytes, 0, bytes.Length)
文字変換
受信した文字データはしたのコードでSift-JisからUnicodeに変換される。
C#、VB2005は共に文字はユニコード処理となり、Sift-Jisのままであると、テキストボックスに表示したとき 文字化けを起こすからである。
C#のコード
uniBytes = Encoding.Convert(ecSjis, ecUni, bytes);
VB2005のコード
uniBytes = Encoding.Convert(ecSjis, ecUni, bytes)
上のコードがSift-JisからUnicodeへの変換コードである。
ただしコードを変換する前に読み込んだバイト数だけ他のバイト配列に移し変える必要が有る。

さてクライアントが接続を切断した場合はどうなるのであろう。
C#のコード
int intStrCount = stream.Read(bytes, 0, bytes.Length);
VB2005のコード
Dim intStrCount As Integer = stream.Read(bytes, 0, bytes.Length)

切断と再接続
実はクライアントが接続を切った場合、上のコードは0を返して実行がその下に移るのである。
else(Else)以下に其の処理が書かれている。
実はここでちょっと苦しいことになった。
通常サーバーの場合クライアントは自由に接続を切り再接続することが出来る。
つまりelseの中で一度サーバーを閉じて再び StartSock()を呼び出す必要が有る。
しかしelse(Else)の中から StartSock()を呼び出すと、このインスタンスの中に再帰呼び出しの様に又新しいスレッドが出来てしまう。
そこで少し工夫をして、メインスレッドのタイマーを呼び出して、タイマーの中からサーバーをCloseして再度サーバーを 接続状態にした。
つまりタイマーを使って別スレッドの外から接続を一旦閉じて再度接続をしているのである。
この方法は余り美しい方法とは言えないので、考の価値は有るのだが、こんな方法も有ると言うことを 知っておいても損は無いだろう。
C#のコード
this.butStart.BeginInvoke(new dlgMydelegate(StopSock),
new object[] {});
VB2005のコード
 Me.butStart.BeginInvoke(New dlgMydelegate(AddressOf StartSock) _
, New Object() {})
さてもう一つ例外処理をしなくてはいけない、つまりボタンで(自分自身で)サーバーを切った時である。
既にクライアントが接続していると、サーバーは
C#のコード
int intStrCount = stream.Read(bytes, 0, bytes.Length);
VB2005のコード
Dim intStrCount As Integer = stream.Read(bytes, 0, bytes.Length)
ここで待っている、其のときサーバーを閉じると、スレッドを削除するコードを実行する。
当然セカンドスレッド上では例外が発生するのである。
それが「ThreadAbortException」である、セカンドスレッド上でこの例外が発生した場合は、直ちに セカンドスレッドを抜けることになる。
C#のコード
//サーバーのクローズ
private void CloseServer()
{
if (listener != null)
{
listener.Stop();
listener = null;
}
//サーバーのインスタンスが有って、接続されていたら
if (server != null && server.Connected)
server.Close();

//スレッドは必ず終了させること
if (threadServer != null)
threadServer.Abort();

//インディケータの色を変える
picIndicator.BackColor = Color.Navy;
//ログを書き込む
writeLog("サーバーが閉じられました。");

}

VB2005のコード
    '//サーバーのクローズ
Private Sub CloseServer()
listener.Stop()
listener = Nothing
'//サーバーのインスタンスが有って、接続されていたら
If server IsNot Nothing AndAlso server.Connected Then
server.Close()
End If
'//スレッドは必ず終了させること
If threadServer IsNot Nothing Then
threadServer.Abort()
End If
'//インディケータの色を変える
picIndicator.BackColor = Color.Navy
'//ログを書き込む
writeLog("サーバーが閉じられました。")
End Sub
上のコードがサーバー終了時のコードである、ラジオボタンでサーバーが選択されており、 其のときStopを押すと、serverを閉じると共にセカンドスレッドを終了させることになる。
ここで注意すべき点はVB2005とC#の言語仕様の違いである。
C#のコード
  
if (server != null && server.Connected)
C#の場合下の様に書けばserver != nullを評価してその結果がfalseならばserver.Connectedは 評価しない。
しかしながらVB2005の場合は
VB2005のコード
  
If server IsNot Nothing And  server.Connected Then
とか書くとserver IsNot NothingがFalseの場合でも server.Connectedが評価され、serverがNothingの 場合はエラーとなる。
VB2005のコード
If server IsNot Nothing AndAlso server.Connected Then
の様にAndAlsoを使用するとserver IsNot NothingがFalseの場合すなわちserverが無い場合は、それ以降は 評価されないのでAndAlsoを私用する必要が有る。
VB2005をお使いの方は注意されたし。
さてここまで来ると後は簡単である、別スレッドから呼ばれるメソドを下に示す。
別スレッドから呼ばれるメソド
C#のコード

//******************************************************
//デリゲートから呼ばれるテキストに書き込むメソド、
//*******************************************************
//受信文字をテキストボックスに書き込む
private void WriteReadText(string text)
{
//受信文字の改行は全て↓に置き換えられる
text = text.Replace("\r\n", "↓");
this.textBoxRead.AppendText(text + "\r\n");
}

//Logを書き込む
private void writeLog(string strlog)
{
this.textBoxLog.AppendText(DateTime.Now.ToString()+" "
+ strlog + "\r\n");
}

VB2005のコード
    '//******************************************************
'//デリゲートから呼ばれるテキストに書き込むメソド、
'//*******************************************************
'//受信文字をテキストボックスに書き込む
Private Sub WriteReadText(ByVal text As String)
'//受信文字の改行は全て↓に置き換えられる
text = text.Replace(Environment.NewLine, "↓")
Me.textBoxRead.AppendText(text + Environment.NewLine)
End Sub

'//Logを書き込む
Private Sub writeLog(ByVal strlog As String)
Me.textBoxLog.AppendText(DateTime.Now.ToString() + " " _
+ strlog + Environment.NewLine)
End Sub

送信
データの送信であるがこれは面倒なことは何も無い。
サーバーもクライアントもstreamを使って送信する。
C#のコード
//************************************************************
//文字データの送信
//************************************************************
//送信ボタン押下
private void butSend_Click(object sender, EventArgs e)
{
SendStringData();
}

//文字データーの送信
private void SendStringData()
{
//sift-jisに変換して送る
Byte[] data = ecSjis.GetBytes(textBoxWrite.Text);
//送信streamを作成
NetworkStream stream=null;

//サーバーとクライアントを分けて送信
if(radioClient.Checked)
stream = client.GetStream();
else
stream = server.GetStream();

//Streamを使って送信
stream.Write(data, 0, data.Length); }

VB2005のコード
    '//************************************************************
'//文字データの送信
'//************************************************************
'//送信ボタン押下
Private Sub butSend_Click( _
ByVal
sender As Object, ByVal e As EventArgs) Handles butSend.Click
SendStringData()
End Sub

'//文字データーの送信
Private Sub SendStringData()
'//sift-jisに変換して送る
Dim data() As Byte = ecSjis.GetBytes(textBoxWrite.Text)
'//送信streamを作成
Dim stream As NetworkStream = Nothing

'//サーバーとクライアントを分けて送信
If radioClient.Checked Then
stream = client.GetStream()
Else
stream = server.GetStream()
End If
'//Streamを使って送信
stream.Write(data, 0, data.Length)
End Sub

最後に
説明は以上である、ただしクライアントの受信の部分は省略した。
クライアントの受信処理はサーバーとほぼ同じでサーバーのコードの説明を読んで頂けば、 クライアントも理解して頂けるものと思われる。
以下全てのコードを表示して、プロジェクトもダウンロード出来るようにしました。


ブログ  NetworkStream.BeginRead も合わせてお読みください。
画像ファイル