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を確実に破棄することでリソースリークを防ぎます。