勿在浅沙筑高楼。在谈论TServerSocket等组件编写之前,这里先对Winsock中一些基本概念和API函数做一个简单的说明。 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中说明编号。从本质上说,端口号分为下面这三类:“已知”端口、已注册端口、动态和(或)私用端口。 1. 特殊地址
对于特定情况下的套接字行为,有两个特殊IP地址可对它们产生影响。特殊地址INADDR_ANY允许服务器应用监听主机计算机上面每个网络接口上的客户机活动。一般情况下,在该地址绑定套接字和本地接口时,网络应用才利用这个地址来监听连接。如果你有一个多址系统,这个地址就允许一个独立应用接受发自多个接口的回应。 特殊地址INADDR_BROADCAST用于在一个IP网络中发送广播UDP数据报。要使用这个特殊地址,需要应用设置套接字选项SO_BROADCAST。 2. 字节排序 针对“大头”(big-endian)和“小头”(little-endian)形式的编号,不同的计算机处理器的表示方法有所不同,这由各自的设计决定。比如, Intel 86处理器上,用“小头”形式来表示多字节编号:字节的排序是从最无意义的字节到最有意义的字节。在计算机中把IP地址和 端口号指定成多字节数时,这个数就按“主机字节”(host-byte)顺序来表示。但是,如果在网络上指定I P地址和端口号,“互联网联网标准”指定多字节值必须用“大头”形式来表示(从最有意义的字节到最无意义的字节),一般称之为“网络字节”(network-byte)顺序。有一系列的函数可用于多字节数的转换,把它们从主机字节顺序转换成网络字节顺序,反之亦然。下面四个API函数便将一个数从主机字节顺序转换成网络字节顺序: HTONL,htons,WSAHtons,WSAHtonl 下面这四个是前面四个函数的反向函数:它们把网络字节顺序转换成主机字节顺序:ntohl,WSANtohl,ntohs,WSANtohs Winsock的初始化 procedure Startup;
var ErrorCode: Integer; begin ErrorCode := WSAStartup($0101, WSAData); if ErrorCode <> 0 then raise ESocketError.CreateResFmt(@sWindowsSocketError, [SysErrorMessage(ErrorCode), ErrorCode, 'WSAStartup']); end; 错误检查和控制 针对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 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函数 connect函数
关于创建套接字和解析服务器名的方法,前面已有简单叙述,这里介绍最后一步连接的API函数。我们先来看看该函数的Winsock 1版本,其定义如下: function connect(s: TSocket; var name: TSockAddr; namelen: Integer): Integer; stdcall; 该函数的参数是相当清楚的: s是即将在其上面建立连接的那个有效TCP套接字; name是针对TCP(说明连接的服务器)的套接字地址结构(SOCKADDR_IN);namelen则是名字参数的长度。 3、数据传输 SOCKET参数是已建立连接的套接字,将在这个套接字上发送数据。第二个参数buf,则是字符缓冲区,区内包含即将发送的数据。第三个参数len,指定即将发送的缓冲区内的字符数。最后,flags可为0、MSG_DONTROUTE或MSG_OOB。另外, flags还可以是对那些标志进行按位“或运算”的一个结果。MSG_DONTROUTE标志要求传送层不要将它发出的包路由出去。由基层的传送决定是否实现这一请求(例如,若传送协议不支持该选项,这一请求就会被忽略)。MSG_OOB标志预示数据应该被带外发送。对返回数据而言,send返回发送的字节数;若发生错误,就返回SOCKET_ERROR。常见的错误是WSAECONNABORTED,这一错误一般发生在虚拟回路由于超时或协议有错而中断的时候。发生这种情况时,应该关闭这个套接字,因为它不能再用了。远程主机上的应用通过执行强行关闭或意外中断操作重新设置虚拟虚路时,或远程主机重新启动时,发生的则是WSAECONNRESET错误。再次提醒大家注意,发生这一错误时,应该关闭这个套接字。最后一个常见错误是WSAETIMEOUT,它发生在连接由于网络故障或远程连接系统异常死机而引起的连接中断时。 4、流协议 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、closeshocket int closesocket ( SOCKET s ); 如果没有对该套接字的其他引用,所有与其描述符关联的资源都会被释放。其中包括丢弃所有等侯处理的数据。对这个进程中任何一个线程来说,它们执行的待决异步调用都在未投递任何通知消息的情况下被删除。待决的重叠操作也被删除。与该重叠操作关联的任何事件,完成例程或完成端口能执行,但最后会失败,出现WSA_OPERATION_ABORTED错误。还有一点会对closesocket的行为产生影响:套接字选项SO_LINGER是否已经设置。LINGER是“拖延”的意思。SO_LINGER用于控制在未发送的数据排队等候于套接字上的时候,一旦执行了closesocket命令,那么该采取什么样的行动。 应用WinSock建立客户机/服务器程序的活动图 一个典型的客户机/服务器模式的会话程序的顺序图 |
|