80年代初,美国政府的高级研究工程机构(ARPA)给加利福尼亚大学Berkeley分校提供了资金,让他们在UNIX操作系统下实现TCP/IP协议。在这个项目中,研究人员为TCP/IP网络通信开发了一个API(应用程序接口)。这个API称为Socket接口(套接字)。今天,SOCKET接口是TCP/IP网络最为通用的API,也是在INTERNET上进行应用开发最为通用的API。
90年代初,由Microsoft联合了其他几家公司共同制定了一套WINDOWS下的网络编程接口,即WindowsSockets规范。它是BerkeleySockets的重要扩充,主要是增加了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。WINDOWSSOCKETS规范是一套开放的、支持多种协议的Windows下的网络编程接口。从1991年的1.0版到1995年的2.0.8版,经过不断完善并在Intel、Microsoft、Sun、SGI、Informix、Novell等公司的全力支持下,已成为Windows网络编程的事实上的标准。目前,在实际应用中的WINDOWSSOKCETS规范主要有1.1版和2.0版。两者的最重要区别是1.1版只支持TCP/IP协议,而2.0版可以支持多协议。2.0版有良好的向后兼容性,任何使用1.1版的源代码,二进制文件,应用程序都可以不加修改地在2.0规范下使用。
SOCKET实际在计算机中提供了一个通信端口,可以通过这个端口与任何一个具有SOCKET接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个SOCKET接口来实现。在应用开发中就像使用文件句柄一样,可以对SOCKET句柄进行读,写操作。
二:基于WINDOWS SOCKET的应用开发介绍。
在WINDOWS95/98,WINDOWSNT进行WINSOCK开发使用的编程语言有很多,VC++,JAVA,DELPHI,VB等。其中VC时使用最普遍,和WINSOCK结合最紧密的。并且VC++对原来的WindowsSockets库函数进行了一系列封装,继而产生了CAsynSocket、CSocket、CSocketFile等类,它们封装着有关Socket的各种功能,是编程变得更加简单。但如果你是一个WINSOCK编程的初学者,那么建议你在一开始还是学习WINSOCK最基本的API函数进行编程,这样可以大大加深对WINSOCK的了解,对将来很有好处。
在VC中进行WINSOCK的API编程开发,需要使用到下面三个文件:
1 WINSOCK.H: 这是WINSOCK API的头文件。
2 WSOCK32.LIB: WINSOCK API连接库文件。在使用中,一点要把它作为项目的非缺省的连接库包含到项目文件中去。
3 WINSOCK.DLL: WINSOCK的动态连接库,位于WINDOWS的安装目录下。
WINSOCK。DLL位于TCP/IP协议栈和应用程序之间。也就是说,WINSOCK管理与TCP/IP协议的接口。在一开始WINSOCK的应有开发时,你不必对TCP/IP协议有很深刻的了解。但是,如果想成为一个为网络编程的高手,就一定要对下层了解得十分清楚。
在网络编程中最常用的方案便是客户机/服务器模型。在这种方案中客户应用程序向服务器程序请求服务。一个服务程序通常在一个众所周知的地址监听对服务的请求,也就是说,服务进程一直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个时刻,服务程序被“惊醒”并且为客户提供服务-对客户的请求作出适当的反应。虽然基于连接协议(流套接字)的服务是设计客户机/服务器应用程序时的标准,但有些服务也是可以通过无连接协议(数据报套接字)提供的。
一般在使用中,面向连接协议的SOCKET编程模型应用最为广泛,因为面向连接协议提供了一系列的数据纠错功能,可以保证在网络上传输的数据及时、无误地到达对方。 总的来说,使用SOCKET接口(面向连接或无连接)进行网络通信时,必须按下面简单的四步进行处理:1、程序必须建立一个 SOCKET。2、程序必须按要求配置此SOCKET。也就是说,程序要么将此SOCKET连接到远方的主机上,要么给此SOCKET指定一个本地协议端口。
3、程序必须按要求通过此SOCKET发送和接收数据。
4、程序必须关闭此SOCKET。
三:WINSOCK API主要函数简介
作者利用WINSOCK API 编写了一个具有聊天室功能的应用程序,可用作学习 WINSOCK 程序设计的参照。WINSOCK API 包括很多函数,但其中最常用,包括在文章所附源程序中的有:
注:只是有关函数的简要说明,具体规则、说明请参见VC++帮助和WINSOCK规范。
1、WSAStartup():连结应用程序与 Windows Sockets DLL 的第一个函数。
说明: 此函数是应用程序调用 Windows Sockets DLL函数中的第一个,也唯有此函数呼叫成功後,才可以再调用其他 Windows Sockets DLL 的函数。
2、WSACleanup():结束 Windows Sockets DLL 的使用。
说明: 当应用程序不再需要使用 Windows Sockets DLL时,须调用此函数来注销使用,以便释放其占用的资源。
3、 socket():建立Socket。
说明: 此函数用来建立一 Socket 描述字,并为此 Socket 建立其所使用的资源。
4、 closesocket():关闭某一Socket。
说明: 此一函数是用来关闭某一 Socket。
5、 bind():将一本地地址与一个SOCKET描述字连接在一起。
说明:此函数在服务程序上使用,是调用监听函数listen()必须要调用的函数。
6、 listen():设定 Socket 为监听状态,准备被连接。
说明: 此函数在服务程序上使用,来设定 Socket 进入监听状态,并设定最多可有多少个在未真正完成连接前的客户端的连接要求。(目前最大值限制为 5, 最
小值为1)
7、 accept():接受某一Socket的连接要求,以完成面向连接的客户端 Socket 的连接请求。
说明: 服务端应用程序调用此函数来接受客户端Socket 连接请求,accept() 函数的返回值为一新的 Socket,新 Socket 就可用来在服务端和客户端之间的信息传递接收,而原来 Socket 仍然可以接收其他客户端的连接要求。
8、 connect():要求连接某一Socket到指定的网络上服务端。
说明: 此函数用在客户端,用来向服务端要求建立连接。当连接建立完成後,客户端即可利用此 Socket 来与服务端进行信息传递。
9、 recv():从面向连接的 Socket 接收信息。
说明: 此函数用来从面向连接的 Socket 接收信息。
10、send():使用面向连接的 Socket 发送信息。
说明: 此函数用来从面向连接的 Socket 发送信息。
11、WSAAsyncSelect():要求某一 Socket 有事件 (event) 发生时通知使用者。
说明: 此函数用来请求Windows Sockets DLL 为窗口句柄发一条消息-无论它何时检测到由lEvent参数指明的网络事件。要发送的消息由wMsg参数标明.被通知的套接口由s标识。本函数自动将套接口设置为非阻塞模式。
lEvent参数由下表中列出的值组成。
值 意义
FD_READ 欲接收读准备好的通知。
FD_WRITE 欲接收写准备好的通知。
FD_OOB 欲接收带边数据到达的通知。
FD_ACCEPT 欲接收将要连接的通知。
FD_CONNECT 欲接收已连接好的通知。
FD_CLOSE 欲接收套接口关闭的通知。
这个函数可以认为是 WINSOCK API 中最为重要的一个函数。要想使用好这个函数,你必须对 WINDOWS 编程的事件驱动和消息传递有很清楚的了解。
四:聊天室应用程序的设计说明:
软件功能:
Internet上可以提供一种叫IRC 的服务。使用者通过客户端的程序登录到IRC服务器上,就可以与登录在同一IRC服务器上的客户进行交谈,这也就是平常所说的聊天室。在这里,给出了一个在运行TCP/IP协议的网络上实现IRC服务的程序。
软件使用说明:
首先,在一台计算机上运行服务端程序,然后就可以在同一网络的其他计算机上运行客户端程序,登录到服务器上,各个客户之间就可以聊天了。
软件设计要点:
1、服务端
核心代码在 CServerViwe 类中,有一个 SOCKET 变量 m_hServerSocket 和 SOCKET 数组 m_aClientSocket[MAXClient](MAXClient:所定义的接收连接客户的最大数目),m_hServerSocket 用来在指定的端口(>1000)进行侦听,如果有客户端请求连接,则在 m_aClientSocket 数组中查找一个空 socket,将客户端的地址赋予此 socket。
每当一个 ClientSocket 接收到信息,都将会向窗口发一条消息。程序接收到这个消息后,再把接收到的信息发送给每一个 ClientSocket。
2、客户端
客户端比较简单,核心代码在 CClientDlg 类中。只有一个 socket 变量 m_hSocket,与服务端进行连接。连接建立好后,通过此 SOCKET 发送和接收信息。
为了简化设计,用户名在客户端控制,服务器端只进行简单的接收信息和“广播”此信息,不进行名字校验,也就是说,可以有同名客户登录到服务端。这个程序设计虽然简单,但是已经具备了聊天室的最基本的功能。
程序在VC++ 6.0 下编译通过,在使用 TCP/IP 协议的 WINDOWS 95/98 对等局域网 和使用 TCP/IP 协议的 WINDOWS NT 局域网上运行良好
Windows Sockets规范以U.C. Berkeley大学BSD UNIX中流行的Socket接口为范例定义了一套microsoft Windows下网络编程接口。它不仅包含了人们所熟悉的Berkeley Socket风格的库函数;也包含了一组针对Windows的扩展库函数,以使程序员能充分地利用Windows消息驱动机制进行编程。Windows Sockets规范本意在于提供给应用程序开发者一套简单的API,并让各家网络软件供应商共同遵守。此外,在一个特定版本Windows的基础上,Windows Sockets也定义了一个二进制接口(ABI),以此来保证应用Windows Sockets API的应用程序能够在任何网络软件供应商的符合Windows Sockets协议的实现上工作。因此这份规范定义了应用程序开发者能够使用,并且网络软件供应商能够实现的一套库函数调用和相关语义。遵守这套Windows Sockets规范的网络软件,我们称之为Windows Sockets兼容的,而Windows Sockets兼容实现的提供者,我们称之为Windows Sockets提供者。一个网络软件供应商必须百分之百地实现Windows Sockets规范才能做到现Windows Sockets兼容。任何能够与Windows Sockets兼容实现协同工作的应用程序就被认为是具有Windows Sockets接口。我们称这种应用程序为Windows Sockets应用程序。Windows Sockets规范定义并记录了如何使用API与Internet协议族(IPS,通常我们指的是TCP/IP)连接,尤其要指出的是所有的Windows Sockets实现都支持流套接口和数据报套接口.应用程序调用Windows Sockets的API实现相互之间的通讯。Windows Sockets又利用下层的网络通讯协议功能和操作系统调用实现实际的通讯工作。它们之间的关系如图
通信的基础是套接口(Socket),一个套接口是通讯的一端。在这一端上你可以找到与其对应的一个名字。一个正在被使用的套接口都有它的类型和与其相关的进程。套接口存在于通讯域中。通讯域是为了处理一般的线程通过套接口通讯而引进的一种抽象概念。套接口通常和同一个域中的套接口交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。Windows Sockets规范支持单一的通讯域,即Internet域。各种进程使用这个域互相之间用Internet协议族来进行通讯(Windows Sockets 1.1以上的版本支持其他的域,例如Windows Sockets 2)。套接口可以根据通讯性质分类;这种性质对于用户是可见的。应用程序一般仅在同一类的套接口间通讯。不过只要底层的通讯协议允许,不同类型的套接口间也照样可以通讯。用户目前可以使用两种套接口,即流套接口和数据报套接口。流套接口提供了双向的,有序的,无重复并且无记录边界的数据流服务。数据报套接口支持双向的数据流,但并不保证是可靠,有序,无重复的。也就是说,一个从数据报套接口接收信息的进程有可能发现信息重复了,或者和发出时的顺序不同。数据报套接口的一个重要特点是它保留了记录边界。对于这一特点,数据报套接口采用了与现在许多包交换网络(例如以太网)非常类似的模型。
一个在建立分布式应用时最常用的范例便是客户机/服务器模型。在这种方案中客户应用程序向服务器程序请求服务。这种方式隐含了在建立客户机/服务器间通讯时的非对称性。客户机/服务器模型工作时要求有一套为客户机和服务器所共识的惯例来保证服务能够被提供(或被接受)。这一套惯例包含了一套协议。它必须在通讯的两头都被实现。根据不同的实际情况,协议可能是对称的或是非对称的。在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变地认为是主机,而另一方则是从机。一个对称协议的例子是Internet中用于终端仿真的TELNET。而非对称协议的例子是Internet中的FTP。无论具体的协议是对称的或是非对称的,当服务被提供时必然存在"客户进程"和"服务进程"。一个服务程序通常在一个众所周知的地址监听对服务的请求,也就是说,服务进程一直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个时刻,服务程序被"惊醒"并且为客户提供服务-对客户的请求作出适当的反应。这一请求/相应的过程可以简单的用图表示。虽然基于连接的服务是设计客户机/服务器应用程序时的标准,但有些服务也是可以通过数据报套接口提供的。
数据报套接口可以用来向许多系统支持的网络发送广播数据包。要实现这种功能,网络本身必须支持广播功能,因为系统软件并不提供对广播功能的任何模拟。广播信息将会给网络造成极重的负担,因为它们要求网络上的每台主机都为它们服务,所以发送广播数据包的能力被限制于那些用显式标记了允许广播的套接口中。广播通常是为了如下两个原因而使用的:1. 一个应用程序希望在本地网络中找到一个资源,而应用程序对该资源的地址又没有任何先验的知识。2. 一些重要的功能,例如路由要求把它们的信息发送给所有可以找到的邻机。被广播信息的目的地址取决于这一信息将在何种网络上广播。Internet域中支持一个速记地址用于广播-INADDR_BROADCAST。由于使用广播以前必须捆绑一个数据报套接口,所以所有收到的广播消息都带有发送者的地址和端口。
Intel处理器的字节顺序是和DEC VAX处理器的字节顺序一致的。因此它与68000型处理器以及Internet的顺序是不同的,所以用户在使用时要特别小心以保证正确的顺序。任何从Windows Sockets函数对IP地址和端口号的引用和传送给Windows Sockets函数的IP地址和端口号均是按照网络顺序组织的,这也包括了sockaddr_in结构这一数据类型中的IP地址域和端口域(但不包括sin_family域)。考虑到一个应用程序通常用与"时间"服务对应的端口来和服务器连接,而服务器提供某种机制来通知用户使用另一端口。因此getservbyname()函数返回的端口号已经是网络顺序了,可以直接用来组成一个地址,而不需要进行转换。然而如果用户输入一个数,而且指定使用这一端口号,应用程序则必须在使用它建立地址以前,把它从主机顺序转换成网络顺序(使用htons()函数)。相应地,如果应用程序希望显示包含于某一地址中的端口号(例如从getpeername()函数中返回的),这一端口号就必须在被显示前从网络顺序转换到主机顺序(使用ntohs()函数)。由于Intel处理器和Internet的字节顺序是不同的,上述的转换是无法避免的,应用程序的编写者应该使用作为Windows Sockets API一部分的标准的转换函数,而不要使用自己的转换函数代码。因为将来的Windows Sockets实现有可能在主机字节顺序与网络字节顺序相同的机器上运行。因此只有使用标准的转换函数的应用程序是可移植的。
在MFC中MS为套接口提供了相应的类CAsyncSocket和CSocket,CAsyncSocket提供基于异步通信的套接口封装功能,CSocket则是由CAsyncSocket派生,提供更加高层次的功能,例如可以将套接口上发送和接收的数据和一个文件对象(CSocketFile)关联起来,通过读写文件来达到发送和接收数据的目的,此外CSocket提供的通信为同步通信,数据未接收到或是未发送完之前调用不会返回。此外通过MFC类开发者可以不考虑网络字节顺序和忽略掉更多的通信细节。
在一次网络通信/连接中有以下几个参数需要被设置:本地IP地址 - 本地端口号 - 对方端口号 - 对方IP地址。左边两部分称为一个半关联,当与右边两部分建立连接后就称为一个全关联。在这个全关联的套接口上可以双向的交换数据。如果是使用无连接的通信则只需要建立一个半关联,在发送和接收时指明另一半的参数就可以了,所以可以说无连接的通信是将数据发送到另一台主机的指定端口。此外不论是有连接还是无连接的通信都不需要双方的端口号相同。
在创建CAsyncSocket对象时通过调用
BOOL CAsyncSocket::Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long lEvent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE, LPCTSTR lpszSocketAddress = NULL )通过指明lEvent所包含的标记来确定需要异步处理的事件,对于指明的相关事件的相关函数调用都不需要等待完成后才返回,函数会马上返回然后在完成任务后发送事件通知,并利用重载以下成员函数来处理各种网络事件: 标记 事件 需要重载的函数
FD_READ 有数据到达时发生 void OnReceive( int nErrorCode );
FD_WRITE 有数据发送时产生 void OnSend( int nErrorCode );
FD_OOB 收到外带数据时发生 void OnOutOfBandData( int nErrorCode );
FD_ACCEPT 作为服务端等待连接成功时发生 void OnAccept( int nErrorCode );
FD_CONNECT 作为客户端连接成功时发生 void OnConnect( int nErrorCode );
FD_CLOSE 套接口关闭时发生 void OnClose( int nErrorCode );
我们看到重载的函数中都有一个参数nErrorCode,为零则表示正常完成,非零则表示错误。通过int CAsyncSocket::GetLastError()可以得到错误值。
下面我们看看套接口类所提供的一些功能,通过这些功能我们可以方便的建立网络连接和发送数据。
BOOL CAsyncSocket::Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long lEvent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE, LPCTSTR lpszSocketAddress = NULL );用于创建一个本地套接口,其中nSocketPort为使用的端口号,为零则表示由系统自动选择,通常在客户端都使用这个选择。nSocketType为使用的协议族,SOCK_STREAM表明使用有连接的服务,SOCK_DGRAM表明使用无连接的数据报服务。lpszSocketAddress为本地的IP地址,可以使用点分法表示如10.1.1.3。
BOOL CAsyncSocket::Bind( UINT nSocketPort, LPCTSTR lpszSocketAddress = NULL )作为等待连接方时产生一个网络半关联,或者是使用UDP协议时产生一个网络半关联。
BOOL CAsyncSocket::Listen( int nConnectionBacklog = 5 )作为等待连接方时指明同时可以接受的连接数,请注意不是总共可以接受的连接数。
BOOL CAsyncSocket::Accept( CAsyncSocket& rConnectedSocket, SOCKADDR* lpSockAddr = NULL, int* lpSockAddrLen = NULL )作为等待连接方将等待连接建立,当连接建立后一个新的套接口将被创建,该套接口将会被用于通信。
BOOL CAsyncSocket::Connect( LPCTSTR lpszHostAddress, UINT nHostPort );作为连接方发起与等待连接方的连接,需要指明对方的IP地址和端口号。
void CAsyncSocket::Close( );关闭套接口。
int CAsyncSocket::Send( const void* lpBuf, int nBufLen, int nFlags = 0 )
int CAsyncSocket::Receive( void* lpBuf, int nBufLen, int nFlags = 0 );在建立连接后发送和接收数据,nFlags为标记位,双方需要指明相同的标记。
int CAsyncSocket::SendTo( const void* lpBuf, int nBufLen, UINT nHostPort, LPCTSTR lpszHostAddress = NULL, int nFlags = 0 )