Delphi Socket编程(1)

 
这篇文章来自网络;个人感觉有点杂乱;撮合着看下吧。
 
1. Socket 定义:     
    socket非常类似于 电话 插座。以一个国家级电话网为例。
              电话 的通话双方相当于相互通信的2个进程,区号是它的 网络地址 ;区内一个单位的 交换机 相当于一台 主机 ,主机分配给每个用户的局内号码相当于socket号。
        1. 申请一个socket;同时要知道对方的号码,相当于对方有一个固定的socket。
        2.发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出 网络地址 )。
        3.对方假如在场并空闲(相当于通信的另一 主机 开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功
       4.双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。
      5. 通话结束后,一方挂起电话机相当于关闭socket,撤消连接

    

WINSOCK的基本概念
        基层网络协议,Winsock是访问它们的首选接口

            要说明的是,Winsock是网络编程接口,而不是协议。  

  Winsock的API是建立在套接字基础上的。所谓套接字,就是一个指向传输提供者的句柄Win32中,套接字不同于文件描述符,所以它是一个独立的类型—SOCKET。

 

第一个程序
        一个在服务器和客户机之间建立连接并通讯的程序。
            我们使用TServerSocket组件来建立服务器端的程序。
      它包含两个memo组件,用来分别处理接收到的数据和发送的数据,然后再在窗体上方一个TServerSocket组件,ServerType设置为stNonBlocking,Port设置为100,active设置为true,写几句简单的代码如下:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, StdCtrls, ScktComp;

type
  TForm1 = class(TForm)
    ServerSocket1: TServerSocket;
    Memo1: TMemo;
    Button1: TButton;
    StatusBar1: TStatusBar;
    Memo2: TMemo;
    procedure ServerSocket1ClientConnect(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure ServerSocket1ClientDisconnect(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure ServerSocket1ClientRead(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure Button1Click(Sender: TObject);
  private
    ...{ Private declarations }
  public
    ...{ Public declarations }
  end;

var
  Form1: TForm1;

implementation

...{$R *.dfm}

procedure TForm1.ServerSocket1ClientConnect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
StatusBar1.SimpleText := 'connect';
end;

procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
  StatusBar1.SimpleText := 'disconnect';
end;

procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
  Socket: TCustomWinSocket);
begin
   Memo1.Lines.Add( Socket.ReceiveText);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
   if ServerSocket1.Active then
     ServerSocket1.Socket.Connections[0].SendText(Memo2.Text);
end;

end.

{然后编译程序。

客户端实现:
   客户端相对复杂一些,因为我们要有一个用来接收服务器IP地址的edit,以及端口地址的edit。
}

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, StdCtrls, ScktComp;

type
  TForm1 = class(TForm)
    ClientSocket1: TClientSocket;
    edIp: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    edPort: TEdit;
    StatusBar1: TStatusBar;
    Memo1: TMemo;
    btnConnect: TButton;
    btnSent: TButton;
    btnDisconnect: TButton;
    Memo2: TMemo;
    procedure ClientSocket1Connect(Sender: TObject;
      Socket: TCustomWinSocket);
    procedure btnConnectClick(Sender: TObject);
    procedure btnDisconnectClick(Sender: TObject);
    procedure btnSentClick(Sender: TObject);
    procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
  private
    ...{ Private declarations }
  public
    ...{ Public declarations }
  end;

var
  Form1: TForm1;

implementation

...{$R *.dfm}

procedure TForm1.ClientSocket1Connect(Sender: TObject;
  Socket: TCustomWinSocket);
begin
   StatusBar1.SimpleText := '连接成功';
end;

procedure TForm1.btnConnectClick(Sender: TObject);
begin
   if not ClientSocket1.Active then
   begin
     ClientSocket1.Host := edIp.Text;
     ClientSocket1.Port := StrToInt(edPort.Text);
     ClientSocket1.Open;
     (Sender as TButton).Enabled := false;
     btnDisconnect.Enabled := true;
   end;
end;

procedure TForm1.btnDisconnectClick(Sender: TObject);
begin
   if ClientSocket1.Active then
   begin
     ClientSocket1.Close;
     btnDisconnect.Enabled := false;
     btnConnect.Enabled := true;

   end;
end;

procedure TForm1.btnSentClick(Sender: TObject);
begin
   if ClientSocket1.Active then
     ClientSocket1.Socket.SendText(Memo1.Text)
end;

procedure TForm1.ClientSocket1Read(Sender: TObject;
  Socket: TCustomWinSocket);
begin
   Memo2.Lines.Add(Socket.ReceiveText);
end;

end


我们可以使用Winsock来编程一个网络会话程序。

      TClientSocket以及TServerSocket是一个组件,他们的目的是让我们可以建立典型的客户机/服务器模式的通讯程序

 

勿在浅沙筑高楼。在谈论TServerSocket等组件编写之前,这里先对Winsock中一些基本概念和API函数做一个简单的说明。
一、定址
      要通过Winsock建立通信,必须了解如何利用指定的协议为工作站定址。Winsock 2引入了几个新的、与协议无关的函数,它们可和任何一个地址家族一起使用;但是大多数情况下,各协议家族都有自己的地址解析机制,要么通过一个函数,要么作为一个投给getsockopt的选项。
     因为目前网络编程中用的最多最普遍的也许就是TCP/IP协议了,所以这里主要介绍此协议下的WinSock编程

1、IP
   网际协议(Internet Protocol, IP)是一种用于互联网的网络协议,已经广为人知。它可广泛用于大多数计算机操作系统上,也可用于大多数局域网LAN(比如办公室小型网络)和广域网WAN(比如说互联网)。从它的设计看来, IP是一个无连接的协议,不能保证数据投递万
无一失。两个比它高级的协议(TCP和UDP)用于依赖IP协议的数据通信。
2、TCP
       面向连接的通信是通过“传输控制协议”(Transmission Control Protocol, TCP)来完成的。TCP提供两台计算机之间的可靠无错的数据传输。应用程序利用TCP进行通信时,源和目标之间会建立一个虚拟连接。这个连接一旦建立,两台计算机之间就可以把数据当作一个双向字
节流进行交换。
3、UDP
       无连接通信是通过“用户数据报协议”(User Datagram Protocol, UDP)来完成的。UDP不保障可靠数据的传输,但能够向若干个目标发送数据,接收发自若干个源的数据。简单地说,如果一个客户机向服务器发送数据,这一数据会立即发出,不管服务器是否已准备接收数据。如果服务器收到了客户机的数据,它不会确认收到与否。数据传输方法采用的是数据报。
        TCP和UDP两者都利用IP来进行数据传输,一般称为TCP/IP和UDP/IP。Winsock通过AF_INET地址家族为IP通信定址。
4、定址
       IP中,计算机都分配有一个IP地址,用一个32位数来表示,正式的称呼是“IPv4地址”。客户机需要通过TCP或UDP和服务器通信时,必须指定服务器的IP地址和服务端口号。另外,服务器打算监听接入客户机请求时,也必须指定一个IP地址和一个端口号。Winsock中,应用通过SOCKADDR_IN结构来指定I P地址和服务端口信息,该结构的在DELPHI中的声明如下:
  sockaddr_in = record
    case Integer of
      0: (sin_family: u_short;
          sin_port: u_short;
          sin_addr: TInAddr;
          sin_zero: array[0..7] of Char);
      1: (sa_family: u_short;
          sa_data: array[0..13] of Char)
  end;
  TSockAddrIn = sockaddr_in;
        在DELPHI中,sockaddr_in结构被声明为了一个变体记录(关于变体记录可以参看我其他的文章)。sin_family: 字段必须设为AF_INET,以告知Winsock我们此时正在使用I P地址家族。

        准备使用哪个TCP或UDP通信端口来标识服务器服务这一问题,则由sin_port字段定义。在选择端口时,应用必须特别小心,因为有些可用端口号是为“已知的”(即固定的)服务保留的(比如说文件传输协议和超文本传输协议,即FTP和HTTP)。

    “已知的协议”,即固定协议,采用的端口由“互联网编号分配认证(IANA)”控制和分配,RFC 1700中说明编号。从本质上说,端口号分为下面这三类:“已知”端口、已注册端口、动态和(或)私用端口

Winsock的初始化
       每个Winsock应用都必须加载Winsock DLL的相应版本。如果调用Winsock之前,没有加载Winsock库,这个函数就会返回一个SOCKET_ERROR,错误信息是WSANOTINITIALISED
   加载Winsock库是通过调用WSAStartup函数实现的。这个函数在DELPHI中的WinSock单元被定义如下:
   function WSAStartup(wVersionRequired: word; var WSData: TWSAData): Integer; stdcall;
   ScktComp中这样使用了此函数

procedure Startup;
var
  ErrorCode: Integer;
begin
  ErrorCode := WSAStartup($0101, WSAData);
  if ErrorCode <> 0 then
    raise ESocketError.CreateResFmt(@sWindowsSocketError,
      [SysErrorMessage(ErrorCode), ErrorCode, 'WSAStartup']);
end;

错误检查和控制
       对编写成功的Winsock应用程序而言,错误检查和控制是至关重要的。事实上,对Winsock函数来说,返回错误是非常常见的。但是,多数情况下,这些错误都是无关紧要的,通信仍可在套接字上进行。尽管其返回的值并非一成不变,但不成功的Winsock调用返回的最常见的值是SOCKET_ERROR。在详细介绍各个API调用时,我们打算指出和各个错误对应的返回值。实际上,SOCKET_ERROR常量是- 1。
       如果调用一个Winsock函数,错误情况发生了,就可用WSAGetLastError函数来获得一段代码,这段代码明确地表明发生的状况。该函数的定义如下:function WSAGetLastError: Integer; stdcall;
      发生错误之后调用这个函数,就会返回所发生的特定错误的完整代码。

针对TCP/IP的WinSock编程
        因为TCP协议是一个面向连接的协议,它存在一个概念上的“服务器”端和“客户端”,在编码时,要区分对待。
1、服务器端的编程
     “服务器”在某种概念上我们可以理解为一个进程,它需要等待任意数量的客户机连接,以便为它们的请求提供服务。

       对服务器监听的连接来说,它必须在一个已知的名字上。在TCP/IP中,这个名字就是本地接口的I P地址,加上一个端口编号。

      每种协议都有一套不同的定址方案,所以有一种不同的命名方法。在Winsock中,第一步是将指定协议的套接字绑定到它已知的名字上。

       这个过程是通过API调用bind来完成的。下一步是将套接字置为监听模式。这时,用API函数listen来完成的。最后,若一个客户机试图建立连接,服务器必须通过accept或WSAAccept调用来接受连接。
1.socket
   function socket(af, Struct, protocol: Integer): TSocket; stdcall;
      在加载Winsock DLL的相应版本之后,你要做的第一件事就是建立一个套接字了。在1.1版本中通过使用socket这个API来实现。第一个参数是你要使用的协议家族,第二个参数为套接字类型,最后一个参数指名你要使用的具体协议。下面的代码创建了一个使用IP协议家族中的TCP协议创建的流模式的套接字。
skc := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

2. bind
       一旦为某种特定协议创建了套接字,就必须将套接字绑定到一个已知地址。bind函数可将指定的套接字同一个已知地址绑定到一起。该函数声明如下;
function bind(s: TSocket; var addr: TSockAddr; namelen: Integer): Integer; stdcall;
       其中第一个参数s代表我们希望在上面等待客户连接的那个套接字第二个参数addr,针对自己打算使用的那个协议,必须把该参数填充一个地址缓冲区,第三个参数是要传递的、由协议决定的地址的长度。例如这样一段代码

var
  ErrorCode : integer;
  SockAdd_In : TSockAddrIn;
  ...
begin
  ...
  SockAdd_In.sin_family := PF_INET;
  SockAdd_In.sin_port := htons(FPort);
  SockAdd_In.sin_addr.S_addr := htonl(INADDR_ANY);
  ErrorCode := bind(FSock,SockAdd_In,sizeof(SockAdd_In));

       一旦出错, bind就会返回SOCKET_ERROR。对bind 来说,最常见的错误是WSAEADDRINUSE。如使用的是TCP/IP,那么WSAEADDRINUSE就表示另一个进程已经同本地IP接口和端口号绑定到了一起,或者那个IP接口和端口号处于TIME_WAIT状态。假如你针对一个套接字调用bind,但那个套接字已经绑定,便会返回WSAEFFAULT错误。

3. listen
       我们接下来要做的是将套接字置入监听模式。bind函数的作用只是将一个套接字和一个指定的地址关联在一起。指示一个套接字等候进入连接的API函数则是listen,其定义如下:
  function listen(s: TSocket; backlog: Integer): Integer; stdcall;
       第一个参数同样是限定套接字。backlog参数指定了正在等待连接的最大队列长度。这个参数非常重要,因为完全可能同时出现几个服务器连接请求。

      例如,假定backlog参数为2。如果三个客户机同时发出请求,那么头两个会被放在一个“待决”(等待处理)队列中,以便应用程序依次为它们提供服务。而第三个连接会造成一个WSAECONNREFUSED错误。注意,一旦服务器接受了一个连接,那个连接请求就会从队列中删去,以便别人可继续发出请求。backlog参数其实本身就存在着限制,这个限制是由基层的协议提供者决定的。如果出现非法值,那么会用与之最接近的一个合法值来取代。除此以外,对于如何知道实际的backlog值,其实并不存在一种标准手段。与listen对应的错误是非常直观的。到目前为止,最常见的错误是WSAEINVAL。该错误通常意味着,你忘记在listen之前调用bind。否则,与bind调用相反,使用listen时可能收到WSAEADDRINUSE。这个错误通常是在进行bind调用时发生的。

4. accept
      现在,我们已做好了接受客户连接的准备。这是通过accept或WSAAccept函数来完成的。
accept格式如下:
   function accept(s: TSocket; addr: PSockAddr; addrlen: PInteger): TSocket; stdcall;
      其中,参数s是一个限定套接字,它处在监听模式。第二个参数应该是一个有效的SOCKADDR_IN结构的地址,而addrlen应该是SOCKADDR_IN结构的长度。对于属于另一种协议的套接字,应当用与那种协议对应的SOCKADDR结构来替换SOCKADDR_IN。通过对accpet函数的调用,可为待决连接队列中的第一个连接请求提供服务。accept函数返回后,addr结构中会包含发出连接请求的那个客户机的I P地址信息,而addrlen参数则指出结构的长度。此外,accept会返回一个新的套接字描述符,它对应于已经接受的那个客户机连接。对于该客户机后续的所有操作,都应使用这个新套接字。至于原来那个监听套接字,它仍然用于接受其他客户机连接,而且仍处于监听模式。

2、客户机API函数
        客户机要简单得多,建立成功连接所需的步骤也要少得多。客户机只需三步操作:
1) 用socket创建一个套接字。
2) 解析服务器名(以基层协议为准)。
3) 用connect初始化一个连接。

connect函数
     关于创建套接字和解析服务器名的方法,前面已有简单叙述,这里介绍最后一步连接的API函数。我们先来看看该函数的Winsock 1版本,其定义如下:
function connect(s: TSocket; var name: TSockAddr; namelen: Integer): Integer; stdcall;
      该函数的参数是相当清楚的: s是即将在其上面建立连接的那个有效TCP套接字; name是针对TCP(说明连接的服务器)的套接字地址结构(SOCKADDR_IN);namelen则是名字参数的长度。

3、数据传输
      收发数据是网络编程的主题。要在已建立连接的套接字上接收数据,在Winsock 1版本中,可用这个A P I函数:

   function send(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;

       SOCKET参数是已建立连接的套接字,将在这个套接字上发送数据。

      第二个参数buf,则是字符缓冲区,区内包含即将发送的数据。

      第三个参数len,指定即将发送的缓冲区内的字符数。

      最后,flags可为0、MSG_DONTROUTE或MSG_OOB。另外, flags还可以是对那些标志进行按位“或运算”的一个结果。

MSG_DONTROUTE标志要求传送层不要将它发出的包路由出去。由基层的传送决定是否实现这一请求(例如,若传送协议不支持该选项,这一请求就会被忽略)。

MSG_OOB标志预示数据应该被带外发送。对返回数据而言,send返回发送的字节数;若发生错误,就返回SOCKET_ERROR。

常见的错误是WSAECONNABORTED,这一错误一般发生在虚拟回路由于超时或协议有错而中断的时候。发生这种情况时,应该关闭这个套接字,因为它不能再用了。

远程主机上的应用通过执行强行关闭或意外中断操作重新设置虚拟虚路时,或远程主机重新启动时,发生的则是WSAECONNRESET错误。再次提醒大家注意,发生这一错误时,应该关闭这个套接字。

最后一个常见错误是WSAETIMEOUT,它发生在连接由于网络故障或远程连接系统异常死机而引起的连接中断时。
      同样地,在已建立了连接的套接字上接收数据也有个函数:

   function recv(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;
       从API的原型中,我们可以看到,所有关系到收发数据的缓冲都属于简单的char类型。也就是说,这些函数没有“Unicode”版本。所有收发函数返回的错误代码都是SOCKET_ERROR。一旦返回错误,系统就会调用WSAGetLastError获得详细的错误信息。最常见的错误是WSAECONNABORED和WSAECONNRESET。两者均涉及到即将关闭连接这一问题—要么通过超时,要么通过通信方关闭连接。另一个常见错误是WSAEWOULDBLOCK,一般出现在套接字处于非暂停模式或异步状态时。这个错误主要意味着指定函数暂不能完成。

4、流协议
        由于大多面向连接的协议同时也是流式传输协议,所以,在此提一下流式协议。

对于流套接字上收发数据所用的函数,需要明白的是:它们不能保证对请求的数据量进行读取或写入。

比如说,一个2048字节的字符缓冲,准备用send函数来发送它。对send函数而言,可能会返回已发出的少于2048的字节。是因为对每个收发数据的套接字来说,系统都为它们分配了相当充足的缓冲区空间。在发送数据时,内部缓冲区会将数据一直保留到应该将它发到线上为止。几种常见的情况都可导致这一情形的发生。比方说,大量数据的传输可以令缓冲区快速填满。

同时,对TCP/IP来说,还有一个窗口大小的问题。接收端会对窗口大小进行调节,以指出它可以接收多少数据。如果有大量数据涌入接收端,接收端就会将窗口大小设为0,为待发数据做好准备。对发送端来说,这样会强令它在收到一个新的大于0的窗口大小之前,不得再发数据。在使用send调用时,缓冲区可能只能容纳1024个字节,这时,便有必要再提取剩下的1024个字节。

5、中断连接
      一旦完成任务,就必须关掉连接,释放关联到那个套接字句柄的所有资源。要真正地释放与一个开着的套接字句柄关联的资源,执行closesocket调用即可。但要明白这一点,closesocket可能会带来负面影响(和如何调用它有关),即可能会导致数据的丢失。鉴于此,应该在调用closesocket函数之前,利用shutdown函数从容中断连接。接下来,我们来谈谈这两个A P I函数。
1. shutdown
  为了保证通信方能够收到应用发出的所有数据,对一个编得好的应用来说,应该通知接收端“不再发送数据”。同样,通信方也应该如此。这就是所谓的“从容关闭”方法,并由shutdown函数来执行。shutdown的定义如下:
int shutdown (
    SOCKET s, 
    int how 
   );
       how参数可以是下面的任何一个值: SD_RECEIVE、SD_SEND或SD_BOTH。如果是SD_RECEIVE,就表示不允许再调用接收函数。这对底部的协议层没有影响。另外,对TCP套接字来说,不管数据在等候接收,还是数据接连到达,都要重设连接。尽管如此, UDP套接字上,仍然接受并排列接入的数据。如果选择SE_SEND,表示不允许再调用发送函数。对TCP套接字来说,这样会在所有数据发出,并得到接收端确认之后,生成一个FIN包。最后,如果指定SD_BOTH,则表示取消连接两端的收发操作。
2. closesocket
   closesocket函数用于关闭套接字,它的定义如下:
int closesocket (
    SOCKET s 
   );
       如果没有对该套接字的其他引用,所有与其描述符关联的资源都会被释放。其中包括丢弃所有等侯处理的数据。对这个进程中任何一个线程来说,它们执行的待决异步调用都在未投递任何通知消息的情况下被删除。待决的重叠操作也被删除。与该重叠操作关联的任何事件,完成例程或完成端口能执行,但最后会失败,出现WSA_OPERATION_ABORTED错误。还有一点会对closesocket的行为产生影响:套接字选项SO_LINGER是否已经设置。LINGER是“拖延”的意思。SO_LINGER用于控制在未发送的数据排队等候于套接字上的时候,一旦执行了closesocket命令,那么该采取什么样的行动。

应用WinSock建立客户机/服务器程序的活动图

 

一个典型的客户机/服务器模式的会话程序的顺序图

 

I/O控制指令
        一系列套接字I/O控制函数用于在套接字之上,控制I/O的行为,同时获取与那个套接字上进行的I/O操作有关的信息。其中,第一个函数是ioctlsocket,起源于Winsock 1规范,声明如下:
int ioctlsocket (
    SOCKET s,
    long cmd,
    u_long FAR* argp
   );
 
        其中,参数s指定的是要在上面采取I/O操作的套接字描述符,而cmd是一个预定义的标志,用于打算执行的I/O控制命令。最后一个参数argp对应的是一个指针,指向与命令密切相关的一个变量。描述好每个命令之后,再给出要求变量的类型。

标准I/O控制命令
1. FIONBIO
       该命令可在套接字s上允许或禁止“非锁定”(Nonblocking)模式。默认情况下,所有套接字在创建好后,都会自动进入“锁定”套接字。若随FIONBIO这个I/O控制命令来调用ioctlsocket,那么应设置argp,令其传递指向一个“无符号”(无正负号)长整数的指针;若打算启用非锁定模式,应将那个长整数的值设为一个非零值。而若设为0值,意味着套接字进入锁定模式。
       调用WSAAsyncSelect或WSAEventSelect函数的时候,会将套接字自动设为非锁定模式。调用了其中任何一个函数之后,再有任何将套接字设回锁定模式的企图,都会以失败告终,并返回WSAEINVAL 错误。要想将套接字改回锁定模式,应用程序首先必须禁止WSAAsyncSelect。具体的做法是调用WSAAsyncSelect,同时令其lEvent参数等于0。或者调用WSAEventSelect,令lNetworkEvents参数等于0,从而禁止WSAEventSelect
2. FIONREAD
       该命令用于决定可从套接字上自动读入的数据量。对ioctlsocket 来说,argp值会返回一个无符号的整数,其中包含了打算读入的字节数。若套接字s是一个“面向数据流”的套接字(类型为SOCK_STREAM),那么FIONREAD会返回在单独一次接收调用中,可返回的数据总量。要注意的是,若使用这种或其他形式的消息预先“窥视”机制,并一定保证能够返回正确的数据量。若在一个数据报套接字(类型为SOCK_DGRAM)上使用I/O控制命令,返回值就是在套接字上排队的第一条消息的大小。
3. SIOCAT M A R K
       若一个套接字配置成接收带外(OOB)数据,而且已设置成以内嵌方式读取这种OOB数据(通过设置SO_OOBINLINE套接字选项),那么本I/O控制命令就会返回一个布尔值,指出接下来是否准备接收OOB数据。如答案是肯定的,则返回TRUE;否则,便返回FALSE,而且下一次接收操作会返回OOB数据之前的所有或部分数据。对ioctlsocket来说,argp会返回一个指向布尔变量的指针。

套接字模式
      Windows套接字在两种模式下执行I/O操作:锁定和非锁定(阻塞和非阻塞)
     在锁定模式下,在I/O操作完成前,执行操作的Winsock函数(比如send和recv)会一直等候下去,不会立即返回程序(将控制权交还给程序)。而在非锁定模式下, Winsock函数无论如何都会立即返回。
      对于处在锁定模式的套接字,我们必须多加留意,因为在一个锁定套接字上调用任何一个Winsock API函数,都会产生相同的后果—耗费或长或短的时间“等待”。大多数Winsock应用都是遵照一种“生产者-消费者”模型来编制的。在这种模型中,应用程序需要读取(或写入)指定数量的字节,然后以它为基础执行一些计算。这种方式下的使用,一定要注意到阻塞作用产生的副作用,例如,我们编写了了一个“服务器端”的进程,创建一个套接字,然后在主线程中用一个循环接受客户端发起的连接请求,我们用到了ACCEPT函数,那么在阻塞模式下,当没有客户端请求发送时,调用accept函数的线程(这里是主线程)将一直阻塞下去,不会返回,这也就意味着你其他的并发操作无法执行,例如你的程序带有GUI界面,那么你将无法操作窗口上的其他按钮。
        为了解决上述问题,我们注意到阻塞的作用是针对调用它的线程,也就是说,如果我们在主线程中创建一个辅助线程来进行轮循操作,那么虽然此辅助线程可能被阻塞,但不会影响到主线程的工作。
       对锁定套接字来说,它的一个缺点在于:应用程序很难同时通过多个建好连接的套接字通信。使用前述的办法,我们可对应用程序进行修改,令其为连好的每个套接字都分配一个读线程,以及一个数据处理线程。尽管这仍然会增大一些开销,但的确是一种可行的方案。唯一的缺点便是扩展性极差,以后想同时处理大量套接字时,恐怕难以下手。

非锁定模式
        除了锁定模式,我们还可考虑采用非锁定模式的套接字。尽管这种套接字在使用上存在着些许难度,但只要排除了这项困难,它在功能上还是非常强大的。除具备锁定套接字已有的各项优点之外,还进行了少许扩充,功能更强。将一个套接字置为非锁定模式之后, Winsock API调用会立即返回。大多数情况下,这些调用都会“失败”,并返回一个WSAEWOULDBLOCK错误。什么意思呢?它意味着请求的操作在调用期间没有时间完成。举个例子来说,假如在系统的输入缓冲区中,尚不存在“待决”的数据,那么recv(接收数据)调用就会返回WSAEWOULDBLOCK错误。通常,我们需要重复调用同一个函数,直至获得一个成功返回代码。
       由于非锁定调用会频繁返回WSAEWOULDBLOCK错误,所以在任何时候,都应仔细检查所有返回代码,并作好“失败”的准备。许多程序员易犯的一个错误便是连续不停地调用一个函数,直到它返回成功的消息为止。
       锁定和非锁定套接字模式都存在着优点和缺点。其中,从概念的角度说,锁定套接字更易使用。但在应付建立连接的多个套接字时,或在数据的收发量不均,时间不定时,却显得极难管理。而另一方面,假如需要编写更多的代码,以便在每个Winsock调用中,对收到一个WSAEWOULDBLOCK错误的可能性加以应付,那么非锁定套接字便显得有些难于操作。在这些情况下,可考虑使用“套接字I / O模型”,它有助于应用程序通过一种异步方式,同时对一个或多个套接字上进行的通信加以管理。

一个例子:
   为了阐述锁定模式和非锁定模式的区别,可以用下面这个例子来演示:


unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,winsock, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    ckbxB: TCheckBox;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    ...{ Private declarations }
  public
    ...{ Public declarations }
  end;
  TSockReadThread= class(TThread)
    private
      FSocket : TSocket;
      FBuf : array[0..255] of Char;
      FMemo : TMemo;
      procedure GetResult;
    protected
      procedure Execute;override;
    public
      constructor Create(pSocket : TSocket;mm : TMemo);
  end;

var
  Form1: TForm1;
  WSAData : TWSAData;

implementation

...{$R *.dfm}

procedure StartUp;
var
  ErrorCode : integer;
begin
  //加载winSock dll
  ErrorCode := WSAStartup($0101, WSAData);
  if ErrorCode <> 0 then
  begin
    ShowMessage('加载失败');
    exit;
  end;
end;
//创建一个服务器
procedure TForm1.Button1Click(Sender: TObject);
var
  ErrorCode,AddSize  : integer;
  SockAdd_In,Add: TSockAddrIn;
  tm : Longint;
  WSAData : TWSAData;
  FSock,AcceptSock : TSocket;
begin

  //创建一个使用TCP协议的套接字
  FSock := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  if FSock = SOCKET_ERROR then
  begin
    showmessage(Format('%s;ErrorCode:%d',['套接字创建失败',WSAGetLastError]) );
    Exit;
  end;
  //根据一个TCheckBox控件的选择情况来决定使用锁定模式还是非锁定模式
  if ckbxB.Checked then
   tm := 1 //非锁定模式
  else tm := 0;  //锁定模式
  ioctlsocket(FSock,FIONBIO,tm);

  SockAdd_In.sin_family := PF_INET;
  SockAdd_In.sin_port := htons(5151);
  SockAdd_In.sin_addr.S_addr := htonl(INADDR_ANY);
  //绑定
  ErrorCode := bind(FSock,SockAdd_In,sizeof(SockAdd_In));
  if ErrorCode = SOCKET_ERROR then
  begin
    showmessage(Format('%s;ErrorCode:%d',['绑定失败:',WSAGetLastError]) );
    Exit;
  end;
  //置为监听模式
  listen(FSock,5);

    //用一个循环来反复判断是否有客户端请求,如果存在请求就创建一个用来接受数据的读取线程
  while true do
  begin
    AddSize := sizeof(Add);
    AcceptSock := accept(FSock,@Add,@AddSize);
    if AcceptSock <> INVALID_SOCKET then
    TSockReadThread.Create(AcceptSock,Memo1);
    Application.ProcessMessages;

  end;

end;

...{ TSockReadThread }

constructor TSockReadThread.Create(pSocket: TSocket; mm: TMemo);
begin
   FMemo := mm;
   FSocket := pSocket;
   inherited Create(false);
end;

procedure TSockReadThread.Execute;
var
  ret : integer;
  FdSet : TFDSet;
  TimeVal : TTimeVal;
begin
  inherited;
  FreeOnTerminate := True;
  while not terminated do
  begin
   ...{ FD_ZERO(FdSet);
    FD_SET(FSocket,FdSet);
    TimeVal.tv_sec := 0;
    TimeVal.tv_usec := 500;
    if (select(0,@fdSet,nil,nil,@TimeVal) > 0) and
      not terminated then
    begin  }
      ret := recv(FSocket,fbuf,256,0);
      if ret > 0 then Synchronize(GetResult)
      else Break;
   // end;
  end;
end;

procedure TSockReadThread.GetResult;
begin
  FMemo.Lines.Add(FBuf);
end;
//创建客户端,并发送数据
procedure TForm1.Button2Click(Sender: TObject);
var
  ErrorCode : integer;
  buf : array[0..10] of Char;
  SockAdd_Inc : TSockAddrIn;
  SkC : TSocket;
begin
  skc := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  if skc = SOCKET_ERROR then
  begin
    showmessage('创建失败');
    Exit;
  end;
  SockAdd_Inc.sin_family := PF_INET;
  SockAdd_Inc.sin_port := htons(5151);
  SockAdd_Inc.sin_addr.S_addr := inet_addr(pchar('127.0.0.1'));
  //连接
  connect(skc,SockAdd_Inc,sizeof(SockAdd_Inc));
  //发送数据
  buf :='wudi_1982';
  send(SkC,buf,10*sizeof(char),0);
  //断开连接
  shutdown(skc,SD_SEND);
  closesocket(skc);

end;

initialization
  StartUp;
finalization
  WSACleanup;

end.


       在上面的例子中,首先通过点击一个按钮创建一个服务器,如果选择的是阻塞模式,你可以发现程序就想“死”了一样,这是阻塞作用产生的效果,因为上面例子调用accept函数的地方是在主线程中,而此时没有客户端发起连接,因此accept将无法返回,主线程被阻塞。这种情况下,你根本无法点击那个用来创建客户端并发送数据的按钮。然后再此执行程序,使用非阻塞模式,你会看到程序执行成功,创建客户端按钮可以执行。如果有兴趣,最好在两种模式下使用单步执行,来看以下效果,主要是关产accept函数执行的情况。当然,你还可以把用来接受客户端请求的那段代码封装到一个线程中去做,例如上面例子的读取线程。
       如果你仔细关产上面代码,你可以看到在读取线程中,有一段被我注释掉的程序,它的主体是select,它是干什么的呢?下面我们就进入Winsock i/o模式。

套接字I/O模型
      共有五种类型的套接字I/O模型,可让Winsock应用程序对I/O进行管理,它们包括: select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及completion port(完成端口)。因为本文的出发点是DELPHI中的TServerSocket控件,基于此控件的实现,在这里,我打算向大家解释主要解释select以及WSAAsyncSelectI/O模型。

select模型
      select(选择)模型是Winsock中最常见的I/O模型。之所以称其为“ select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理!最初设计该模型时,主要面向的是某些使用Unix操作系统的计算机,它们采用的是Berkeley套接字方案。select模型已集成到Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。由于Winsock 1.1向后兼容于Berkeley套接字实施方案,所以假如有一个Berkeley套接字应用使用了select函数,那么从理论角度讲,毋需对其进行任何修改,便可正常运行。
       利用select函数,我们判断套接字上是否存在数据,或者能否向一个套接字写入数据。之所以要设计这个函数,唯一的目的便是防止应用程序在套接字处于锁定模式中时,在一次I / O绑定调用(如send或recv)过程中,被迫进入“锁定”状态;同时防止在套接字处于非锁定模式中时,产生WSAEWOULDBLOCK错误。除非满足事先用参数规定的条件,否则select函数会在进行I/O操作时锁定。select的函数原型如下:
int select (
    int nfds,
    fd_set FAR * readfds,
    fd_set FAR * writefds,
    fd_set FAR * exceptfds,
    const struct timeval FAR * timeout
   );
       其中,第一个参数nfds会被忽略。之所以仍然要提供这个参数,只是为了保持与早期的Berkeley套接字应用程序的兼容。大家可注意到三个fd_set参数:一个用于检查可读性(readfds),一个用于检查可写性(writefds),另一个用于例外数据(exceptfds)。从根本上说,fd_set数据类型代表着一系列特定套接字的集合。其中,readfds集合包括符合下述任何一个条件的套接字:

■ 有数据可以读入。
■ 连接已经关闭、重设或中止。
■ 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
writefds集合包括符合下述任何一个条件的套接字:
■ 有数据可以发出。
■ 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
最后,exceptfds集合包括符合下述任何一个条件的套接字:

■ 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
■ 有带外(OOB)数据可供读取。

      例如,假定我们想测试一个套接字是否“可读”,必须将自己的套接字增添到readfds集合,再等待select函数完成。select完成之后,必须判断自己的套接字是否仍为readfds集合的一部分。若答案是肯定的,便表明该套接字“可读”,可立即着手从它上面读取数据。在三个参数中(readfds、writefds和exceptfds),任何两个都可以是空值( NULL);但是,至少有一个不能为空值!在任何不为空的集合中,必须包含至少一个套接字句柄;否则, select函数便没有任何东西可以等待。最后一个参数timeout对应的是一个指针,它指向一个timeval结构,用于
决定select最多等待I/O操作完成多久的时间。如timeout是一个空指针,那么select调用会无限期地“锁定”或停顿下去,直到至少有一个描述符符合指定的条件后结束。对timeval结构的
定义如下:
  timeval = record
    tv_sec: Longint;
    tv_usec: Longint;
  end;
       其中,tv_sec字段以秒为单位指定等待时间;tv_usec字段则以毫秒为单位指定等待时间。若将超时值设置为( 0 , 0),表明select会立即返回,允许应用程序对select操作进行“轮询”。出于对性能方面的考虑,应避免这样的设置。select成功完成后,会在fd_set结构中,返回刚好有未完成的I / O操作的所有套接字句柄的总量。若超过timeval设定的时间,便会返回0。不管由于什么原因,假如select调用失败,都会返回SOCKET_ERROR。用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄分配给一个集合,设置好一个或全部读、写以及例外fd_set结构。将一个套接字分配给任何一个集合后,再来调用select,便可知道一个套接字上是否正在发生上述的I / O活动。
        例如上面的例程,在阻塞模式下,我们将while 循环调用accept的那段代码做如下修改,执行一下,你会发现在阻塞模式下,刚才无法完成的动作现在可以了。

var
  FdSet : TFDSet;
  TimeVal : TTimeVal;
  ...
begin
  //前面的代码不便
    while true do
  begin
    FD_ZERO(FdSet);
    FD_SET(FSock,FdSet);
    TimeVal.tv_sec := 0;
    TimeVal.tv_usec := 500;
    //使用select函数
    if (select(0,@fdSet,nil,nil,@TimeVal) > 0) then
    begin
    AddSize := sizeof(Add);
    AcceptSock := accept(FSock,@Add,@AddSize);
    if AcceptSock <> INVALID_SOCKET then
    TSockReadThread.Create(AcceptSock,Memo1);
    end;
    Application.ProcessMessages;  end;
end;

WSAAsyncSelect
      Winsock提供了一个有用的异步I/O模型。利用这个模型,应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。该模型最早出现于Winsock的1.1版本中,用于帮助应用程序开发者面向一些早期的16位Windows平台,适应其“落后”的多任务消息环境。应用程序仍可从这种模型中得到好处,特别是它们用一个标准的Windows例程(常称为“ winproc”),对窗口消息进行管理的时候。

消息通知
        要想使用WSAAsyncSelect模型,程序必须具备一个窗口,然后有消息循环系统,我们通常会自定义一个消息,然后调用WSAAsyncSelect函数将此消息投递到制定的窗口句柄中。
WSAAsyncSelect函数定义如下:
int WSAAsyncSelect (
    SOCKET s,
    HWND hWnd,
    unsigned int wMsg,
    long lEvent
   );
       其中, s参数指定的是我们感兴趣的那个套接字。hWnd参数指定的是一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口或对话框。wMsg参数指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。最后一个参数是lEvent,它指定的是一个位掩码,对应于一系列网络事件的组合,应用程序感兴趣的便是这一系列事件。大多数应用程序通常感兴趣的网络事件类型包括: FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT和FD_CLOSE。当然,到底使用FD_ACCEPT,还是使用FD_CONNECT类型,要取决于应用程序的身份到底是一个客户机呢,还是一个服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位O R(或)运算,然后将它们分配给lEvent就可以了。
       特别要注意的是,多个事件务必在套接字上一次注册!另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,或者由应用程序针对那个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知。
       若应用程序针对一个套接字调用了WSAAsyncSelect ,那么套接字的模式会从“锁定”自动变成“非锁定”,我们在前面已提到过这一点。

事件类型        含义
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_OOB 应用程序想接收是否有带外( O O B)数据抵达的通知
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接或者多点join操作完成的通知
FD_CLOSE 应用程序想接收与套接字关闭有关的通知
FD_QOS 应用程序想接收套接字“服务质量”(Q o S)发生更改的通知
FD_GROUP_QOS 应用程序想接收套接字组“服务质量”发生更改的通知(现在没什么用处,为未来套接字组的使用保留)
FD_ROUTING_INTERFACE_CHANGE 应用程序想接收在指定的方向上,与路由接口发生变化的通知
FD_ADDRESS_LIST_CHANGE应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知
       应用程序在一个套接字上成功调用了WSAAsyncSelect 之后,应用程序会在与hWnd窗口句柄参数对应的窗口例程中,以Windows消息的形式,接收网络事件通知。
       就我们的情况来说,感兴趣的是WSAAsyncSelect 调用中定义的消息。wParam参数指定在其上面发生了一个网络事件的套接字。假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。在lParam参数中,包含了两方面重要的信息。其中, lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何错误代码。
        网络事件消息抵达一个窗口例程后,应用程序首先应检查lParam的高字位,以判断是否在套接字上发生了一个网络错误。若应用程序发现套接字上没有产生任何错误,接着便应调查到底是哪个网络事件类型,造成了这条Windows消息的触发—具体的做法便是读取lParam之低字位的内容。此时可使用另一个特殊的宏:WSAGetSelectEvent(在DELPHI中,它以一个函数的形式存在),用它返回lParam的低字部分。

一个例子:

Unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls,winsock;

const
  //自定义一个消息
  WM_MySockMessage=wm_user + $0101;

type
  TForm1 = class(TForm)
    Button1: TButton;
    ListBox1: TListBox;
    mmSRec: TMemo;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    //消息处理过程
    procedure SockMessage(var msg : TMessage);message WM_MySockMessage;
  public
    ...{ Public declarations }
  end;

var
  Form1: TForm1;
  WSAData : TWSAData;

implementation

...{$R *.dfm}

procedure StartUp;
var
  ErrorCode : integer;
begin
  ErrorCode := WSAStartup($0101, WSAData);
  if ErrorCode <> 0 then
  begin
    ShowMessage('加载winsock dll失败');
    exit;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  ErrorCode : integer;
  SockAdd_In : TSockAddrIn;
  FSock : TSocket;
begin
  //建立套接字
  FSock := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  if FSock = SOCKET_ERROR then
  begin
    showmessage(Format('%s;ErrorCode:%d',['套接字建立失败',WSAGetLastError]) );
    Exit;
  end;

  SockAdd_In.sin_family := PF_INET;
  SockAdd_In.sin_port := htons(5150);
  SockAdd_In.sin_addr.S_addr := htonl(INADDR_ANY);
  ErrorCode := bind(FSock,SockAdd_In,sizeof(SockAdd_In));
  if ErrorCode = SOCKET_ERROR then
  begin
    showmessage(Format('%s;ErrorCode:%d',['绑定套接字失败',WSAGetLastError]) );
    Exit;
  end;
  //注意这里
  WSAAsyncSelect(FSock,Form1.Handle ,WM_MySockMessage,FD_READ or FD_ACCEPT);

  //进入监听状态
  listen(FSock,5);
end;

//对自定义消息的处理过程
procedure TForm1.SockMessage(var msg: TMessage);
var
  AddSize,size,rev : integer;
  Acceptsk : TSocket;
  Sockin,Add : TSockAddrIn;
  buf : array[0..255] of char;
begin
   case WSAGetSelectEvent(msg.LParam) of
     FD_READ : begin//如果有数据可以读取,调用recv
        rev := recv(msg.WParam,buf,256,0);
        if  rev > 0 then
        begin
          Form1.mmSRec.Lines.Add(buf);
        end;
     end;
     FD_ACCEPT : begin
       //存在连接请求,调用accept
       AddSize := sizeof(Add);
       Acceptsk := accept(msg.WParam,@Add,@AddSize);
       if Acceptsk <> INVALID_SOCKET then
       begin
          FillChar(SockIn, SizeOf(SockIn), 0);
          size := SizeOf(SockIn);
          //将请求连接的客户端地址添加到一个listbox
          getpeername(Acceptsk,SockIn,size);
          Form1.ListBox1.Items.Add(inet_ntoa(SockIn.sin_addr));

       end;
     end;

   end;
end;

//客户端代码
procedure TForm1.Button2Click(Sender: TObject);
var
  ErrorCode : integer;
  buf : array[0..10] of Char;
  SockAdd_Inc : TSockAddrIn;
  SkC : TSocket;
begin

  skc := socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  if skc = SOCKET_ERROR then
  begin
    showmessage('套接字建立失败');
    Exit;
  end;
  SockAdd_Inc.sin_family := PF_INET;
  SockAdd_Inc.sin_port := htons(5150);
  SockAdd_Inc.sin_addr.S_addr := inet_addr(pchar('192.168.1.3'));
  //连接
  connect(skc,SockAdd_Inc,sizeof(SockAdd_Inc));
  //发送数据
  buf :='wudi_1982';
  send(SkC,buf,10*sizeof(char),0);
  shutdown(skc,SD_SEND);
  closesocket(skc);

end;

initialization
  StartUp;
finalization
  WSACleanup;
end.

I/O模型的问题
       现在,对于如何挑选最适合自己应用程序的I/O模型,大家心中可能还没什么数。前面已经提到,每种模型都有自己的优点和缺点。同开发一个简单的锁定模式应用相比(运行许多服务线程),其他每种I/O模型都需要更为复杂的编程工作。因此,针对客户机和服务器应用的开发,我们分别提供了下述建议。
1. 客户机的开发
   若打算开发一个客户机应用,令其同时管理一个或多个套接字,那么建议采用重叠I/O或WSAEventSelect模型(关于这两种模型,以后会有涉及),以便在一定程度上提升性能。然而,假如开发的是一个以Windows为基础的应用程序,要进行窗口消息的管理,那么WSAAsyncSelect模型恐怕是一种最好的选择,因为WSAAsyncSelect本身便是从Windows消息模型借鉴来的。若采用这种模型,我们的程序一开始便具备了处理消息的能力。
2. 服务器的开发
若开发的是一个服务器应用,要在一个给定的时间,同时控制几个套接字,建议大家采用重叠I/O模型,这同样是从性能出发点考虑的。但是,如果预计到自己的服务器在任何给定的时间,都会为大量I/O请求提供服务,便应考虑使用I/O完成端口模型,从而获得更好的性能。

转自:http://www.cnblogs.com/linyawen/archive/2010/12/16/1908564.html

/*文章太长了,没什么耐性看下去。应该说TServerSocket以及TClientSocket是两个组件,其组件源码中也是使用这个TSockAddrIn结构

也就是说可以使用TServerSocket以及TClientSocket组件开发网络程序,也可以使用这个TSockAddrIn结构开发网络程序。

这个笔记纯属个人理解,目的是为了要明确学习方向,因为网络上的文章多而且零乱*/

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页