C#のSocket通信を利用した画像ファイルの転送実装

Socket通信を用いて、端末間で画像データを送受信する機能を実装します。データの欠落を防ぐため、送信データの先頭にデータサイズを付与するプロトコルを採用し、受信側で正確にパケットを再構築する手法を解説します。 まずは、ネットワーク設定やポート管理を行うユーティリティクラスを作成します。
public static class NetworkUtil
{
    // ローカルマシンのIPv4アドレスリストを取得
    public static IPAddress[] GetLocalIPv4List()
    {
        string hostName = Dns.GetHostName();
        IPHostEntry entry = Dns.GetHostEntry(hostName);
        return entry.AddressList
            .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork)
            .ToArray();
    }

    // 動的に使用可能なポート番号を特定する
    public static int GetAvailablePort()
    {
        using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        {
            socket.Bind(new IPEndPoint(IPAddress.Any, 0));
            return ((IPEndPoint)socket.LocalEndPoint).Port;
        }
    }
}
次に、可変長のデータを確実に送受信するための通信補助クラスを実装します。データの先頭4バイトにコンテンツの長さを格納することで、受信側は読み取るべきバイト数を正確に把握できます。
public static class SocketDataHandler
{
    // サイズ情報を付与してデータを送信
    public static void SendPayload(Socket socket, byte[] buffer)
    {
        int length = buffer.Length;
        byte[] header = BitConverter.GetBytes(length);
        
        // ヘッダー(サイズ)の送信
        socket.Send(header);

        // ボディ(データ本体)の送信
        int sentBytes = 0;
        while (sentBytes < length)
        {
            sentBytes += socket.Send(buffer, sentBytes, length - sentBytes, SocketFlags.None);
        }
    }

    // サイズ情報を読み取ってデータを受信
    public static byte[] ReceivePayload(Socket socket)
    {
        // 4バイトのヘッダーを受信してデータ長を解析
        byte[] header = new byte[4];
        socket.Receive(header, 0, 4, SocketFlags.None);
        int expectedLength = BitConverter.ToInt32(header, 0);

        byte[] payload = new byte[expectedLength];
        int receivedTotal = 0;

        // 指定されたサイズに達するまで受信を繰り返す
        while (receivedTotal < expectedLength)
        {
            int currentRead = socket.Receive(payload, receivedTotal, expectedLength - receivedTotal, SocketFlags.None);
            if (currentRead == 0) break;
            receivedTotal += currentRead;
        }
        return payload;
    }
}
WPFアプリケーションでのメイン処理は以下の通りです。バックグラウンドで接続を待機し、受信したバイト配列を画像ソースに変換してUIに表示します。
public partial class PhotoTransferWindow : Window
{
    private Socket _listener;
    private string _selectedFilePath = string.Empty;

    public PhotoTransferWindow()
    {
        InitializeComponent();
        SetupServer();
    }

    private void SetupServer()
    {
        var targetIp = NetworkUtil.GetLocalIPv4List().FirstOrDefault();
        int targetPort = NetworkUtil.GetAvailablePort();

        txtIpAddress.Text = targetIp?.ToString();
        txtPortNumber.Text = targetPort.ToString();

        _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        _listener.Bind(new IPEndPoint(targetIp, targetPort));
        _listener.Listen(5);

        // 受信待機スレッドの開始
        Task.Run(() => ListenLoop());
    }

    private void ListenLoop()
    {
        while (true)
        {
            using (Socket client = _listener.Accept())
            {
                byte[] rawData = SocketDataHandler.ReceivePayload(client);
                
                // UIスレッドで画像を表示
                Dispatcher.BeginInvoke(new Action(() => {
                    imageDisplay.Source = ConvertBytesToImage(rawData);
                }));

                client.Shutdown(SocketShutdown.Both);
            }
        }
    }

    private BitmapImage ConvertBytesToImage(byte[] data)
    {
        var bitmap = new BitmapImage();
        using (var stream = new MemoryStream(data))
        {
            bitmap.BeginInit();
            bitmap.CacheOption = BitmapCacheOption.OnLoad;
            bitmap.StreamSource = stream;
            bitmap.EndInit();
        }
        bitmap.Freeze(); // スレッド間転送を容易にするため
        return bitmap;
    }

    // 画像選択処理
    private void OnSelectImageClick(object sender, RoutedEventArgs e)
    {
        var dialog = new Microsoft.Win32.OpenFileDialog();
        if (dialog.ShowDialog() == true)
        {
            _selectedFilePath = dialog.FileName;
            imageDisplay.Source = new BitmapImage(new Uri(_selectedFilePath));
        }
    }

    // 送信処理
    private void OnSendImageClick(object sender, RoutedEventArgs e)
    {
        if (string.IsNullOrEmpty(_selectedFilePath)) return;

        Task.Run(() => {
            try
            {
                IPEndPoint remoteEp = new IPEndPoint(IPAddress.Parse(txtIpAddress.Text), int.Parse(txtPortNumber.Text));
                using (Socket senderSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
                {
                    senderSocket.Connect(remoteEp);
                    byte[] fileBytes = File.ReadAllBytes(_selectedFilePath);
                    SocketDataHandler.SendPayload(senderSocket, fileBytes);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"送信エラー: {ex.Message}");
            }
        });
    }
}
この実装では、画像をバイト配列に変換し、独自に定義したメッセージヘッダーを付与して送信しています。これにより、ネットワークのセグメンテーション(データの断片化)が発生しても、受信側で正しいデータサイズを待機して結合することが可能です。 実装のポイントは以下の通りです:
  • BitmapImageのフリーズ: UIスレッド以外で生成した画像をUIに反映させる際、Freeze()を呼び出すことで読み取り専用にし、スレッド間の安全な共有を可能にします。
  • 非同期待機: Socket.Acceptはブロッキングメソッドであるため、UIフリーズを避けるためにTask.Runで別スレッドから実行します。
  • ストリームの適切な管理: 受信したデータから画像を生成する際、MemoryStreamを確実に破棄することでリソースリークを防ぎます。

タグ: C# socket WPF Networking ImageProcessing

8月15日 04:34 投稿