Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

ソケットライブラリのIndyを利用したクリップボード共有ソフト

IndyコンポーネントでTCP/UDP通信を行うアプリケーションの製作

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/10/28 12:00

ダウンロード 実行ファイル (245.9 KB)
ダウンロード ソースコード (15.8 KB)

LAN内の複数のPCで、クリップボードを共有するソフトを製作します。サーバーの検索にUDPブロードキャスト通信を利用し、クリップボードのデータ送受信にはTCPを利用します。これをDelphi 7に付属しているIndy 9のコンポーネントを利用して、手軽に作ってみます。

はじめに

 LANで繋がれた複数のPCを利用して作業しているときに、テキストを別のPCにコピーしたい場合があります。例えば、WebページのURLや、ちょっとした予定のメモなどです。これらを別のPCで利用するために、わざわざ共有フォルダを開き、ファイルへ保存して受け渡すのは非常に面倒です。

 そこで本稿では、簡単なTCP/UDP通信を行って、複数のPCでクリップボードを共有するソフトを製作します。また、このためにDelphi 7に付属しているIndy 9コンポーネントを使ってみます。

クリップボード共有アプリケーション
クリップボード共有アプリケーション

対象読者

  • TCP/UDPのことを知りたい人
  • Indyコンポーネントの使い方を知りたい人
  • チャットの作り方を知りたい人

必要な環境

 開発には、Delphi 7と、Delphi 7に付属しているIndy 9コンポーネントを使用しました。Indyはオープンソースのコンポーネントです。Delphi 6を使用している方は、Indy ProjectのホームページからIndy 9を入手してインストールしてください。

Indyのページ
Indyのページ

今回作成するクリップボード共有ソフトの概要

 LAN内でクリップボードを共有するソフトです。ソフトを起動すると、自動的にクリップボードの共有が始まります。テキストをコピーしてクリップボードが更新されると、これを検出してクリップボードの内容を別のPCへ送信します。

クリップボード共有の仕組み

 プログラムを起動すると、まず、ネットワーク内にサーバーがあるかどうかを確認します。もし、サーバーがあれば、自身はクライアントとしてサーバーに接続します。サーバーがなければ、自身がサーバーになりクライアントが接続してくるのを待ちます。

 以下が、アプリケーションを起動した時の動きをまとめたものです。

  • サーバーがあるか問い合わせる
  • サーバーから応答がないかしばらく待つ
  • サーバーから応答があればクライアントとしてサーバーに接続する
  • 応答がなかった場合、自身をサーバーとしてクライアントからの接続を待つ

 あとは、タイマーで定期的にクリップボードの内容を確認し、クリップボードが更新されていれば、クリップボードの内容を別のPCへ送信します。

TCP/UDPの使い分け

 サーバーがあるかどうかの問い合わせを行うのにUDPを使い、クリップボードのデータの受け渡しにはTCPを使っています。なぜ2つのプロトコルを使い分けるのか、このプロトコルの性格を確認してみます。

 TCPとUDPは、インターネットで利用される標準プロトコルで、OSI参照モデルのトランスポート層に当たります。HTTPやFTPなどのアプリケーション層のプロトコルはトランスポート層の上で利用されています。

 TCPとUDPの一番大きな違いは信頼性です。TCPでは、信頼性のある通信を行うことができますが、UDPでは、信頼性のない通信しかできません。送信したパケットの順序が入れ替わったり、途中で消失する可能性があります。信頼性がないのに、なぜUDPを使うのかと言えば、UDPはパケットの順番の入れ換えや受信確認を行わないので、転送速度が速いからです。

 UDPで確実な通信を行うためには、受信確認などの処理をアプリケーション側で行う必要があります。そうなると、結局TCPを使った方が確実で手っ取り早いため、UDPを使う場面というのは、途中で多少パケットが失われても問題のない通信を行う場合に限られています。例えば、音楽や映像のストリーミング配信では、多少データが欠けても問題がないので、UDPが使われています。

 また、UDPではブロードキャスト通信やマルチキャスト通信ができます。これは、複数のノードに対して、同時に情報を伝えることができるというものです。

 つまり今回、ネットワーク内にサーバーが存在するかどうかを確認するのにUDPを使うのは、ブロードキャスト通信を行うためです。そして、クリップボードのデータをTCPを使ってやり取りするのは、信頼性のないUDPを使うよりも、正確に通信できるという理由からです。

Indyコンポーネントについて

 Indyコンポーネントは、オープンソースのソケットライブラリです。TCP/UDPをはじめ、HTTP/FTP/POP3/SMTPなど多くのプロトコルをサポートしています。Delphi 7には、Indy 9が標準で付属しています。Indyコンポーネントを利用すると、手軽にチャットソフトなどを作成することができます。しかし、国内ではあまり利用されていないようなので、実際に使おうと思うと、自分で試行錯誤しながら作ることになります。

 Indyコンポーネントの一番のお手本は、Indy Projectのホームページから入手できるデモプログラムです。各プロトコルごとに簡単なサンプルが示されています。

UDPの基本的な使い方

 クリップボードの共有サーバーが存在するかを確認するのに、UDPを利用します。ここでは、IndyコンポーネントのTIdUDPServerを利用します。これはIndy Serversのタブにあります。

 UDPはコネクションレスのプロトコルです。送信も受信も一方的に行うので、UDPでは明確なサーバーとクライアントの区別がありません。以下は、TIdUDPServerの簡単な使用例です。ボタンを押すと自分自身に対して「test」という文字列データを送受信します。

 TIdUDPServerのほかに、TButtonTMemoを1つずつフォームに貼り付けて、以下のプログラムを記述します。

UDPの利用例
procedure TForm1.FormCreate(Sender: TObject);
begin
  // コンポーネントの設定
  Memo1.Text := '';
  IdUDPServer1.DefaultPort := 12345; // 適当なポートを使用
  IdUDPServer1.OnUDPRead   := IdUDPServer1UDPRead; // 受信時のイベント
  // UDPサーバーを起動する
  IdUDPServer1.Active := True;
end;

// ボタンを押したら自分に対し'test'を送信する
procedure TForm1.Button1Click(Sender: TObject);
begin
  IdUDPServer1.Send('127.0.0.1', 12345, 'test'); // 自分自身に送信
end;

// データを受信した時
procedure TForm1.IdUDPServer1UDPRead(Sender: TObject; AData: TStream;
  ABinding: TIdSocketHandle);
var
  s: string;
begin
  // TStreamを文字列に変換する
  SetLength(s, AData.Size);
  AData.Read(s[1], AData.Size);
  // メモの先頭に受信した文字列を挿入
  Memo1.Lines.Insert(0, s);
end;
 Delphi 7では、このプログラムを記述してコンパイルすると、TIdSocketHandleが未定義の識別子としてエラーになってしまいます。そのため、uses節にIdSocketHandleを追加します。

UDPブロードキャスト通信

 次にブロードキャスト通信を行ってみます。このためには、TIdUDPServerBroadcastEnabledプロパティをTrueにして、Broadcastメソッドを使ってデータを送信します。先ほどのプログラムのボタンを押した時の処理を、以下のように書き換えます。受信処理は全く同じです。

UDPのブロードキャスト通信
procedure TForm1.FormCreate(Sender: TObject);
begin
  // コンポーネントの設定
  Memo1.Text := '';
  IdUDPServer1.DefaultPort := 12345;
  IdUDPServer1.OnUDPRead   := IdUDPServer1UDPRead;
  IdUDPServer1.BroadcastEnabled := True;
  IdUDPServer1.Active := True;
end;

// ボタンを押したら全員に対し'test'を送信する
procedure TForm1.Button1Click(Sender: TObject);
begin
  IdUDPServer1.Broadcast('test', 12345);
end;

//IdUDPServer1UDPReadは同じなので省略

TCPの使い方

 今度はTCPです。TCPはUDPと違って、明確にサーバーとクライアントが分かれています。TCPでは必ずサーバー側、クライアント側とプログラムを分けて作る必要があります。

 以下がサーバー側のプログラムです。ボタンを押すと、接続しているクライアント全員に「test」という文字列を送信します。

 TIdTCPServerに加えて、TButtonを1つと、TMemoを1つ貼り付けてください。

TCPサーバーの例
procedure TForm1.FormCreate(Sender: TObject);
begin
  // TCPサーバーを起動する
  IdTCPServer1.DefaultPort := 12345;
  IdTCPServer1.OnExecute := IdTCPServer1Execute; // 処理イベント
  IdTCPServer1.Active := True;
end;

// サーバーの行うべき処理を記述するイベント
procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
var s: string;
begin
  // 一行分受信して Memo1 に追加する
  s := AThread.Connection.ReadLn(#13#10);
  Memo1.Lines.Insert(0, s);
end;

// 接続しているクライアント全員に str を送信する
procedure TForm1.BroadcastMessage(str : string);
var
  Count: Integer;
  List : TList;
begin
  List := IdTCPServer1.Threads.LockList;
  try
    for Count := 0 to List.Count -1 do
    try
      TIdPeerThread(List.Items[Count]).Connection.Write(str);
    except
      TIdPeerThread(List.Items[Count]).Stop;
    end;
  finally
    IdTCPServer1.Threads.UnlockList;
  end;
end;

// ボタンを押したら全員に test を送信する
procedure TForm1.Button1Click(Sender: TObject);
begin
  BroadcastMessage('test'#13#10);
  Memo1.Lines.Insert(0, 'test');
end;

 このプログラムでポイントとなるのは、TIdTCPServer1.OnExecuteで実行されるIdTCPServer1Executeメソッドです。ここでは接続しているクライアントが何を行うのかを記述します。今回はMemo1に受信したデータを表示させています。

 本来スレッド内でVCLコンポーネントを操作する際はSyncronizeメソッドを利用しますが、ここではプログラムの簡略化のため直接操作しています。

 また、BroadcastMessageメソッドは、全てのクライアントにデータを送信しています。TIdTCPServerで各クライアントはTThreadListで管理されていますので、TThreadListのアイテムを参照するときには、このプログラムのように、リストをロックして、参照し、ロックを解除という手順を踏みます。

 サーバーの次はクライアントです。ボタンを押すと「test」という文字列をサーバーに対して送信します。TIdTCPClientと、TButtonTMemoを1つずつ貼り付けてください。

TCPクライアントの例
procedure TForm1.FormCreate(Sender: TObject);
begin
  // サーバーに接続する
  IdTCPClient1.Host := '127.0.0.1'; // localhost
  IdTCPClient1.Port := 12345;
  IdTCPClient1.Connect;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  IdTCPClient1.Write('test'#13#10);
end;

 さて、上のプログラムだけでは、送信しかできません。以下が受信のためのプログラムなのですが、Indyコンポーネントは基本的にブロッキング接続です。「ブロッキング接続」とは、受信を行うと実際に受信があるまで処理を中断する接続です。そのため、受信を行うためのスレッドを作り、その中で受信を行います。

TCPクライアントで受信用スレッド
// 受信用クラスの定義
type
  TTcpListenerThread = class(TThread)
  private
    RecvData: string;
    procedure SyncPrint;
  protected
    procedure Execute; override;
  end;
  
implementation

procedure TTcpListenerThread.Execute;
begin
  while not Terminated do
  begin
    if not Form1.IdTCPClient1.Connected then // 切断されたら終わる
    begin
      Terminate;
      Continue;
    end;
    // 受信処理
    RecvData := Form1.IdTCPClient1.ReadLn(#13#10);
    Synchronize(SyncPrint);
  end;
end;

procedure TTcpListenerThread.SyncPrint;
begin
  Form1.Memo1.Lines.Insert(0, RecvData);
end;

 受信用のスレッドを起動するのは、サーバーに接続完了した時です。TCPクライアントの初めのサンプルを、以下のように書き換えます。

TCPクライアントのスレッド使用例
procedure TForm1.FormCreate(Sender: TObject);
begin
  // サーバーに接続する
  IdTCPClient1.Host := '127.0.0.1'; // localhost
  IdTCPClient1.Port := 12345;
  IdTCPClient1.OnConnected := IdTCPClient1Connected; // 接続した時
  IdTCPClient1.Connect;
end;

procedure TForm1.IdTCPClient1Connected(Sender: TObject);
begin
  // 受信用のスレッドを起動
  with TTcpListenerThread.Create(True) do
  begin
    FreeOnTerminate := True;
    Resume;
  end;
end;

画面の設計

コンポーネントの配置

 本題のクリップボードの共有アプリケーションに戻ります。以下の図のように、フォームにコンポーネントを貼り付けます。起動の順序により、サーバーになるかクライアントになるかが変わります。そのため、TIdTCPServerTIdTCPClientの両方を貼り付けます。

配置コンポーネントの一覧
配置コンポーネントの一覧

イベントの実装

 プログラムの中でいくつかポイントになる部分だけピックアップしてみました。以下はUDPブロードキャストにより、ネットワーク内に既にサーバーが存在するかどうかを確認する部分です。

UDPでサーバーが存在するか確認
procedure TfrmMain.FormShow(Sender: TObject);
begin
  // 起動したら既にサーバーがあるか確認する
  UdpServer.BroadcastEnabled := True;
  UdpServer.Active := True;
  UdpServer.Broadcast('サーバーある?'#13#10, UDP_PORT);
  Mode := cmFindingServer;
  timerFindServer.Enabled := True;
end;

procedure TfrmMain.UdpServerUDPRead(Sender: TObject; AData: TStream;
  ABinding: TIdSocketHandle);
var
  ip, a: string;
begin
  // 受信したデータを文字列に変換
  a  := '';
  SetLength(a, AData.Size);
  AData.Read(a[1], Length(a));
  ip := ABinding.PeerIP;

  // 受信したコマンドの解析
  if a = 'サーバーある?' then // サーバーがあるか問い合わせがあった
  begin
    if Mode = cmServer then
    begin
      a := '私はサーバー';
      UdpServer.Send(ip, UDP_PORT, a); // サーバーであることを送り返す
    end;
  end else
  if a = '私はサーバー' then // サーバーが見つかった
  begin
    if Mode = cmFindingServer then
    begin
      // サーバーに接続する
      Mode := cmClient;
      TcpClient.Host := ip;
      TcpClient.Connect;
    end;
  end;
end;

procedure TfrmMain.timerFindServerTimer(Sender: TObject);
begin
  // サーバーの起動処理
  timerFindServer.Enabled := False;
  // モードが変わっていたら何もしない
  if Mode <> cmFindingServer then Exit;
  Mode := cmServer;
  FBoard := Clipboard.AsText;
  // サーバーがないので自らをサーバーとして起動する
  TcpServer.Active := True;
  timerCheckClipboard.Enabled := True;
end;

 フォームの表示処理で「サーバーある?」というコマンドをブロードキャスト送信します。そして、UDPの受信処理で「私はサーバー」というコマンドが返って来れば、そのサーバーに接続を行います。

 timerFindServerTimer手続きは一定期間経過後に実行されます。サーバーが見つかっていれば何もしないのですが、サーバーが見つからなかった場合は、自身をサーバーとして、TCPサーバーを起動します。

 以下はクリップボードを監視するプログラムです。クリップボードの内容がテキストで、かつ前回確認した時と異なっていれば、サーバーへ「copy」というコマンドを送信します。

クリップボードの監視
procedure TfrmMain.timerCheckClipboardTimer(Sender: TObject);
begin
  // クリップボードの監視
  if Clipboard.HasFormat(CF_TEXT) then
  if Clipboard.AsText <> FBoard then
  begin
    FBoard := Clipboard.AsText;
    SendToServer(LanClipEncode('all', 'copy', FBoard));
  end;
end;

 TCPにはプロトコルの仕様上、明確なデータの区切りがありません。そのため、本来は区切り文字でデータを区切るという処理が必要になるのですが、Indyコンポーネントには、RecvLnWriteLnという手続きが用意されており、改行を区切り文字として簡単にデータを取り出せるようになっています。

 今回のプログラムでも改行をデータの区切りとしていますが、クリップボードに改行が含まれることがあるので、TLanClipCommandクラス内で改行を「\n」に置換しています。

おわりに

 UDPブロードキャストを利用すれば、今回のようなクリップボードの共有のほかに、チャットなどのアプリケーションも作成できるでしょう。Indyコンポーネントを利用すれば、思ったより手軽にアプリケーションが作成できます。ぜひ、本稿やIndyのデモを参考にチャットなどを作ってみてください。

参考資料

  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • クジラ飛行机(クジラヒコウヅクエ)

    ソフト企画「くじらはんど」にて、多数のフリーソフトを公開しています。日本語プログラミング言語「なでしこ」、テキスト音楽「サクラ」、日本語Wiki記法が特徴の「KonaWiki」などを公開しています。

All contents copyright © 2005-2017 Shoeisha Co., Ltd. All rights reserved. ver.1.5