My FAQ,最新最全的IT技术教程
最新100篇 | 推荐100篇 | 专题100篇 | 排行榜 | 搜索 | 在线API文档 | 网通镜像
首 页 | 程序开发 | 操作系统 | 软件应用 | 图形图象 | 网络应用 | 精文荟萃 | 教育认证 | 硬件维护 | 未整理篇 | 站长教程
ASP JS PHP工程 ASP.NET 网站建设 UML J2EESUN .NET VC VB VFP 网络维护 数据库 DB2 SQL2000 Oracle Mysql
服务器 Win2000 Office C DreamWeaver FireWorks Flash PhotoShop 上网宝典 CorelDraw 协议大全 网络安全 微软认证
硬件维护  CPU  主板  硬盘  内存  显卡  显示器  键盘鼠标  声卡音箱  打印机  机箱电源  BIOS  网卡  C#  Java  Delphi  vs.net2005
  当前位置:> 程序开发 > 编程语言 > Delphi > 临时文章
ServerSocket,ClientSocket控件源码阅读笔记
作者:linzhengqun 时间:2002-09-29 11:42 出处:互联网 责编:MyFAQ
              摘要:ServerSocket,ClientSocket控件源码阅读笔记

ServerSocketClientSocket控件源码阅读笔记

 

本篇将通过一次Socket通信操作来对ServerSocketClientSocket这两个控件的源码进行一次阅读,希望能理出一个脉络来,以供大家参考。其实说得确切一点,应该是对Scktcomp这个单元进行解读,但由于这个单元的代码太多了,所以不可能面面俱到,我试图以非阻塞式的通信来一步步说明它们是怎么样封装WinSockAPI的,至于阻塞式的,在ServerSocketClientSocket并不常用,所以这一篇就不打算说了,可能源码中会有一些忽略掉,以后有时间来补一篇阻塞式的阅读吧。虽然现在Delphi已经用Indy控件替换了SSCS等网络控件,但毕竟这两个控件是对Socket函数进行封装,学习它怎么样封装也好吧。

在这之前,须对WinSock有一个大概的了解,但也仅止大概,对Socket编程有一个总体的掌握行了,毕竟那不是我的能力所及。想要系统一点的学习Socket,可以去网上下WinSocket的中文文档以及参考MSDN

Socket中分服务端和客户端,它们是怎么样互交的呢,请下面图例:

 

 

3-1就是我们所说的UDP协议,而图3-2就是TCP协议,如果是用纯API编写,大概程序流程就是那上面那样子,这里将说的是图3-2的面向连接的Socket的应用,因为SSCS就是封装这种形式的Socket编程的。

以下是说明这个过程中各个函数的声明

在开始使用WinSockApi的时候,必须加载WinSock Dll的相应版本,这时用到的函数是:

int WSAStartup (
  WORD wVersionRequested,  
  LPWSADATA lpWSAData  
);

wVersionRequested指定用到的WinSock的最低版本,高字节指定副版本,低字节指定主版本,关于WinSock的版本,详见MSDN

lpWSAData结构类型,系统把加载进去的版本信息添加到这个结构中。

该函数成功返回0,不成功则返回几个错误代码之一

这个函数和WSACleanup对应,查看MSDN

MSDN列出一个例子:

WORD wVersionRequested;

WSADATA wsaData;

int err;

 

wVersionRequested = MAKEWORD( 2, 2 );

 

err = WSAStartup( wVersionRequested, &wsaData );

if ( err != 0 ) {

    /* Tell the user that we could not find a usable */

    /* WinSock DLL.                                  */

    return;

}

调用上面的函数之后,就可以用下面的函数来创建一个Socket了,SocketWinSock定义的数据类型(整数),相当于句柄,用于标识系统中的每一个Socket连接

SOCKET socket (
  int af,       
  int type,     
  int protocol  
);

Af协议标志,Internet协议族的标志是PINET

Type 协议类型志,SOCISTREAM表明是面向连接的字节流类型,SOCIDGRAM表明是面向无连接的数据报类型。

Protocol Socket采用的协议类型,如果采用IPPROTO_TCP常数就是采用了TCP协议。

如果调用失败,会返回INVALID_SOCKET值,正常的Socket取值范围是1INVALID_SOCKET-1;

下面的函数将Socket与特定的主机进行绑定:

int bind (
  SOCKET s,                          
  const struct sockaddr FAR*  name,  
  int namelen                        
);

S就是一个Socket,由Socket()函数返回得到的。

Namesockaddr结构的变量

NamelenName结构的长度。

如果函数调用成功,将返回0,如果不成功,将返回SOCKET_ERROR,可以从WSAGetLastError.函数获得错误代码。

现在说sockaddr这个结构,在Winsock中其实有两个功能相似的结构,它们是:

struct sockaddr {

        u_short    sa_family;

        char       sa_data[14];

};

struct sockaddr_in {

        short   sin_family;

        u_short sin_port;

        struct  in_addr sin_addr;

        char    sin_zero[8];

};

这两个结构在Delphi中被声明为一个变体记录,这两个指定通信的本地主机地址,本地协议端口,另外还有通信过程中使用的协议类型。

其中:sin_family规定了哪个协议用来实现套按字连接。WinSock必须设置常数AF_INET

sin_port;WinSock应用程序使用的端口号

sin_addr:这个是32IP地址

sin_zero[8];这个保留,没有使用。

Server端必须调用Bind()函数,设计时可以将地址设定为INADDR_ANY,这样WinSock会自动加入机器正确的地址.

以下是客户端向服务端连接时调用的函数

int connect (
  SOCKET s,                          
  const struct sockaddr FAR*  name,  
  int namelen                        
);

函数里的参数和bind()一个,不多说了,函数成功时返回0,否则返回SOCKET_ERROR

服务端在Bind之后,调用下面函数进行监视。

int listen (
  SOCKET s,    
  int backlog  
);

其中backlog是可以建立的最大连接数,如果值设为SOMAXCONN,将由Socket的服务提供商设定一个合理的值,这个值不确定。

函数成功时返回0,否则返回SOCKET_ERROR

当客户端连接服务端,服务端调用下面函数接收客户的请求,并向客户机发送应答信息

SOCKET accept (
  SOCKET s,                   
  struct sockaddr FAR* addr,  
  int FAR* addrlen            
);

其中中addr是用来保存客户机地址信息的指针

Addrlenaddr的长度一般是Sizeof(addr)

如果函数成功,则返回一个Socket,这个Socket才是与客户实际通信的套接字,原来的那个Socket继续监视其他客户端的连接

以下几个是用个服务机和客户机通信的函数,
int send (SOCKET s,const char FAR * buf, int len,int flags );
在面向连接的情况下发送数据
int recv (SOCKET s, char FAR* buf,  int len, int flags );
在面向连接的情况下接收数据
另外还有sendtorecvfrom用于无连接情况,具体查MSDN
其中Buf是发送或接收的缓冲区,Len是缓冲区的大小,Flags是网络呼叫产生方式标志,值可以为0MSG_DONTROUTE MSG_OOB用途具体看MSDN
通信完毕后,需要关闭Socket,函数声明如下:
int closesocket (
  SOCKET s  
);
在程序的最后,需要调用下面的函数,结束WinSock DLL
int  WSACleanup (void);

函数成功时返回0,否则返回SOCKET_ERROR

 
下面开始TServerSockettTClientSocket的原代码阅读:

不过在分析之前,先要明白,对于WinSock的封装其实是由ScktComp单元的几个类一起完成的,它们的关系是:

TServerSocket继承于TCustomServerSocket

TCustomServerSocket继承于TCustomSocket

TCustomSocket继承于TAbstractSocketTAbstractSockeet的上级则是TComponent

TClientSocket继承于TCustomSocket

另外还有几个类:

TServerWinSocketTClientWinSocket以及TServerClientWinSocket继承TCustomSocket

它们才是真正封装Socket的类。作为上面提到的类的成员存在

此外还有几个用于阻塞式传输的类。这里就忽略不讲了,以后有机会再补上来吧。

 

ServerSocketClientSocket的一次操作流程来跟踪它们的源代码,看看他们是怎么样封WinSocket的。以非阻塞方式来例子。

 

1.  ServerSocket要创建,构造函数如下:

11

constructor TServerSocket.Create(AOwner: TComponent);

begin

  inherited Create(AOwner);

  FServerSocket := TServerWinSocket.Create(INVALID_SOCKET);

  InitSocket(FServerSocket);

  FServerSocket.ThreadCacheSize := 10;

end;

inherited Create(AOwner);是继承自TComponent的构造函数。

接下来创建它的一个成员,这是一个TServerWinSocket的对象,这个才是真正封装SocketApi的类,等一个讨论它。事实上这个构造函数继承自它的父类TCustomServerSocket

接下来调用InitSocket(FServerSocket);这是ServerSocket的祖先类TAbstractSocket的一个方法,传入参数是成员FserverSocket完成的功能是将ServerSocket的事件指针指向TServerWinSocket的事件,使其能处理Socket触发的事件。

最后,设置FServerSocket允许连接的线程数,这里为10

 

好,现在回过头来看看FServerSocket := TServerWinSocket.Create(INVALID_SOCKET);做了什么:

111

constructor TServerWinSocket.Create(ASocket: TSocket);

begin

  FConnections := TList.Create;

  FActiveThreads := TList.Create;

  FListLock := TCriticalSection.Create;

  inherited Create(ASocket);

  FAsyncStyles := [asAccept];

end;

首先创建两个TList对象,一个是FConnections,代表各个处理客户连接的Socket,它对应于属性:property Connections[Index: Integer]: TCustomWinSocket,你可以通过这个属性对各个客户连接进行操作。FActiveThreads 管理由Connections 数组确定的的客户端连接线程TServerClientThread,它对应的属性是ActiveThreads,这个是只读属性,返回当前正在使用的TServerClientThread对象的个数。接下来创建互斥量对象,用于线程同步的处理。

到这里又调用其父类的构造函数,传递的参数就是ServerSocket给的值INVALID_SOCKET,(想想上面提到的这个值的意义)

好,再继续跟踪,到其父类的构造函数去看一下,我们这时应该保留一个问题,按照WinSock的编程模式,刚开始应该是初始化Winsock.DLL,并调用绑定监听函数,这些API是什么在地方被调用呢?

1111

constructor TCustomWinSocket.Create(ASocket: TSocket);

begin

  inherited Create;

  Startup;

  FSocketLock := TCriticalSection.Create;

  FASyncStyles := [asRead, asWrite, asConnect, asClose];

  FSocket := ASocket;

  FAddr.sin_family := PF_INET;

  FAddr.sin_addr.s_addr := INADDR_ANY;

  FAddr.sin_port := 0;

  FConnected := FSocket <> INVALID_SOCKET;

end;

首先调用TObject的构造函数,

再调用Sartup;分析完这个函数再看里面的代码,这里可以猜测它里面会调用WSAStartup函数。

接下来看到从ServerSocket传过来的参数指定给了TCustomWinSocket的成员,还有下面几个成员的设置,可以肯定,这里是对Socket进行初始化,结合开头所讲的知识,再看看源代码。应该不难理解了吧。

再看看Startup的源码:

11111

procedure Startup;

var

  ErrorCode: Integer;

begin

  ErrorCode := WSAStartup($0101, WSAData);

  if ErrorCode <> 0 then

    raise ESocketError.CreateResFmt(@sWindowsSocketError,

      [SysErrorMessage(ErrorCode), ErrorCode, 'WSAStartup']);

end;

这是一个全局函数,看到其中一行重要的代码了,

ErrorCode := WSAStartup($0101, WSAData);WinSock就是在这里被初始化的。

而在服务端没有建立监听之时,Socket就在上面被默认设置了。

好,现在ServerSocket的成员FserverSocket的创建就完成了,再回到最上面,

FServerSocket := TServerWinSocket.Create(INVALID_SOCKET);之后,便是

InitSocket(FServerSocket);了,看看它的代码:

12

procedure TAbstractSocket.InitSocket(Socket: TCustomWinSocket);

begin

  Socket.OnSocketEvent := DoEvent;

  Socket.OnErrorEvent := DoError;

end;

正好和上面所说的一样,现在可以认为当Socket发生了事件(比如连接,接收等)之后,就是调用了DoEvent了,错误发生了也一样。不妨看看DoEvent代码:

121

procedure TAbstractSocket.DoEvent(Sender: TObject; Socket: TCustomWinSocket;

  SocketEvent: TSocketEvent);

begin

  Event(Socket, SocketEvent);

end;

里面是调用Event,再看看Event的声明:

procedure Event(Socket: TCustomWinSocket; SocketEvent: TSocketEvent);virtual; abstract;

这是一个抽象函数,由此可以知道,TAbstractSocket是一个抽象的类,它只是封装了一般的操作,具体地都留到了它的子类中去了,所以它的子类一定覆盖了这个Event方法,而DoEvent中调用的Event实际上就是调用它子类的Event方法,这个就是典型的模板模式。

好,看看它的子类有没有覆盖这个方法,果然在TCustomSocket中看到了这个方法,并实现了它,看看它的源代码:

1211

procedure TCustomSocket.Event(Socket: TCustomWinSocket; SocketEvent: TSocketEvent);

begin

  case SocketEvent of

    seLookup: if Assigned(FOnLookup) then FOnLookup(Self, Socket);

    seConnecting: if Assigned(FOnConnecting) then FOnConnecting(Self, Socket);

    seConnect:

      begin

        FActive := True;

        if Assigned(FOnConnect) then FOnConnect(Self, Socket);

      end;

    seListen:

      begin

        FActive := True;

        if Assigned(FOnListen) then FOnListen(Self, Socket);

      end;

    seDisconnect:

      begin

        FActive := False;

        if Assigned(FOnDisconnect) then FOnDisconnect(Self, Socket);

      end;

    seAccept: if Assigned(FOnAccept) then FOnAccept(Self, Socket);

    seRead: if Assigned(FOnRead) then FOnRead(Self, Socket);

    seWrite: if Assigned(FOnWrite) then FOnWrite(Self, Socket);

  end;

end;

其中FonAccept等这些都是TCusotmSocket的成员,都是TSocketNotifyEvent类型的,TCustomSocket还通过下面这些属性,使该类拥有了这些事件处理的能力:

property OnLookup: TSocketNotifyEvent read FOnLookup write FOnLookup;

    property OnConnecting: TSocketNotifyEvent read FOnConnecting write FOnConnecting;

    property OnConnect: TSocketNotifyEvent read FOnConnect write FOnConnect;

    property OnDisconnect: TSocketNotifyEvent read FOnDisconnect write FOnDisconnect;

    property OnListen: TSocketNotifyEvent read FOnListen write FOnListen;

    property OnAccept: TSocketNotifyEvent read FOnAccept write FOnAccept;

    property OnRead: TSocketNotifyEvent read FOnRead write FOnRead;

property OnWrite: TSocketNotifyEvent read FOnWrite write FOnWrite;

而上面的Event实现,我们可以看到它是根据SocketEvent的类型来调用相应的事件指针的,而它的子类ServerSocketClientSocket也拥有了这些事件的处理能力,到这里我们终于明白了,它们的事件是怎么样出来的,但如果想得知这些事件是怎么触发的,还要看这一句:Socket.OnSocketEventOnSocketEvent到后面再说,这里先略。

错误处理事件原理上也是一样,到后面再说。

 

好了,到这里第一步解读完毕,我们所知道的ServerSocket创建之后所完成事件就是初始化Socket DLL,并指定好事件处理的方法指针和错误处理的方法指针。我们接下来看第二步。

 

 

2.我们创建完ServerSocket后,下步要做什么呢,当然是指定端口啦,然后Open开始监视啦,这个过程会触发一些事件,我们正好看看这些事件是怎么样发生的。

由于这一次操作是非阻塞方式的,所以ServerType默认为stNonBlocking,我们开始设置Port,这个属性的声明在哪里,这个属性一直到跟踪祖先类AbstractSocket中去,因为无论客户端和服务端都是要设置端口的,所以理所当然要封装到最高层的类去了,以让它的子类拥有这些属性:

property Port: Integer read FPort write SetPort;

再看看SetPort方法:

21

procedure TAbstractSocket.SetPort(Value: Integer);

begin

  if FPort <> Value then

  begin

    if not (csLoading in ComponentState) and FActive then

      raise ESocketError.CreateRes(@sCantChangeWhileActive);

    FPort := Value;

  end;

end;

if not (csLoading in ComponentState) and FActive then

      raise ESocketError.CreateRes(@sCantChangeWhileActive);

这一句是防止你在运行时改变端口,最后才将值赋给FPort

好了,设置后Port后,就要Open了,开始监视客户端了。

监视有两个方式,一种是直接设Active属性为True,一种直接调用Open方法。

其中Open方法如下:

procedure TAbstractSocket.Open;

begin

  Active := True;

end;

它还是用到了属性Active,所以我们可以集中来讨论Active属性。

property Active: Boolean read FActive write SetActive;

看看SetActive

22

procedure TAbstractSocket.SetActive(Value: Boolean);

begin

  if Value <> FActive then

  begin

    if (csDesigning in ComponentState) or (csLoading in ComponentState) then

      FActive := Value;

    if not (csLoading in ComponentState) then

      DoActivate(Value);

  end;

end;

我们可以这样认为,当设计时就直接设FActive,当运行时,就调用DoActivate(Value);

TAbstractSocket覆盖了Loaded方法,则当窗体开始运行,从Form文件中开始流入时,也调用了DoActive方法。

再看DoActive:

221

procedure DoActivate(Value: Boolean); virtual; abstract;又是一种抽象方法,得到它的子类去看了,在它的子类TCustomServerSocket找到这个方法:

procedure TCustomServerSocket.DoActivate(Value: Boolean);

begin

  if (Value <> FServerSocket.Connected) and not (csDesigning in ComponentState) then

  begin

    if FServerSocket.Connected then

      FServerSocket.Disconnect(FServerSocket.SocketHandle)

    else FServerSocket.Listen(FHost, FAddress, FService, FPort, SOMAXCONN);

  end;

end;

可以这样理解,不在设计时,当Value不等它的成员FServerSocket.Connected时(我们可先认为这个表示当前的监听状态),而如果此时FServerSocket.ConnectedTrue,则表示ValueFalse,,那么这时要断开连接,调用FServerSocket.Disconnect(FServerSocket.SocketHandle)

很多时候ServerSocket都是调用它的成员FServerSocket来完成操作的,这一点我已经看到了,下面还有更多这样的情况。否则,则ValueTrue,调用

FServerSocket.Listen(FHost, FAddress, FService, FPort, SOMAXCONN);来开始进行监视。好的,现在我们就要打开FServerSocket的源码,看看它是怎么样完成断开连接和开始连接的了。

先看开始监听的:

2211

procedure TServerWinSocket.Listen(var Name, Address, Service: string; Port: Word;

  QueueSize: Integer);

begin

  inherited Listen(Name, Address, Service, Port, QueueSize, ServerType = stThreadBlocking);

  if FConnected and (ServerType = stThreadBlocking) then

    FServerAcceptThread := TServerAcceptThread.Create(False, Self);

end;

这里调用其父类的Listen方法,如果它是阻塞方式的,则还要生成一个线程类,由于我用的非阻塞方式,所以暂不去理它,看看他的父类的Listen方法:

22111

procedure TCustomWinSocket.Listen(const Name, Address, Service: string; Port: Word;

  QueueSize: Integer; Block: Boolean);

begin

  if FConnected then raise ESocketError.CreateRes(@sCannotListenOnOpen);

  FSocket := socket(PF_INET, SOCK_STREAM, IPPROTO_IP);

  if FSocket = INVALID_SOCKET then raise ESocketError.CreateRes(@sCannotCreateSocket);

  try

    Event(Self, seLookUp);

    if Block then

    begin

      FAddr := InitSocket(Name, Address, Service, Port, False);

      DoListen(QueueSize);

    end else

      AsyncInitSocket(Name, Address, Service, Port, QueueSize, False);

  except

    Disconnect(FSocket);

    raise;

  end;

end;

这里第一句重要函数出现了:

FSocket := socket(PF_INET, SOCK_STREAM, IPPROTO_IP);

记得上面Fsockeet曾被赋过值吗,是INVALID_SOCKET;,而这里终于被替回了一个可用的套接字了。

第二个重要的函数:Event(Self, seLookUp);可以先猜测这个方法最终于会调用事件指针,等一下再看。

接下来判断Block的值,回头看看这一句:

inherited Listen(Name, Address, Service, Port, QueueSize, ServerType = stThreadBlocking);

如果为阻塞方式,则为True,否则为False,非阻塞为这时为False,调用下方法:

AsyncInitSocket(Name, Address, Service, Port, QueueSize, False);

好了,再看一下Even吧:

221111

procedure TCustomWinSocket.Event(Socket: TCustomWinSocket; SocketEvent: TSocketEvent);

begin

  if Assigned(FOnSocketEvent) then FOnSocketEvent(Self, Socket, SocketEvent);

end;

不出所料,调用事件指针了,这时ServerSocket的事件就会触发了,回过头去看看,

seLookup: if Assigned(FOnLookup) then FOnLookup(Self, Socket);

这里CustomSocketOnLookup事件发生,但ServerSocket并没有显化它,ClientSocket就有这个事件。

按我们所设的方式,接下来要调用如下函数了(另外一些方法以后再说):

AsyncInitSocket(Name, Address, Service, Port, QueueSize, False);

这函数很大,但所用到的技巧也非常精彩,得好好分析它:

221112

procedure TCustomWinSocket.AsyncInitSocket(const Name, Address,

  Service: string; Port: Word; QueueSize: Integer; Client: Boolean);

var

  ErrorCode: Integer;

begin

  try

    case FLookupState of

      lsIdle:

        begin

          if not Client then

          begin

            FLookupState := lsLookupAddress;

            FAddr.sin_addr.S_addr := INADDR_ANY;

          //下面的情况到客户端时才会用到

          end else if Name <> ' then

          begin

            if FGetHostData = nil then

              FGetHostData := AllocMem(MAXGETHOSTSTRUCT);

         FLookupHandle := WSAAsyncGetHostByName(Handle, CM_LOOKUPCOMPLETE,

              PChar(Name), FGetHostData, MAXGETHOSTSTRUCT);

            CheckSocketResult(Ord(FLookupHandle = 0), 'WSAASyncGetHostByName');

            FService := Service;

            FPort := Port;

            FQueueSize := QueueSize;

            FClient := Client;

            FLookupState := lsLookupAddress;

            Exit;

          end else if Address <> ' then

          begin

            FLookupState := lsLookupAddress;<