C#でソケット(socket)を使って非同期でプログラムを実装することがあったのですが、ソケットについて理解が不十分で苦戦したことがありました。
ソケットについて記事にまとめてあれば、スムーズに理解が進むと思うので、いろいろと調べたことを整理して記事にまとめてみました。
ソケットを使うことで TCP/IP 通信による通信ができるので、この TCP/IP についても理解をしておく必要があります。
ソケットを使って TCP/IP 通信を実装する方法をこの記事では紹介していますので、これから始める方には特にオススメの記事となりますので参考にしてみてください。
オススメの参考書
C#の使い方が丁寧に解説しており、「基礎からしっかりと学びたい」という初心者の方にオススメの一冊です。サンプルコードも記載してあり、各章の最後に復習問題があるので理解度を確認しながら読み進めることができます。新しい C# のバージョンにも対応している書籍です。
TCP/IP通信について
まずは TCP/IP についておさらいしましょう。
TCP/IPとは
TCP/IP はインターネット・プロトコル・スイートとも呼ばれ、現在でも世界標準で利用されている通信規格です。TCP/IP は機器や OS が異なっていても共通のプロトコルを用いて通信を成立させることができます。
例えば、パソコンとパソコン同士だけでなく、産業機器で使用される PLC(プログラマブルコントローラー)と言った異なる機器同士で通信することができます。
特徴として、通信相手を確認して接続をしてから通信をするため、確実にデータを送受信することができます。確実にデータの送受信ができることから通信品質が高く、現在でも使用されている通信手段の1つです。
- これから通信を始めることを確認した後で通信を開始する方式である
- 通信を開始する時点から通信相手に届くことが保証されている
- 通信終了時も、通信相手との間で通信を終了することを確認してから終える
この通信プロトコルをプログラムで利用するにはソケットを使って通信をします。
ソケットとは
ソケットは、TCP/IP をプログラムから利用する際に使用され、プログラムとネットワークを結びつける為のエンドポイント(通信の出入口)のことです。
エンドポイントは、IP アドレスとポート番号を指定して識別します。この中でポート番号の指定が特に重要です。
通信相手の IP アドレスが分かれば、その IP アドレスにデータを送信することまではできますが、そのデータをどこに受け渡したらいいのか相手は判断ができません。
ポート番号があることで、通信を行うアプリケーションを特定することができ、データの受け渡しができます。
また、ポート番号は16ビットの数値なので、0~65535(0xFFFF)の範囲内で指定されます。
ポート番号のタイプ | ポート番号の範囲 | 意味 |
---|---|---|
ウェルノウンポート | 0~1023 | サーバーアプリケーション用に予約されているポート番号 (例:HTTPなら80、FTPなら20/21) |
登録済みポート | 1024~49151 | よく利用されるアプリケーションのサーバー側ポート番号 |
ダイナミック 又は プライベートポート | 49152~65535 | クライアントアプリケーション用のポート番号 |
通信処理の流れ
上記の「TCP/IPとは」で説明した内容をフローチャートにすると以下になります。
この通信フローの順番でプログラムの処理を書くことになります。
今回のサンプルコードは、サーバーとクライアント間で接続をして、クライアントから「Hello」を送信し、サーバーから送信される「OK」を受信するプログラムを作成します。通信には非同期なソケット通信を使用します。
クライアント側のソケット通信処理
クライアントは以下のような関数を用意します。上図のフローチャートと比較しながら関数の役割を確認してみてください。
関数 | 役割 |
---|---|
StartClient | 指定したIPアドレスとポート番号をでソケットを作成する。サーバーの接続、データの送信、データの受信を非同期で要求を行う。 |
ConnectCallback | 非同期でサーバーとソケットで接続を確立する。 |
SendCallback | 非同期でサーバーへデータの送信を行う。 |
ReceiveCallback | 非同期でサーバーからデータを受信し、受信したデータを蓄積する。蓄積データに終了タブ<EOF>があれば、受信を終了する。 |
サンプルソースコード
「tcpclient.cs」という新しい項目(ファイル)を作成して、プロジェクトに追加します。
ソケット関連で使用する名前空間 System.Net.Sockets と System.Net を参照するために、using ディレクティブを用いてコードの先頭に記述します。
using System;
using System.Net.Sockets;
using System.Net;
StartClient関数
まず IP アドレスとポート番号を指定して、エンドポイントを設定します。IP アドレスとポート番号は引数で渡すことで変更できるようにしました。
この関数には接続処理の呼び出し、データ送信の呼び出し、データ受信の呼び出し処理が含まれています。
まず、設定したエンドポイントで TCP/IP のソケットを作成します。TcpClient の接続処理を非同期にするために、BeginConnect
を用いて実装します。第1引数にはエンドポイント、第2引数には接続が確立された時に実行されるメソッド、第3引数には、ソケットを指定します。
WaitOne()
はシグナルを受け取るまでスレッドをブロックして待機し続けます。例えば、非同期処理ConnectCallback
が実行されると、connectDone
がシグナル状態になるので、次の処理(データの送信)が開始されます。これにより、接続→データ送信→データ受信の順番に処理が行われるようになります。
シグナル状態になった後は、再利用するために必ず Reset() を呼び出しします。
// マニュアルリセットイベントのインスタンスを生成
private static ManualResetEvent connectDone = new ManualResetEvent(false); //接続シグナル用
private static ManualResetEvent sendDone = new ManualResetEvent(false); //送信シグナル用
private static ManualResetEvent receiveDone = new ManualResetEvent(false); //受信シグナル用
// 受信データのレスポンス
private static string response = string.Empty;
public string StartClient(string ipaddress, int port, string data)
{
response = string.Empty;
//シグナルをリセット
connectDone.Reset();
sendDone.Reset();
receiveDone.Reset();
// サーバーへ接続
try
{
// IPアドレスとポート番号を取得
IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(ipaddress), port);
// TCP/IPのソケットを作成
Socket client = new Socket(IPAddress.Parse(ipaddress).AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// エンドポイント(IPアドレスとポート)へ接続
client.BeginConnect(endpoint, new AsyncCallback(ConnectCallback), client);
connectDone.WaitOne(); //接続シグナルになるまで待機
// ASCIIエンコーディングで送信データをバイトの配列に変換
byte[] byteData = Encoding.ASCII.GetBytes(data + "<EOF>");
// サーバーへデータを送信
client.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), client);
sendDone.WaitOne(); //送信シグナルになるまで待機
// ソケット情報を保持する為のオブジェクトを生成
StateObject state = new StateObject();
state.workSocket = client;
// サーバーからデータ受信
client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
receiveDone.WaitOne(); //受信シグナルになるまで待機
// ソケット接続終了
client.Shutdown(SocketShutdown.Both);
client.Close();
Debug.WriteLine("接続終了");
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
}
return response;
}
ConnectCallback関数
サーバーへの接続が完了すると、この関数が呼び出しされます。EndConnect
を用いて非同期の接続処理を終了します。
connectDone
をSet()
して、シグナル状態にすることでメインスレッドの処理を続行します。
private static void ConnectCallback(IAsyncResult ar)
{
try
{
// ソケットを取得
Socket client = (Socket)ar.AsyncState;
// 非同期接続を終了
client.EndConnect(ar);
Debug.WriteLine("接続完了");
// シグナル状態にし、メインスレッドの処理を続行する
connectDone.Set();
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
}
}
SendCallback関数
サーバーへデータの送信が完了すると、この関数が呼び出しされます。EndSend
を用いて非同期の送信処理を終了します。
sendDone
をSet()
して、シグナル状態にすることでメインスレッドの処理を続行します。
private static void SendCallback(IAsyncResult ar)
{
try
{
// ソケットを取得
Socket client = (Socket)ar.AsyncState;
// 非同期送信を終了
int bytesSent = client.EndSend(ar);
Debug.WriteLine("送信完了");
// シグナル状態にし、メインスレッドの処理を続行する
sendDone.Set();
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
}
}
ReceiveCallback関数
サーバーからデータを受信すると、この関数が呼び出しされます。EndReceive
を用いて非同期の受信処理を終了します。戻り値には受信されたバイト数が返されます。
このバイト数をチェックして残りバイト数が0以上なら、未受信のバイトがあることになります。受信データを蓄積し、BeginReceive
を用いて受信処理を再開します。
残りバイト数が0なら、受信完了したことになりますので、receiveDone
をSet()
して、シグナル状態にすることでメインスレッドの処理を続行します。
private static void ReceiveCallback(IAsyncResult ar)
{
try
{
// ソケット情報を保持する為のオブジェクトから情報取得
StateObject state = (StateObject)ar.AsyncState;
Socket client = state.workSocket;
// 非同期受信を終了
int bytesRead = client.EndReceive(ar);
if (bytesRead > 0)
{
// 受信したデータを蓄積
state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));
// 受信処理再開(まだ受信しているデータがあるため)
client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
}
else
{
// 受信完了
if (state.sb.Length > 1)
{
response = state.sb.ToString();
Debug.WriteLine("サーバーから「{0}」を受信", response);
}
// シグナル状態にし、メインスレッドの処理を続行する
receiveDone.Set();
}
}
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でクライアントのアプリケーションを作成してみます。
IP アドレスとポート番号、送信するデータを入力欄に入力して、送信開始ボタンをクリックすれば、サーバーと接続されるまで待機します。画面はマテリアルデザインを適用させてオシャレな画面に仕上げました。
マテリアルデザインの適用方法については、以下の記事で詳しく記載しています。
XAML のソースは以下の記述になります。
<Window
x:Class="TcpClinet.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="370"
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通信のクライアント側のサンプル画面です。" />
<!-- IPアドレス入力欄 -->
<Label
x:Name="lblIpaddress"
Margin="15,99,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="IP アドレス:" />
<TextBox
x:Name="txtIpaddress"
Width="280"
Margin="90,86,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
materialDesign:HintAssist.Hint="IPアドレスを入力してください。"
Style="{StaticResource MaterialDesignFloatingHintTextBox}" />
<!-- ポート番号入力欄 -->
<Label
x:Name="lblPort"
Margin="15,141,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="ポート番号:" />
<TextBox
x:Name="txtPort"
Width="280"
Margin="90,128,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
materialDesign:HintAssist.Hint="ポート番号を入力してください。"
Style="{StaticResource MaterialDesignFloatingHintTextBox}" />
<!-- 送信するデータ入力欄 -->
<Label
x:Name="lblSendData"
Margin="15,183,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="送信データ:" />
<TextBox
x:Name="txtSendData"
Width="280"
Margin="90,170,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="Hello"
materialDesign:HintAssist.Hint="送信するデータを入力してください。"
Style="{StaticResource MaterialDesignFloatingHintTextBox}" />
<!-- 送信開始ボタン -->
<Button
x:Name="btnStart"
Width="90"
Height="30"
Margin="0,177,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Click="BtnStart_Click"
Content="送信開始"
Foreground="Black"
Style="{StaticResource MaterialDesignOutlinedButton}" />
<!-- ログ出力欄 -->
<TextBox
x:Name="txtLog"
Margin="10,215,10,10"
AcceptsReturn="True"
IsEnabled="{Binding ElementName=MaterialDesignOutlinedTextBoxEnabledComboBox}"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</Grid>
</Window>
コードは以下の記述になります。
using System.Windows;
namespace TcpClinet
{
public partial class MainWindow : Window
{
tcpclinet tcp = new tcpclinet();
public MainWindow()
{
InitializeComponent();
}
private void BtnStart_Click(object sender, RoutedEventArgs e)
{
btnStart.IsEnabled = false;
var responce = tcp.StartClient(txtIpaddress.Text, int.Parse(txtPort.Text), txtSendData.Text);
MessageBox.Show(responce);
btnStart.IsEnabled = true;
}
}
}
動作確認
サーバー側とクライアント側のアプリケーションをそれぞれ起動します。
クライアントアプリで IP アドレスとポート番号を入力してから送信ボタンをクリックしてみましょう。
ログの結果からTCP/IP通信でデータの送受信ができていることが確認できました。
まとめ
この記事ではクライアント側の TCP/IP 通信について記載しました。TCP/IP 通信はよく使用する通信方式なので、参考になれば幸いです。
以上、最後まで読んでいただきありがとうございました。