C#でソケット通信を使った簡単なチャットアプリを作って動かすことで、ソケット通信について内容をまとめてみます。
また、この記事を読んで、ネットワークについてもっと勉強したいと思った方は以下の書籍がオススメです。
ネットワーク通信の内容をプログラミングのイメージにまで落として解説されているので、より具体的なイメージがつくと思います。
ソケット通信は組み込みソフトでも使われています
ソケット通信とは
概要
イーサネットを通してデータを送受信するための手段の1つです。
よく耳にするのは、Webブラウザで使用されているHTTP、HTTPS通信のような高レベルなものがあると思いますが、ソケット通信はそれらの土台となる技術です。
簡単ではありませんが、通信について理解を深めたい場合はここから勉強することをオススメします。
以下にHTTP通信とソケット通信を比較したメリット・デメリットをまとめます。
アセンブラとPythonのような関係かな
今回は以下の構成でソケット通信のチャットアプリを作ります。
トランスポート層 | TCP |
ネットワーク層 | IP |
データリンク層 | Ethenet |
物理層 | 無線LAN |
詳細は実際に動かしながらみていきます
動作原理
チャットアプリの動作原理を簡単にご説明します。
①サーバー側で、サーバーのIPアドレスやポート番号を関連付けたソケットを作成して、接続待機状態にします。
②クライアント側で、サーバーのIPアドレスやポート番号を関連付けたソケットを作成します。
③クライアント側からサーバーへ接続します。
④お互いに任意のメッセージを送受信します。
ソースコード
ソースコードをgitで公開しています。
動作環境
Visual Studio Community 2022 (64ビット)
Version 17.7.0
C# コンソール アプリケーション
.NET 6.0
ソケットの作成
ソケットとは、通信を制御するための制御情報を記録したものです。
通信相手のIPアドレスやポート番号等をソケットに記録しておき、プロトコルスタックはこれを参照しながら通信を進めていきます。
サーバー側
以下のプログラムでソケットを作り、クライアントから接続されるまで待機します。
using System.Net;
using System.Net.Sockets;
using System.Text;
IPAddress ipAddress = new IPAddress(0xFFFFFFFF);
// 自分のIPアドレスを取得する
string hostname = Dns.GetHostName();
IPAddress[] selfIPAddress = Dns.GetHostAddresses(hostname);
foreach (IPAddress address in selfIPAddress)
{
if (address.AddressFamily == AddressFamily.InterNetwork)
{
ipAddress = address;
}
}
// エンドポイント(IPアドレスとポートの組み合わせ)を作成する
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 11_000);
// ソケットを作成する
using Socket listener = new(
ipEndPoint.AddressFamily,
SocketType.Stream,
ProtocolType.Tcp);
// ソケットをバインドする
listener.Bind(ipEndPoint);
// 接続待ち状態にする
listener.Listen(100);
サーバーは、まず自分のIPアドレスを取得して、そのIPアドレスと任意のポート番号(11000)を指定したIPEndPointを作成します。
次にアドレスファミリ(IPv4)、ソケットタイプ(ストリームソケット)、プロトコルタイプ(TCP)を指定してソケットのインスタンスを作成します。
このソケットでBind()を呼び出して、先ほどのIPEndPointと関連付けを行います。
最後にListen()を呼び出してサーバーを接続待ち状態にします。
クライアント側
以下のプログラムでソケットを作成し、サーバーへ接続を試みます。
using System.Net.Sockets;
using System.Net;
using System.Text;
IPAddress ipAddress = new IPAddress(0xFFFFFFFF);
// 正しいIPアドレスを入力させる
bool ret = false;
while (!ret)
{
Console.WriteLine("Input server IP address");
var inIP = Console.ReadLine();
ret = IPAddress.TryParse(inIP, out ipAddress);
if (!ret) Console.WriteLine($"Wrong IP address({inIP}).Input again.");
}
// エンドポイント(IPアドレスとポートの組み合わせ)を作成する
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 11_000);
// ソケットを作成する
using Socket client = new Socket(
ipEndPoint.AddressFamily,
SocketType.Stream,
ProtocolType.Tcp);
// 同期で接続を試みる
await client.ConnectAsync(ipEndPoint);
まずキーボードからサーバーIPアドレスの入力を要求して、入力されたIPアドレスが正しいフォーマットであれば、そのサーバーIPアドレスとサーバー側で指定されたポート番号を持ったIPEndPointを作成します。
次にソケットのインスタンスをサーバー側と同様に作成します。
最後にConnectAsyncを呼び出して、サーバーの情報を持ったIPEndPointを引数に渡し、サーバーに接続を試みます。
メッセージの送信処理
チャットアプリはメッセージの送信処理と受信処理の両方を同時に処理する必要があります。
今回は送信処理と受信処理をタスクで分けることによって、同時に両方の処理を進めるようにします。
メッセージの送信処理はサーバー側とクライアント側で共通のソースコードなので、代表でサーバー側のソースコードで解説します。
(変数名などに若干の違いはあります。)
サーバー・クライアント共通
// メッセージ送信タスクを生成
Action ASendMsg = startSend;
Task TSendTask = new Task(ASendMsg);
TSendTask.Start();
Console.WriteLine("メッセージを入力してください。");
void startSend()
{
while (true)
{
// 同期でメッセージを送信する
var message = Console.ReadLine();
if (message != null)
{
var messageBytes = Encoding.UTF8.GetBytes(message);
_ = handler.SendAsync(messageBytes, SocketFlags.None);
}
}
}
メッセージ送信処理であるstartSend()を新しいタスクとして登録します。
このタスクのStart()を呼び出すことによって、startSend()の処理をメインの処理と並列で処理することができます。
startSend()では、キーボードから送信するメッセージの入力を要求して、UTF8の形式で送信します。
メッセージの受信処理
メイン処理を行うタスクは、そのままメッセージ受信処理に利用します。
メッセージ送信処理は前節のとおり、別のタスクに分けて処理しています。
サーバー・クライアント共通
while (true)
{
// 同期でメッセージを受信する
var buffer = new byte[1_024];
var received = await handler.ReceiveAsync(buffer, SocketFlags.None);
var response = Encoding.UTF8.GetString(buffer, 0, received);
// クライアントのACKメッセージを受信
if (response == "<|ACK|>")
{
Console.WriteLine("送信成功!");
}
// クライアントの通常メッセージを受信
else
{
Console.WriteLine($"Receive message:\"{response}\"");
// メッセージでACKを送信
var ackMessage = "<|ACK|>";
var echoBytes = Encoding.UTF8.GetBytes(ackMessage);
await handler.SendAsync(echoBytes, 0);
}
}
受信用のバッファを準備し、通信相手からメッセージが送られてくるまで待機します。
送られてきたメッセージが「<|ACK|>」という文字列であれば、「送信成功!」の文字列を自分のコンソール画面に表示します。
それ以外の文字列であれば、通信相手から送られてきたメッセージであると判断し、自分のコンソール画面にメッセージを表示した後に、正常に受け取った合図である「<|ACK|>」のメッセージを送信します。
TCPプロトコルを使用しているので、スリーハンドシェイクでACKのやり取りはされていますが、アプリケーションの中でもその真似ごとをしています。
動作確認
実際に作成したアプリケーションの動作確認をします。
上記のソースコードをビルドしてできたアプリケーションの内、まずはサーバー側の「Server.exe」を起動してクライアントからの接続を待ちます。
このとき、サーバー側のPCでコマンドプロンプトを起動して「ipconfig」のコマンドを入力し、サーバーのIPアドレスを確認しておきます。
次に「Client.exe」を起動してサーバーのIPアドレスを入力したら接続が確立します。
便宜上、1つのPC上でサーバーとクライアントを立ち上げていますが、無事通信ができていることが分かります。
次に、コマンドプロンプトでソケットの状態を確認してみます。
コマンドプロンプトを起動して、「netstat -ano」のコマンドを入力します。
ソケットの接続が確立していることが分かります。
今回はサーバーもクライアントも同じPCなのでIPアドレスが一緒ですが、ポート番号が異なっています。
クライアントはソケット作成時に空いているポート番号が自動で割り当てられます。
Wiresharkで通信データのやり取りを確認します。
次のように操作を行いました。
①でスリーウェイハンドシェイクで通信が確立しています。
②でクライアントからサーバーに対して「test from client」のメッセージが送られ、③でサーバーから「<|ACK|>」のメッセージが返されています。
④でサーバーから「test from server」のメッセージが送られ、⑤でクライアントから「<|
ACK|>」のメッセージが送られています。
無事、通信されていることが分かります。
まとめ
C#でソケット通信を使用したチャットアプリを作成しました。
ソケット通信は低レベルな通信で、自分で考えないといけないことが多く難しい通信です。
ですが、汎用性が高く組み込みソフトではよく使われる通信なので、なんとかマスターしたいですね。
コメント