同步异步与阻塞和非阻塞

来源:互联网 发布:兴复汉室知乎 编辑:程序博客网 时间:2024/05/07 19:54

同步异步指的是通信模式,而阻塞和非阻塞指的是在接收和发送时是否等待动作完成才返回所以不能混淆这四个词。
以下是我的一些理解,请大家多指教
     首先是通信的同步,主要是指客户端在发送请求后,必须得在服务端有回应后才发送下一个请求。所以这个时候的所有请求将会在服务端得到同步
     其次是通信的异步,指客户端在发送请求后,不必等待服务端的回应就可以发送下一个请求,这样对于所有的请求动作来说将会在服务端得到异步,这条请求的链路就象是一个请求队列,所有的动作在这里不会得到同步的。


阻塞和非阻塞只是应用在请求的读取和发送。
在实现过程中,如果服务端是异步的话,客户端也是异步的话,通信效率会很高,但如果服务端在请求的返回时也是返回给请求的链路时,客户端是可以同步的,这种情况下,服务端是兼容同步和异步的。相反,如果客户端是异步而服务端是同步的也不会有问题,只是处理效率低了些。

同步=阻塞式,异步=非阻塞式
同步和异步都只针对于本机SOCKET而言的
同步模式下,比如RECIEV和SEND,都要确保收到或发送完才返回,继续执行下面的代码不然就阻塞在哪里,所以,同步模式下,一般要用到线程来处理。
异步模式就不同了,不管有没有收到或发送出去,他都马上返回,继续执行下面的代码,结果又消息通知。

 

套接字模式
      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, SkinCaption, WinSkinData;

type
TForm1 = class(TForm)
    ckbxB: TCheckBox;
    Memo1: TMemo;
    Button1: TButton;
    Button2: TButton;
    SkinData1: TSkinData;
    SkinCaption1: TSkinCaption;
    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模型
      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;

原创粉丝点击