Delphiでお手軽プログラミング

Delphiでお手軽プログラミングメニュー

DelphiでWinsockを使ってTcpClientを作る



1.汎用TcpClientユニット

以下ソースコードをファイル名「MamTcpClient.pas」として保存します。
ちょっと微妙な個所もあります。(すいません。汗)
unit MamTcpClient;

interface
uses
  SysUtils, Classes, Windows, WinSock,StrUtils,StdCtrls
  ,dialogs,DateUtils;

type
  TMamErrorEvent = procedure(Sender: TObject;SendMessage:string) of object;

  //TCPクライアント
  TMamTcpClient=class(TObject)
    private
      FPort:Word;           //ポート番号 通常は25
      FHost:AnsiString;     //ホスト名
      FConnecting:Boolean;  //接続中かどうか
      FErrCode:Integer;     //エラーコード
      FErrMsg:String;       //エラーメッセージ
      FErrEvent:TMamErrorEvent;
      FSocket:TSocket;      //ソケット
      FBufSize: Integer;
      FRecvTimeout:TTimeVal;
      FConnectTimeout:TTimeVal;
    public
      constructor Create();//コンストラクタ
      destructor Destroy;override;       //デストラクタ
      function Open():boolean;           //接続
      function Close():boolean;          //切断
      procedure SendBytes(b:TBytes);     ////バイト配列を送信
      procedure Send(st:String);         //AnsiString(SJIS)でデータ送信
      procedure SendLn(st:String;EOL:String = #13#10);//AnsiString(SJIS)でCR+LFを付けて送信
      function RecvBytes():TBytes;
      function RecvStr():String;         //AnsiString(SJIS)でデータ受信
      function RecvStrDelCRLF():String; //AnsiString(SJIS)でデータを受信して最後がCR+LFの場合はCR+LFを削除
      procedure RecvStringList(RecvStrings:TStrings;Enc:TEncoding); //エンコーディングEncのデータを受信してRecvStringsへ
      procedure RecvStream(strm:TMemoryStream);//ストリームにテータを受信
      procedure SetRecvTimeout(sec:LongInt);
      procedure SetConnectTimeout(sec:LongInt);
      property Port:Word read FPort write FPort;
      property Host:AnsiString read FHost write FHost;
      property Connecting:Boolean read FConnecting;
      property ErrCode:Integer read FErrCode;
      property ErrMsg:String read FErrMsg;
      property OnErrEvent:TMamErrorEvent read FErrEvent write FErrEvent;
      property RecvTimeout:LongInt read FRecvTimeout.tv_sec write SetRecvTimeout;
      property ConnectTimeout:LongInt read FConnectTimeout.tv_sec write SetConnectTimeout;
    end;

// ホスト名からHostEnt情報を取得
function GetPHostEnt(HostName:AnsiString):PHostEnt;
// WinSockのエラー番号からエラー文字列に変換
function GetSocketErrorMessage(errno:integer):String;

var
  WSAData: TWSAData;

implementation

function GetPHostEnt(HostName:AnsiString):PHostEnt;
//ホスト名からIPアドレスをPHostEnt型で取り出す
var addr: Integer;
    phe: PHostEnt;
begin
  phe := gethostbyname(PansiChar(HostName));
  if phe = nil then
  begin
    // IPアドレスでの指定の場合
    addr := inet_addr(PansiChar(HostName));
    phe  := gethostbyaddr(@addr, 4, AF_INET);
  end;
  Result := phe;
end;

function GetSocketErrorMessage(ErrNo:integer):String;
//Socketのエラーメッセージを出力
var st:string;
begin
  st:='error'+IntToStr(ErrNo)+':';
  case ErrNo of
    WSABASEERR:          Result :='No Error';
    WSAEINTR:            Result := st+'Interrupted system call';
    WSAEBADF:            Result := st+'Bad file number';
    WSAEACCES:           Result := st+'Permission denied';
    WSAEFAULT:           Result := st+'Bad address';
    WSAEINVAL:           Result := st+'Invalid argument';
    WSAEMFILE:           Result := st+'Too many open files';
    WSAEWOULDBLOCK:      Result := st+'Operation would block';
    WSAEINPROGRESS:      Result := st+'Operation now in progress';
    WSAEALREADY:         Result := st+'Operation already in progress ';
    WSAENOTSOCK:         Result := st+'Socket operation on non-socket';
    WSAEDESTADDRREQ:     Result := st+'Destination address required';
    WSAEMSGSIZE:         Result := st+'Message too long';
    WSAEPROTOTYPE:       Result := st+'Protocol wrong type for socket';
    WSAENOPROTOOPT:      Result := st+'Bad protocol option';
    WSAEPROTONOSUPPORT:  Result := st+'Protocol not supported';
    WSAESOCKTNOSUPPORT:  Result := st+'Socket type not supported';
    WSAEOPNOTSUPP:       Result := st+'Operation not supported on socket';
    WSAEPFNOSUPPORT:     Result := st+'Protocol family not supported';
    WSAEAFNOSUPPORT:     Result := st+'Address family not supported by protocol family';
    WSAEADDRINUSE:       Result := st+'Address already in use';
    WSAEADDRNOTAVAIL:    Result := st+'Can''t assign requested address';
    WSAENETDOWN:         Result := st+'Network is down';
    WSAENETUNREACH:      Result := st+'Network is unreachable';
    WSAENETRESET:        Result := st+'Net dropped connection or reset';
    WSAECONNABORTED:     Result := st+'Software caused connection abort';
    WSAECONNRESET:       Result := st+'Connection reset by peer';
    WSAENOBUFS:          Result := st+'No buffer space available';
    WSAEISCONN:          Result := st+'Socket is already connected';
    WSAENOTCONN:         Result := st+'Socket is not connected';
    WSAESHUTDOWN:        Result := st+'Can''t send after socket shutdown';
    WSAETOOMANYREFS:     Result := st+'Too many referencescan''t splice';
    WSAETIMEDOUT:        Result := st+'Connection timed out';
    WSAECONNREFUSED:     Result := st+'Connection refused';
    WSAELOOP:            Result := st+'Too many levels of symbolic links';
    WSAENAMETOOLONG:     Result := st+'File name too long';
    WSAEHOSTDOWN:        Result := st+'Host is down';
    WSAEHOSTUNREACH:     Result := st+'No Route to Host';
    WSAENOTEMPTY:        Result := st+'Directory not empty';
    WSAEPROCLIM:         Result := st+'Too many processes';
    WSAEUSERS:           Result := st+'Too many users';
    WSAEDQUOT:           Result := st+'Disc Quota Exceeded';
    WSAESTALE:           Result := st+'Stale NFS file handle';
    WSAEREMOTE:          Result := st+'Too many levels of remote in path';
    WSASYSNOTREADY:      Result := st+'Network SubSystem is unavailable';
    WSAVERNOTSUPPORTED:  Result := st+'WINSOCK DLL Version out of range';
    WSANOTINITIALISED:   Result := st+'Successful WSASTARTUP not yet performed';
    WSAHOST_NOT_FOUND:   Result := st+'Host not found';
    WSATRY_AGAIN:        Result := st+'Non-Authoritative Host not found';
    WSANO_RECOVERY:      Result := st+'Non-Recoverable errors: FORMER RREFUSED NOTIMP';
    WSANO_DATA:          Result := st+'Valid name no data record of requested type';
    else                 Result := st+'Other Error';
  end;
end;

{ TMamTcpClient }

function TMamTcpClient.Close: boolean;
begin
  result:=True;
  if FSocket <> INVALID_SOCKET then
  begin
    //ソケットの送受信を無効にする
    //SD_SEND、SD_RECEIVE、SD_BOTHのいずれかを指定
    if winsock.shutdown(FSocket,SD_BOTH)=SOCKET_ERROR then
    begin
      result:=False;
      FErrCode := WSAGetLastError;
      FErrMsg:=GetSocketErrorMessage(FErrCode);
      if Assigned(FErrEvent) then
        FErrEvent(Self,FErrMsg)
      else
        raise Exception.Create(FErrMsg);
    end;
    //ソケットを閉じる
    if winsock.closesocket(FSocket)=SOCKET_ERROR then
    begin
      FErrCode := WSAGetLastError;
      FErrMsg:=GetSocketErrorMessage(FErrCode);
      if Assigned(FErrEvent) then
        FErrEvent(Self,FErrMsg)
      else
        raise Exception.Create(FErrMsg);
    end;
    FSocket:=INVALID_SOCKET;
    FConnecting := False;
  end
  else
  begin
    result:=False;
    FConnecting:=False;
  end;
end;

constructor TMamTcpClient.Create();
begin
  inherited Create();
  FPort:=80;
  FHost:='';
  FConnecting:=false;
  FErrCode:=0;
  FErrMsg:='';
  FErrEvent:=nil;
  FSocket:=INVALID_SOCKET;
  //バッファサイズ
  FBufSize := 1 * 1024;
  //接続タイムアウト 5秒
  FConnectTimeout.tv_sec:=5;
  FConnectTimeout.tv_usec:=0;
  //読み取りタイムアウト5秒
  FRecvTimeout.tv_sec:=5;
  FRecvTimeout.tv_usec:=0;
end;

destructor TMamTcpClient.Destroy;
begin
  if FConnecting then Close;
  if FSocket <> INVALID_SOCKET then
  begin
  end;
  inherited;
end;

function TMamTcpClient.Open: boolean;
//サーバーへ接続する
var phe:PHostEnt;
    sockaddr:sockaddr_in;
    ret:Integer;
    mode:Integer;
    fds:TFDSet;
begin
  Result:=false;
  if FConnecting then
  begin
    if Assigned(FErrEvent) then
      FErrEvent(Self,'既に接続しています')
    else
      raise Exception.Create('既に接続しています');
    Exit;
  end;

  //ホスト名からIPアドレスをPHostEnt型で取り出す
  phe:=GetPHostEnt(FHost);
  if phe=nil then
  begin
    if Assigned(FErrEvent) then
      FErrEvent(Self,'指定したサーバー('+String(FHost)+')が見つかりません')
    else
      raise Exception.Create('指定したサーバー('+String(FHost)+')が見つかりません。');
    Exit;
  end;

  ZeroMemory(@sockaddr,sizeof(sockaddr));
  sockaddr.sin_family:=AF_INET;
  //ポート番号をリトルエンディアンから
  //ビッグエンディアンへ変換して指定する
  sockaddr.sin_port:=htons(FPort);
  sockaddr.sin_addr:=PInAddr(phe.h_addr_list^)^;//接続先アドレスの指定

  //TCPソケットの作成 SOCK_STREAM=TCP、SOCK_DGRAM=UDP
  //ソケットは
  //  サーバーIP+サーバーPort+クライアントIP+クライアントPort
  //で一意として作られるため接続時に作る(コンストラクタで作らない)
  FSocket:=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
  if FSocket=INVALID_SOCKET then
  begin
    if Assigned(FErrEvent) then
      FErrEvent(Self,'ソケットを作成することができませんでした')
    else
      raise Exception.Create('ソケットを作成することができませんでした');
    exit;
  end;

  //一旦非同期に設定
  mode:=1;
  ioctlsocket(FSocket,FIONBIO,mode);
  FErrMsg:='';
  //サーバーへ接続する
  ret:=WinSock.connect(FSocket,sockaddr,sizeof(sockaddr));
  if ret=SOCKET_ERROR then
  begin
    FErrCode:=WSAGetLastError;
    if FErrCode=WSAEWOULDBLOCK then
    begin
      FD_ZERO(fds);
      FD_SET(FSocket,fds);
      //winsockのselectの第1引数はUnix互換の為で無視される
      ret:=winsock.select(0,nil,@fds,nil,@FConnectTimeout);
      if ret = SOCKET_ERROR then
      begin
        //送信エラーチェック
        FErrCode := WSAGetLastError;
        FErrMsg:=GetSocketErrorMessage(FErrCode);
      end
      else if ret=0 then
      begin
        //接続タイムアウトの場合
        FErrMsg:='Connection Timeout';
      end;
    end
    else
    begin
      FErrMsg:=GetSocketErrorMessage(FErrCode);
    end;
  end;
  //同期に戻す
  mode:=0;
  ioctlsocket(FSocket,FIONBIO,mode);
  //エラーの場合
  if FErrMsg<>'' then
  begin
    winsock.closesocket(FSocket);
    FSocket:=INVALID_SOCKET;
    if assigned(FErrEvent) then
    begin
      FErrEvent(self,FErrMsg);
      exit;
    end
    else
    begin
      raise Exception.Create(FErrMsg);
    end;
  end;
  FConnecting:=True;
  Result:=True;
end;

function TMamTcpClient.RecvBytes: TBytes;
var strm:TMemoryStream;
    b:TBytes;
begin
  strm:=TMemoryStream.Create;
  try
    Self.RecvStream(strm);
    strm.Position:=0;
    SetLength(b,strm.Size);
    strm.Read(b,strm.Size);
    Result:=b;
  finally
    strm.Free;
  end;
end;

function TMamTcpClient.RecvStr: String;
//受信しデータをAnsiString(SJIS)と想定して返す
var strm:TMemoryStream;
    b:TBytes;
begin
  Result := '';
  strm:=TMemoryStream.Create;
  try
    Self.RecvStream(strm);
    strm.Position:=0;
    SetLength(b,strm.Size);
    strm.Read(b,strm.Size);
    Result:=TEncoding.ANSI.GetString(b);
  finally
    strm.Free;
  end;
end;


function TMamTcpClient.RecvStrDelCRLF: String;
//受信しデータをAnsiString(SJIS)と想定して返すが
//受信データの最後がCR+LFの場合は削除する
var st:String;
begin
  st:=self.RecvStr;
  if AnsiRightStr(st,2)=#13#10 then
    result:=AnsiLeftStr(st,Length(st)-2)
  else
    result:=st;
end;


procedure TMamTcpClient.RecvStream(strm: TMemoryStream);
//データをTStreamに受信
var buf: TBytes;
    ret,rets: Integer;
    fds:TFDSet;
    timeout:TTimeVal;
begin
  if FSocket=INVALID_SOCKET then exit;
  strm.Size:=0;
  SetLength(buf, FBufSize);

  timeout.tv_sec:=0;           //秒
  timeout.tv_usec:=1000*100*1; //マイクロ秒
  FD_ZERO(fds);
  FD_SET(FSocket,fds);
  //読み込み可能になるまでタイムアウト時間待つ
  //winsockのselectの第1引数はUnix互換の為で無視される
  rets:=winsock.select(0,@fds,nil,nil,@FRecvTimeout);
  if rets=0 then
  begin
    //読み込みタイムアウトしたので抜ける
    if strm.Size=0 then
    begin
      FErrMsg:='Receive Timeout';
      if assigned(FErrEvent) then
        FErrEvent(self,FErrMsg)
      else
        raise Exception.Create(ErrMsg);
    end;
    exit;
  end;

  while true do
  begin
    FD_ZERO(fds);
    FD_SET(FSocket,fds);
    //読み込み可能になるまでタイムアウト時間待つ
    //winsockのselectの第1引数はUnix互換の為で無視される
    rets:=winsock.select(0,@fds,nil,nil,@timeout);
    if rets=0 then
    begin
      //読み込みタイムアウトしたので抜ける
      if strm.Size=0 then
      begin
        FErrMsg:='Receive Timeout';
        if assigned(FErrEvent) then
          FErrEvent(self,FErrMsg)
        else
          raise Exception.Create(ErrMsg);
      end;
      Break;
    end;

    if FD_ISSET(FSocket,fds) then
    begin
      // 受信バッファを初期化
      ZeroMemory(@buf[0], FBufSize);
      // 受信処理
      ret := WinSock.recv(FSocket, buf[0], FBufSize, 0);
      //受信エラーチェック
      if ret = SOCKET_ERROR then
      begin
        FErrCode := WSAGetLastError;
        FErrMsg:=GetSocketErrorMessage(FErrCode);
        if assigned(FErrEvent) then
          FErrEvent(self,FErrMsg)
        else
          raise Exception.Create(ErrMsg);
      end;
      //受信したデータ数が0なら受信データがもう無いので抜ける
      if ret = 0 then
      begin
        Break;
      end;
      // ストリームに保存
      strm.Write(buf[0],ret);
      if ret<FBufSize then
      begin
        //Break;
      end;
    end
    else
    begin
      Break;
    end;
  end;
end;


procedure TMamTcpClient.RecvStringList(RecvStrings: TStrings;Enc:TEncoding);
//データをTStringsに受信、Encにエンコードを指定する
var
  strm:TMemoryStream;
begin
  //Streamで読み込んでからTStringsに読み込み
  strm:=TMemoryStream.Create;
  try
    Recvstream(strm);
    strm.Position:=0;
    RecvStrings.LoadFromStream(strm,Enc);
    //RecvStrings.LoadFromStream(strm,TEncoding.GetEncoding(20932));
  finally
    strm.Free;
  end;
end;

procedure TMamTcpClient.SendBytes(b: TBytes);
//バイト配列を送信
begin
  if not FConnecting then Exit;
  if length(b)=0 then exit;
  if WinSock.send(FSocket,b[0],Length(b),0)=SOCKET_ERROR then
  begin
    FErrCode := WSAGetLastError;
    FErrMsg:=GetSocketErrorMessage(FErrCode);
    if Assigned(FErrEvent) then
      FErrEvent(Self,FErrMsg)
    else
      raise Exception.Create(ErrMsg);
  end;
end;

procedure TMamTcpClient.Send(st: String);
//文字列をAnsiString(SJIS)で送信する
var b:TBytes;
begin
  if st = '' then Exit;
  b:=TEncoding.ANSI.GetBytes(st);
  Self.SendBytes(b);
end;

procedure TMamTcpClient.SendLn(st: String;EOL: String = #13#10);
//文字列をAnsiString(SJIS)でCR+LF付きで送信する
begin
  Self.Send(st + EOL);
end;

procedure TMamTcpClient.SetConnectTimeout(sec: LongInt);
begin
  if sec<1 then sec:=1;
  if sec>86400 then sec:=86400;
  FConnectTimeout.tv_sec:=sec;
end;

procedure TMamTcpClient.SetRecvTimeout(sec: LongInt);
begin
  if sec<1 then sec:=1;
  if sec>86400 then sec:=86400;
  FRecvTimeout.tv_sec:=sec;
end;

initialization
// WinSock 初期化
  WSAStartup($0101, WSAData);

finalization
// WinSock 終了処理
  WSACleanup;

end.

2.ユニット「MamTcpClient.pas」を使う

uses MamTcpClient;
として使用します。
以下はButton1を押したときに、「http://localhost/delphi/」をGETします。
Button2を押したときに、「http://localhost/delphi/first.html」をGETします。

uses MamTcpClient;

procedure TForm1.Button1Click(Sender: TObject);
var tc:TMamTcpClient;
    ret:boolean;
    b:TBytes;
begin
  tc:=TMamTcpClient.Create();
  //エラー時はsockerrorプロシージャを呼び出す
  tc.OnErrEvent:=sockerror;
  tc.Port:=80;
  tc.Host:='localhost';

  ret:=tc.Open;
  if ret then
  begin
    //文字列+CR+LFを送信する
    tc.SendLn('GET /delphi/ HTTP/1.1');
    tc.SendLn('User-Agent: MyUserAgent[ja]');
    tc.sendLn('Host: hoge.jp');
    //$0D(CR)+$0A(LF)のみを送信する
    tc.SendLn('');
    //受信タイミングが難しい(相手サーバー次第)
    //即時に送ってくれるか、ゆっくり送るか、送った直後に閉じるプロトコルもあるし
    //複数パケットで送ってくるかもしれないし、そもそも何バイト送ってくるかわからない
    //例えばhttpの場合はContent-Lengthヘッダを調べて受信すべきかも
    //とりあえず適当にちょっと待つ
    sleep(500);
    //バイト配列で受信する場合
    b:=tc.RecvBytes;
    //受信したバイト配列をUTF8に変換
    Memo1.Lines.Add(TEncoding.UTF8.GetString(b));
    tc.Close;
  end;
  tc.Free;
end;

procedure TForm1.Button2Click(Sender: TObject);
var tc:TMamTcpClient;
    ret:boolean;
    b:TBytes;
begin
  tc:=TMamTcpClient.Create();
  //エラー時はsockerrorプロシージャを呼び出す
  tc.OnErrEvent:=sockerror;
  tc.Port:=80;
  tc.Host:='localhost';
  ret:=tc.Open;
  if ret then
  begin
    //文字列をバイト配列に変換して送信する場合
    b:=TEncoding.ASCII.GetBytes(
      'GET /delphi/first.html HTTP/1.1'+#13#10+
      'User-Agent: Telnet [ja] (Linux)'+#13#10+
      'Host: hoge.jp'+#13#10+
      #13#10
    );
    tc.SendBytes(b);
    //受信タイミングが難しい(相手サーバー次第)
    //即時に送ってくれるか、ゆっくり送るか、送った直後に閉じるプロトコルもあるし
    //複数パケットで送ってくるかもしれないし、そもそも何バイト送ってくるかわからない
    //例えばhttpの場合はContent-Lengthヘッダを調べて受信すべきかも
    //とりあえず適当にちょっと待つ
    sleep(500);
    //SJIS文字列として受信する場合(UTF8コンテンツは文字化けする)
    Memo1.Lines.Add(tc.RecvStr);
    tc.Close;
  end;
  tc.Free;
end;

Copyright 2019 Mam