今日の収穫

2017/12/21

 今日はいろいろと収穫がありました。
まず一つに、TServerSocketの基本的な使い方を覚えたこと
もう一つが、名前で関数を呼ぶ方法が分かったこと です。

 TMemo APIの実現方法をいろいろ考えて、ソケットを使うことにしました。
これならもしかしたらインターネットでの利用もできるかもしれないし、
何よりテキスト同士の通信だけなら簡単にできるので、PickTokenあたりを使えば結構なんでもできたり。
いちおうDelphi6でいじるユーザーのことも考えて、TServerSocket(Delphi7では非推奨*1としてパレットに出てこないので、コードで直接呼び出し)をつかいます。
というか、これしかサンプルコードが見つからなかったんですけどね(^^ゞ
まあ、使い方自体はたいしたことはないみたいです。フォームのOnCreateイベントで

  Server                    := TServerSocket.Create(Self);
  Server.Port               := TMemoAPIPortNumber; // いまのところ8200固定
  Server.ServerType         := stNonBlocking;
  Server.OnClientRead       := ServerClientRead;
  Server.OnClientConnect    := ServerClientConnect;
  Server.OnClientDisconnect := ServerClientDisconnect;
  Server.Open;

などとやって、サーバーをたてれば、あとはクライアントがつないだときにServerClientConnectが呼ばれ、
文字が送られてきたときはServerClientReadが呼ばれます。
OnDestroyあたりで

  Server.Close

とやっておけばOKです。
Telnetあたりで open 127.0.0.1 8200 で、接続
TelnetはHelloメッセージを返すまで動かないようですので、とりあえず「こちらTMemo APIサーバー」みたいなことを言っておきます。
まあ、ここまでは簡単。難しいのはここから。
 問題はOnClientReadでした。
文字が一つタイプされるごとに呼ばれるのですが、
Socket.RecieveTextは、TStreamなどで言うReadメソッドのようなもので、読み込み済みの文字は全部、ソケットから消されてしまいます。
これでは一文字ずつしか得られない(-_-;)
結局、WinAPIのマニュアルを見てみたところ、WinSockのrecv APIのフラグに、MSG_PEEKなんてものがあるらしいので、それを使って何とか解決。

 // 改行文字が出てくるまでは、処理しない
  SetLength(ch, Socket.ReceiveBuf(Pointer(nil)^, -1));
  recv(Socket.SocketHandle, Pointer(ch)^, Length(ch), MSG_PEEK);
  if not AnsiEndsStr(#13#10, ch) then exit;

ですが、MSG_PEEKというのは、どうやら非推奨なようで。Peekできないストリームってなによって言うのも、まあ疑問はあるんですけど。
 それに、これを使うと、下の行のif文が成立するまで(あとのRecieveTextでバッファが空になるまで)ずっとOnClientReadが発生し、
結果この部分がループしますから、その間TMemoの動作は止まってしまいます。
まあ、Telnetでもやらない限り、途中までコマンドが入力されて止まるみたいなことはないので、問題はないかもしれませんが、
RecieveTextして、改行がでるまでどこかにためておこうにも、クライアントが何人いるか分からないから、どこに保存すればいいか分からないし、
せめて今接続してるクライアントが何人目か分かれば、TStringListでも使えるんでしょうけどね。Socket.Dataも、なんか文字化けするし…。
まあ、中間まで来たってとこでしょうか(^_^;まだちょっと続きそうです。この問題。

 あと一つ。名前で関数を呼ぶには。
これは割と簡単なことです。TObjectにMethodAddressというメソッドがあるので、それに関数名(大文字小文字区別無し)を渡せば、そのアドレスが手に入ります。
あとは、GetProcAddressで動的ロードしたAPIを使う時みたいに、

  TTMemoAPIFunc = procedure(Argument: String);
 …
  func := TTMemoAPI.MethodAddress(PickToken(msg, ' '));
  if Assigned(func) then begin
    func(msg);
    Socket.SendText(Result + #13#10)
  end else
    Socket.SendText(TMemoAPIFunctionNotFound);

とやって使えばいいです。クラスメソッド(class procedure Delphiにも実はあります)でもいいみたいです*2。
まあ、プロトタイプが全て同じでなければいけないという制約がありますが、VBのCallByNameのように、スクリプトとか作るときにはかなり便利かも。

 ただ、どうもFunctionはダメみたいです。Resultに値を入れようとした瞬間にエラーになります。
varで引数を宣言して、それを戻り値に使ってもダメ。上のはユニット変数として、Result : String を宣言して誤魔化してます。

 ええと、これで何とか、
TMemoGetAppDir と入力すると
H:ProjectsTMemo などとちゃんと返してくれます。
がだぶってるのは、改行を区切りにしてしまったので、変わりにを 改行を と表現するようにしたせいです。Cの表記みたいです。
ただ、途中でBackSpaceキーを押したりすると、そのキーコードが書き込まれてしまってコマンドが無効になったりしますが…。
むー、HTTPサーバーとかはどうやってるんだろう?いちいちキーコードが来たときは、バッファを同じように処理したりしてるのかなぁ…。

 そろそろネタもたまってきたし、旧学校を更新したいな。その前に課題とか終わったらですけど。

*1:Kylixとのかねあいで、あんまり使ってほしくないそうです。KylixをわすれてDelphiは.NETになってしまいましたけど、どうするんでしょう

*2:クラスメソッドを使う場合は、TTMemoAPI.MethodAddress でOK