【C#】ソケット通信で簡単なチャットアプリを作ってわかりやすく解説

チャットアプリの動作確認 Tsubatter
スポンサーリンク

C#でソケット通信を使った簡単なチャットアプリを作って動かすことで、ソケット通信について内容をまとめてみます。

また、この記事を読んで、ネットワークについてもっと勉強したいと思った方は以下の書籍がオススメです。
ネットワーク通信の内容をプログラミングのイメージにまで落として解説されているので、より具体的なイメージがつくと思います。

メカつば
メカつば

ソケット通信は組み込みソフトでも使われています

こんな人にオススメ
  • ソケット通信について概要を知りたい
  • C#でチャットアプリを作りたい

ソケット通信とは

概要

イーサネットを通してデータを送受信するための手段の1つです。

よく耳にするのは、Webブラウザで使用されているHTTP、HTTPS通信のような高レベルなものがあると思いますが、ソケット通信はそれらの土台となる技術です。
簡単ではありませんが、通信について理解を深めたい場合はここから勉強することをオススメします。

以下にHTTP通信とソケット通信を比較したメリット・デメリットをまとめます。

チェック

ソケット通信のメリット:

  1. 低レベルの通信方法で、自由なカスタマイズが可能
    データの送受信をプログラマが直接制御できる
  2. リアルタイム性に優れていて、効率的に通信ができる
  3. TCPやUDPなど複数のプロトコルを選択できる

ソケット通信のデメリット:

  1. 低レベルの操作が必要なため、プロトコルやネットワークの詳細な知識が必要
  2. セキュリティ対策を自前でする必要がある

HTTP通信のメリット:

  1. 高レベルの通信方法で、簡単にデータを送受信できる
  2. 拡張可能なプロトコルであり、ヘッダーやメソッドをカスタマイズすることができる
  3. HTTPSを使用することで、通信の暗号化やセキュリティを担保できる

HTTP通信のデメリット:

  1. ヘッダーやメタデータが必要で、データの送受信にオーバーヘッドが生じる
  2. リアルタイム性に劣る

アセンブラとPythonのような関係かな

今回は以下の構成でソケット通信のチャットアプリを作ります。

トランスポート層TCP
ネットワーク層IP
データリンク層Ethenet
物理層無線LAN
OSI参照モデル
ソケット通信の概要
チェック
  • TCP
    スリーウェイハンドシェイクを使い信頼性が高く、パケットの順序を保証します。
  • UDP
    高速な通信を実現しますが、信頼性が低く、順序が保証されない代わりに軽量です。
メカつば
メカつば

詳細は実際に動かしながらみていきます

動作原理

チャットアプリの動作原理を簡単にご説明します。

ソケット通信の動作原理

①サーバー側で、サーバーのIPアドレスやポート番号を関連付けたソケットを作成して、接続待機状態にします。

②クライアント側で、サーバーのIPアドレスやポート番号を関連付けたソケットを作成します。

③クライアント側からサーバーへ接続します。

④お互いに任意のメッセージを送受信します。

ソースコード

ソースコードをgitで公開しています。

Tsubablog / ChatAppli · GitLab
GitLab.com

動作環境

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を作成します。

チェック
  • IPアドレス
    ネットワークの中からPCを特定するために使用します。
  • ポート番号
    IPアドレスでネットワークの中からPCを特定したら、次にポート番号でどのアプリケーションにパケットを届けるかを特定します。

次にアドレスファミリ(IPv4)、ソケットタイプ(ストリームソケット)、プロトコルタイプ(TCP)を指定してソケットのインスタンスを作成します。
このソケットでBind()を呼び出して、先ほどのIPEndPointと関連付けを行います。

チェック
  • アドレスファミリ
    通信する際のアドレス表現や通信プロトコルを指定するための識別子です。
    ここではIPv4であることを指定しています。
  • ソケットタイプ
    ソケット通信に使用するソケットにはいくつか種類があります。
    ここでは、ストリームソケットを指定しており、TCPを使用した通信で信頼性と順序性を提供し、データが連続的に送受信されることを想定しています。
  • プロトコルタイプ
    どのようなプロトコルを使用して通信するかを指定します。
    ここでは、TCPを指定しています。

最後に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」のコマンドを入力します。

チェック
  • netstat(Network Statistics)
    ネットワーク接続やポートの利用状況を確認することができます。

ソケットの接続が確立していることが分かります。
今回はサーバーもクライアントも同じPCなのでIPアドレスが一緒ですが、ポート番号が異なっています。
クライアントはソケット作成時に空いているポート番号が自動で割り当てられます。


Wiresharkで通信データのやり取りを確認します。

次のように操作を行いました。

操作内容
  1. サーバーアプリとクライアントアプリを立ち上げて、接続
  2. クライアントから「test from client」のメッセージを送信
  3. サーバーから「test from server」のメッセージを送信
ワイヤーシャークの概要

①でスリーウェイハンドシェイクで通信が確立しています。
②でクライアントからサーバーに対して「test from client」のメッセージが送られ、③でサーバーから「<|ACK|>」のメッセージが返されています。

クライアントからテストメッセージを送信
サーバーからACKメッセージを送信


④でサーバーから「test from server」のメッセージが送られ、⑤でクライアントから「<|
ACK|>」のメッセージが送られています。

無事、通信されていることが分かります。

まとめ

C#でソケット通信を使用したチャットアプリを作成しました。

ソケット通信は低レベルな通信で、自分で考えないといけないことが多く難しい通信です。
ですが、汎用性が高く組み込みソフトではよく使われる通信なので、なんとかマスターしたいですね。

Tsubatter
スポンサーリンク
tsubablog

メーカーで組み込みプログラマーとして勤務しているサラリーマンです。
プログラミングの楽しさについて発信しています。

業務で扱っている言語はC、C++です。

tsubablogをフォローする
つばブログ

コメント

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