はじめに
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を入手してインストールしてください。
今回作成するクリップボード共有ソフトの概要
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
のほかに、TButton
とTMemo
を1つずつフォームに貼り付けて、以下のプログラムを記述します。
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;
TIdSocketHandle
が未定義の識別子としてエラーになってしまいます。そのため、uses
節にIdSocketHandle
を追加します。UDPブロードキャスト通信
次にブロードキャスト通信を行ってみます。このためには、TIdUDPServer
のBroadcastEnabled
プロパティをTrue
にして、Broadcast
メソッドを使ってデータを送信します。先ほどのプログラムのボタンを押した時の処理を、以下のように書き換えます。受信処理は全く同じです。
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つ貼り付けてください。
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
に受信したデータを表示させています。
Syncronize
メソッドを利用しますが、ここではプログラムの簡略化のため直接操作しています。 また、BroadcastMessage
メソッドは、全てのクライアントにデータを送信しています。TIdTCPServer
で各クライアントはTThreadList
で管理されていますので、TThreadList
のアイテムを参照するときには、このプログラムのように、リストをロックして、参照し、ロックを解除という手順を踏みます。
サーバーの次はクライアントです。ボタンを押すと「test」という文字列をサーバーに対して送信します。TIdTCPClient
と、TButton
、TMemo
を1つずつ貼り付けてください。
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コンポーネントは基本的にブロッキング接続です。「ブロッキング接続」とは、受信を行うと実際に受信があるまで処理を中断する接続です。そのため、受信を行うためのスレッドを作り、その中で受信を行います。
// 受信用クラスの定義 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クライアントの初めのサンプルを、以下のように書き換えます。
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;
画面の設計
コンポーネントの配置
本題のクリップボードの共有アプリケーションに戻ります。以下の図のように、フォームにコンポーネントを貼り付けます。起動の順序により、サーバーになるかクライアントになるかが変わります。そのため、TIdTCPServer
とTIdTCPClient
の両方を貼り付けます。
イベントの実装
プログラムの中でいくつかポイントになる部分だけピックアップしてみました。以下は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コンポーネントには、RecvLn
やWriteLn
という手続きが用意されており、改行を区切り文字として簡単にデータを取り出せるようになっています。
今回のプログラムでも改行をデータの区切りとしていますが、クリップボードに改行が含まれることがあるので、TLanClipCommand
クラス内で改行を「\n」に置換しています。
おわりに
UDPブロードキャストを利用すれば、今回のようなクリップボードの共有のほかに、チャットなどのアプリケーションも作成できるでしょう。Indyコンポーネントを利用すれば、思ったより手軽にアプリケーションが作成できます。ぜひ、本稿やIndyのデモを参考にチャットなどを作ってみてください。