WinSock API 总结

 WinSock API 总结
 
我的 Windows Socket API 使用经验
 
本文是我在进行 MS-Windows HP-Unix网络 编程的实践过程中总结出来的一些经验,仅供大家参考。本文所谈到的 Socket 函数如果没有特别说明,都是指的 Windows Socket API
 
一、 WSAStartup 函数
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData );
使用 Socket 的程序在使用 Socket 之前必须调用 WSAStartup 函数。该函数的第一个参数指明程序请求使用的 Socket 版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的 Socket 的版本信息。当一个应用程序调用 WSAStartup 函数时,操作系统根据请求的 Socket 版本来搜索相应的 Socket 库,然后绑定找到的 Socket 库到该应用程序中。以后应用程序就可以调用所请求的 Socket 库中的其它 Socket 函数了。该函数执行成功后返回 0
例:假如一个程序要使用 2.1 版本的 Socket, 那么程序代码如下
wVersionRequested = MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
 
二、 WSACleanup 函数
int WSACleanup (void);
应用程序在完成对请求的 Socket 库的使用后,要调用 WSACleanup 函数来解除与 Socket 库的绑定并且释放 Socket 库所占用的系统资源。
 
三、 socket 函数
SOCKET socket(int af, int type, int protocol );
应用程序调用 socket 函数来创建一个能够进行 网络 通信的套接字。第一个参数指定应用程序使用的通信协议的协议族,对于 TCP/IP 协议族,该参数置 PF_INET; 第二个参数指定要创建的套接字类型,流套接字类型为 SOCK_STREAM 、数据报套接字类型为 SOCK_DGRAM ;第三个参数指定应用程序所使用的通信协议。该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回 INVALID_SOCKET 。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。下面是一个创建流套接字的例子:
struct protoent *ppe;
ppe=getprotobyname("tcp");
SOCKET ListenSocket = socket(PF_INET,SOCK_STREAM,ppe->p_proto);
 
四、 closesocket 函数
int closesocket(SOCKET s );
closesocket 函数用来关闭一个描述符为 s 套接字。由于每个进程中都有一个套接字描述符表,表中的每个套接字描述符都对应了一个位于操作系统缓冲区中的套接字数据结构,因此有可能有几个套接字描述符指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构的被引用次数,即有多少个套接字描述符指向该结构。当调用 closesocket 函数时,操作系统先检查套接字数据结构中的该字段的值,如果为 1 ,就表明只有一个套接字描述符指向它,因此操作系统就先把 s 在套接字描述符表中对应的那条表项清除,并且释放 s 对应的套接字数据结构;如果该字段大于 1 ,那么操作系统仅仅清除 s 在套接字描述符表中的对应表项,并且把 s 对应的套接字数据结构的引用次数减 1 closesocket 函数如果执行成功就返回 0 ,否则返回 SOCKET_ERROR
 
五、 send 函数
int send(SOCKET s,  const char FAR *buf,  int len, int flags);
不论是客户还是服务器应用程序都用 send 函数来向 TCP 连接的另一端发送数据。客户程序一般用 send 函数向服务器发送请求,而服务器则通常用 send 函数来向客户程序发送应答。该函数的第一个参数指定发送端套接字描述符;第二个参数指明一个存放应用程序要发送数据的缓冲区;第三个参数指明实际要发送的数据的字节数;第四个参数一般置 0 。这里只描述同步 Socket send 函数的执行流程。当调用该函数时, send 先比较待发送数据的长度 len 和套接字 s 的发送缓冲区的长度,如果 len 大于 s 的发送缓冲区的长度,该函数返回 SOCKET_ERROR ;如果 len 小于或者等于 s 的发送缓冲区的长度,那么 send 先检查协议是否正在发送 s 的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送 s 的发送缓冲中的数据或者 s 的发送缓冲中没有数据,那么 send 就比较 s 的发送缓冲区的剩余空间和 len ,如果 len 大于剩余空间大小 send 就一直等待协议把 s 的发送缓冲中的数据发送完,如果 len 小于剩余空间大小 send 就仅仅把 buf 中的数据 copy 到剩余空间里(注意并不是 send s 的发送缓冲中的数据传到连接的另一端的,而是协议传的, send 仅仅是把 buf 中的数据 copy s 的发送缓冲区的剩余空间里)。如果 send 函数 copy 数据成功,就返回实际 copy 的字节数,如果 send copy 数据时出现错误,那么 send 就返回 SOCKET_ERROR ;如果 send 在等待协议传送数据时 网络 断开的话,那么 send 函数也返回 SOCKET_ERROR 。要注意 send 函数把 buf 中的数据成功 copy s 的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现 网络 错误的话,那么下一个 Socket 函数就会返回 SOCKET_ERROR 。(每一个除 send 外的 Socket 函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现 网络 错误,那么该 Socket 函数就返回 SOCKET_ERROR
注意:在 Unix 系统下,如果 send 在等待协议传送数据时 网络 断开的话,调用 send 的进程会接收到一个 SIGPIPE 信号,进程对该信号的默认处理是进程终止。
 
六、 recv 函数
int recv(SOCKET s,  char FAR *buf,  int len,  int flags );
不论是客户还是服务器应用程序都用 recv 函数从 TCP 连接的另一端接收数据。该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据;第三个参数指明 buf 的长度;第四个参数一般置 0 。这里只描述同步 Socket recv 函数的执行流程。当应用程序调用 recv 函数时, recv 先等待 s 的发送缓冲中的数据被协议传送完毕,如果协议在传送 s 的发送缓冲中的数据时出现 网络 错误,那么 recv 函数返回 SOCKET_ERROR ,如果 s 的发送缓冲中没有数据或者数据被协议成功发送完毕后, recv 先检查套接字 s 的接收缓冲区,如果 s 接收缓冲区中没有数据或者协议正在接收数据,那么 recv 就一直等待,只到协议把数据接收完毕。当协议把数据接收完毕, recv 函数就把 s 的接收缓冲中的数据 copy buf 中(注意协议接收到的数据可能大于 buf 的长度,所以在这种情况下要调用几次 recv 函数才能把 s 的接收缓冲中的数据 copy 完。 recv 函数仅仅是 copy 数据,真正的接收数据是协议来完成的), recv 函数返回其实际 copy 的字节数。如果 recv copy 时出错,那么它返回 SOCKET_ERROR ;如果 recv 函数在等待协议接收数据时 网络 中断了,那么它返回 0
注意:在 Unix 系统下,如果 recv 函数在等待协议接收数据时 网络 断开了,那么调用 recv 的进程会接收到一个 SIGPIPE 信号,进程对该信号的默认处理是进程终止。
 
七、 bind 函数
int bind(SOCKET s,  const struct sockaddr FAR *name,  int namelen );
当创建了一个 Socket 以后,套接字数据结构中有一个默认的 IP 地址和默认的端口号。一个服务程序必须调用 bind 函数来给其绑定一个 IP 地址和一个特定的端口号。客户程序一般不必调用 bind 函数来为其 Socket 绑定 IP 地址和断口号。该函数的第一个参数指定待绑定的 Socket 描述符;第二个参数指定一个 sockaddr 结构,该结构是这样定义的:
struct sockaddr
{
u_short sa_family;
char sa_data[14]; 
};
sa_family 指定地址族,对于 TCP/IP 协议族的套接字,给其置 AF_INET 。当对 TCP/IP 协议族的套接字进行绑定时,我们通常使用另一个地址结构:
struct sockaddr_in
{
short   sin_family;
u_short sin_port; 
struct  in_addr sin_addr;
char    sin_zero[8];
};
其中 sin_family AF_INET sin_port 指明端口号; sin_addr 结构体中只有一个唯一的字段 s_addr ,表示 IP 地址,该字段是一个整数,一般用函数 inet_addr ()把字符串形式的 IP 地址转换成 unsigned long 型的整数值后再置给 s_addr 。有的服务器是多宿主机,至少有两个网卡,那么运行在这样的服务器上的服务程序在为其 Socket 绑定 IP 地址时可以把 htonl(INADDR_ANY) 置给 s_addr ,这样做的好处是不论哪个网段上的客户程序都能与该服务程序通信;如果只给运行在多宿主机上的服务程序的 Socket 绑定一个固定的 IP 地址,那么就只有与该 IP 地址处于同一个网段上
 
Windows Socket1.1 程序设计
一、简介
Windows Sockets 是从 Berkeley Sockets 扩展而来的,其在继承 Berkeley Sockets 的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合 WINDOWS 消息驱动特性的 网络 事件异步选择机制。
Windows Sockets 由两部分组成:开发组件和运行组件。
开发组件: Windows Sockets 实现文档、应用程序接口 (API) 引入库和一些头文件。
运行组件: Windows Sockets 应用程序接口的动态链接库 (WINSOCK.DLL)
 
二、主要扩充说明
1 、异步选择机制:
Windows Sockets 的异步选择函数提供了消息机制的 网络 事件选择,当使用它登记 网络 事件发生时,应用程序相应窗口函数将收到一个消息,消息中指示了发生的 网络 事件,以及与事件相关的一些信息。
Windows Sockets 提供了一个异步选择函数 WSAAsyncSelect() ,用它来注册应用程序感兴趣的 网络 事件,当这些事件发生时,应用程序相应的窗口函数将收到一个消息。
函数结构如下 :
int PASCAL FAR WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg,long lEvent);
参数说明:
hWnd :窗口句柄 ;
wMsg :需要发送的消息 ;
lEvent :事件(以下为事件的内容)值含义:
FD_READ 期望在套接字上收到数据(即读准备好)时接到通知
FD_WRITE 期望在套接字上可发送数据(即写准备好)时接到通知
FD_OOB 期望在套接字上有带外数据到达时接到通知
FD_ACCEPT 期望在套接字上有外来连接时接到通知
FD_CONNECT 期望在套接字连接建立完成时接到通知
FD_CLOSE 期望在套接字关闭时接到通知
例如:我们要在套接字读准备好或写准备好时接到通知,语句如下:
rc = WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE);
如果我们需要注销对套接字 网络 事件的消息发送,只要将 lEvent 设置为 0   
 
2 、异步请求函数
Berkeley Sockets 中请求服务是阻塞的, WINDOWS SICKETS 除了支持这一类函数外,还增加了相应的异步请求函数 (WSAAsyncGetXByY();)
 
3 、阻塞处理方法
Windows Sockets 为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃 CPU 让其它应用程序运行,它在调用处于阻塞时便进入一个叫“ HOOK ”的例程,此例程负责接收和分配 WINDOWS 消息,使得其它应用程序仍然能够接收到自己的消息并取得控制权。
WINDOWS 是非抢先的多任务环境,即若一个程序不主动放弃其控制权,别的程序就不能执行。因此在设计 Windows Sockets 程序时,尽管系统支持阻塞操作,但还是反对程序员使用该操作。但由于 SUN 公司下的 Berkeley Sockets 的套接字默认操作是阻塞的, WINDOWS 作为移植的 SOCKETS 也不可避免对这个操作支持。
Windows Sockets 实现中,对于不能立即完成的阻塞操作做如下处理: DLL 初始化→循环操作。在循环中,它发送任何 WINDOWS 消息,并检查这个 Windows Sockets 调用是否完成,在必要时,它可以放弃 CPU 让其它应用程序执行(当然使用超线程的 CPU 就不会有这个麻烦了 ^_^ )。我们可以调用 WSACancelBlockingCall() 函数取消此阻塞操作。
Windows Sockets 中,有一个默认的阻塞处理例程 BlockingHook() 简单地获取并发送 WINDOWS 消息。如果要对复杂程序进行处理, Windows Sockets 中还有 WSASetBlockingHook() 提供用户安装自己的阻塞处理例程能力;与该函数相对应的则是 SWAUnhookBlockingHook() ,它用于删除先前安装的任何阻塞处理例程,并重新安装默认的处理例程。请注意,设计自己的阻塞处理例程时,除了函数 WSACancelBlockingHook() 之外,它不能使用其它的 Windows Sockets API 函数。在处理例程中调用 WSACancelBlockingHook() 函数将取消处于阻塞的操作,它将结束阻塞循环。
 
4 、出错处理
Windows Sockets 为了和以后多线程环境( WINDOWS/UNIX )兼容,它提供了两个出错处理函数来获取和设置当前线程的最近错误号。( WSAGetLastEror() WSASetLastError()
 
5 、启动与终止
使用函数 WSAStartup() WSACleanup() 启动和终止套接字。

三、 Windows Sockets网络 程序设计核心
我们终于可以开始真正的 Windows Sockets 网络 程序设计了。不过我们还是先看一看每个 Windows Sockets 网络 程序都要涉及的内容。让我们一步步慢慢走。
 
1 、启动与终止
在所有 Windows Sockets 函数中,只有启动函数 WSAStartup() 和终止函数 WSACleanup() 是必须使用的。
启动函数必须是第一个使用的函数,而且它允许指定 Windows Sockets API 的版本,并获得 SOCKETS 的特定的一些技术细节。本结构如下:
int PASCAL FAR WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
其中 wVersionRequested 保证 SOCKETS 可正常运行的 DLL 版本,如果不支持,则返回错误信息。
我们看一下下面这段代码,看一下如何进行 WSAStartup() 的调用
WORD wVersionRequested;// 定义版本信息变量
WSADATA wsaData;// 定义数据信息变量
int err;// 定义错误号变量
wVersionRequested = MAKEWORD(1,1);// 给版本信息赋值
err = WSAStartup(wVersionRequested, &wsaData);// 给错误信息赋值
if(err!=0)
{
return;// 告诉用户找不到合适的版本
}
// 确认 Windows Sockets DLL 支持 1.1 版本
//DLL 版本可以高于 1.1
// 系统返回的版本号始终是最低要求的 1.1 ,即应用程序与 DLL 中可支持的最低版本号
if(LOBYTE(wsaData.wVersion)!= 1|| HIBYTE(wsaData.wVersion)!=1)
{
WSACleanup();// 告诉用户找不到合适的版本
return;
}
//Windows Sockets DLL 被进程接受,可以进入下一步操作
关闭函数使用时,任何打开并已连接的 SOCK_STREAM 套接字被复位,但那些已由 closesocket() 函数关闭的但仍有未发送数据的套接字不受影响,未发送的数据仍将被发送。程序运行时可能会多次调用 WSAStartuo() 函数,但必须保证每次调用时的 wVersionRequested 的值是相同的。
 
2 、异步请求服务
Windows Sockets 除支持 Berkeley Sockets 中同步请求,还增加了了一类异步请求服务函数 WSAAsyncGerXByY() 。该函数是阻塞请求函数的异步版本。应用程序调用它时,由 Windows Sockets DLL 初始化这一操作并返回调用者,此函数返回一个异步句柄,用来标识这个操作。当结果存储在调用者提供的缓冲区,并且发送一个消息到应用程序相应窗口。常用结构如下:
HANDLE taskHnd;
char hostname="rs6000";
taskHnd = WSAAsyncBetHostByName(hWnd,wMsg,hostname,buf,buflen);
需要注意的是,由于 Windows 的内存对像可以设置为可移动和可丢弃,因此在操作内存对象是,必须保证 WIindows Sockets DLL 对象是可用的。
 
3 、异步数据传输
使用 send() sendto() 函数来发送数据,使用 recv() recvfrom() 来接收数据。 Windows Sockets 不鼓励用户使用阻塞方式传输数据,因为那样可能会阻塞整个 Windows 环境。下面我们看一个异步数据传输实例:
假设套接字 s 在连接建立后,已经使用了函数 WSAAsyncSelect() 在其上注册了 网络 事件 FD_READ FD_WRITE ,并且 wMsg 值为 UM_SOCK ,那么我们可以在 Windows 消息循环中增加如下的分支语句:
case UM_SOCK:
switch(lParam)
{
case FD_READ:
len = recv(wParam,lpBuffer,length,0);
break;
 
case FD_WRITE:
while(send(wParam,lpBuffer,len,0)!=SOCKET_ERROR)
break;
}
break;
 
4 、出错处理
Windows 提供了一个函数来获取最近的错误码 WSAGetLastError() ,推荐的编写方式如下:
len = send (s,lpBuffer,len,0);
if((len == SOCKET_ERROR)&&(WSAGetLastError()==WSAWOULDBLOCK)){...}
实例应用:
基于 Visual C++ Winsock API 研究
为了方便 网络编程 90 年代初,由 Microsoft 联合了其他几家公司共同制定了一套 WINDOWS 下的 网络编程 接口,即 Windows Sockets 规范,它不是一种 网络 协议 , 而是一套开放的、支持多种协议的 Windows 下的 网络编程 接口。现在的 Winsock 已经基本上实现了与协议无关,你可以使用 Winsock 来调用多种协议的功能,但较常使用的是 TCP/IP 协议。 Socket 实际在计算机中提供了一个通信端口,可以通过这个端口与任何一个具有 Socket 接口的计算机通信。应用程序在 网络 上传输,接收的信息都通过这个 Socket 接口来实现。
微软为 VC 定义了 Winsock 类如 CAsyncSocket 类和派生于 CAsyncSocket CSocket 类,它们简单易用,读者朋友当然可以使用这些类来实现自己的 网络 程序,但是为了更好的了解 Winsock API编程 技术,我们这里探讨怎样使用底层的 API 函数实现简单的 Winsock 网络 应用程式设计,分别说明如何在 Server 端和 Client 端操作 Socket ,实现基于 TCP/IP 的数据传送,最后给出相关的源代码。
VC 中进行 WINSOCK API编程 开发的时候,需要在项目中使用下面三个文件,否则会出现编译错误。
1 WINSOCK.H: 这是 WINSOCK API 的头文件,需要包含在项目中。
2 WSOCK32.LIB: WINSOCK API 连接库文件。在使用中,一定要把它作为项目的非缺省的连接库包含到项目文件中去。
3 WINSOCK.DLL: WINSOCK 的动态连接库,位于 WINDOWS 的安装目录下。
 
一、服务器端操作 socket (套接字)
 
1) 在初始化阶段调用 WSAStartup()
此函数在应用程序中初始化 Windows Sockets DLL ,只有此函数调用成功后,应用程序才可以再调用其他 Windows Sockets DLL 中的 API 函数。在程式中调用该函数的形式如下: WSAStartup((WORD)((1<<8|1) ,( LPWSADATA &WSAData) ,其中 (1<<8|1) 表示我们用的是 WinSocket1.1 版本, WSAata 用来存储系统传回的关于 WinSocket 的资料。
 
2) 建立 Socket
初始化 WinSock 的动态连接库后,需要在服务器端建立一个监听的 Socket ,为此可以调用 Socket() 函数用来建立这个监听的 Socket ,并定义此 Socket 所使用的通信协议。此函数调用成功返回 Socket 对象,失败则返回 INVALID_SOCKET( 调用 WSAGetLastError() 可得知原因,所有 WinSocket 的函数都可以使用这个函数来获取失败的原因 )
SOCKET PASCAL FAR socket( int af, int type, int protocol )
参数 : af: 目前只提供 PF_INET(AF_INET)
type Socket 的类型 (SOCK_STREAM SOCK_DGRAM)
protocol :通讯协定 ( 如果使用者不指定则设为 0)
如果要建立的是遵从 TCP/IP 协议的 socket ,第二个参数 type 应为 SOCK_STREAM ,如为 UDP (数据报)的 socket ,应为 SOCK_DGRAM
 
3) 绑定端口
接下来要为服务器端定义的这个监听的 Socket 指定一个地址及端口( Port ),这样客户端才知道待会要连接哪一个地址的哪个端口,为此我们要调用 bind() 函数,该函数调用成功返回 0 ,否则返回 SOCKET_ERROR
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );
数: s Socket 对象名;
name Socket 的地址值,这个地址必须是执行这个程式所在机器的 IP 地址;
namelen name 的长度;
如果使用者不在意地址或端口的值,那么可以设定地址为 INADDR_ANY ,及 Port 0 Windows Sockets 会自动将其设定适当之地址及 Port (1024 5000 之间的值 ) 。此后可以调用 getsockname() 函数来获知其被设定的值。
 
4 )监听
当服务器端的 Socket 对象绑定完成之后 , 服务器端必须建立一个监听的队列来接收客户端的连接请求。 listen() 函数使服务器端的 Socket 进入监听状态,并设定可以建立的最大连接数 ( 目前最大值限制为 5, 最小值为 1) 。该函数调用成功返回 0 ,否则返回 SOCKET_ERROR
int PASCAL FAR listen( SOCKET s, int backlog );
数: s :需要建立监听的 Socket
backlog :最大连接个数;
服务器端的 Socket 调用完 listen ()后,如果此时客户端调用 connect ()函数提出连接申请的话, Server 端必须再调用 accept() 函数,这样服务器端和客户端才算正式完成通信程序的连接动作。为了知道什么时候客户端提出连接要求,从而服务器端的 Socket 在恰当的时候调用 accept() 函数完成连接的建立,我们就要使用 WSAAsyncSelect ()函数,让系统主动来通知我们有客户端提出连接请求了。该函数调用成功返回 0 ,否则返回 SOCKET_ERROR
int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent );
参数: s Socket 对象;
hWnd :接收消息的窗口句柄;
wMsg :传给窗口的消息;
lEvent :被注册的 网络 事件,也即是应用程序向窗口发送消息的网路事件,该值为下列值 FD_READ FD_WRITE FD_OOB FD_ACCEPT FD_CONNECT FD_CLOSE 的组合,各个值的具体含意为 FD_READ :希望在套接字 S 收到数据时收到消息; FD_WRITE :希望在套接字 S 上可以发送数据时收到消息; FD_ACCEPT :希望在套接字 S 上收到连接请求时收到消息; FD_CONNECT :希望在套接字 S 上连接成功时收到消息; FD_CLOSE :希望在套接字 S 上连接关闭时收到消息; FD_OOB :希望在套接字 S 上收到带外数据时收到消息。
具体应用时, wMsg 应是在应用程序中定义的消息名称,而消息结构中的 lParam 则为以上各种 网络 事件名称。所以,可以在窗口处理自定义消息函数中使用以下结构来响应 Socket 的不同事件:  
switch(lParam)  
{
case FD_READ:
   …  
  break;
case FD_WRITE
  
  break;
 
}   
 
5 )服务器端接受客户端的连接请求
Client 提出连接请求时, Server hwnd 视窗会收到 Winsock Stack 送来我们自定义的一个消息,这时,我们可以分析 lParam ,然后调用相关的函数来处理此事件。为了使服务器端接受客户端的连接请求,就要使用 accept() 函数,该函数新建一 Socket 与客户端的 Socket 相通,原先监听之 Socket 继续进入监听状态,等待他人的连接要求。该函数调用成功返回一个新产生的 Socket 对象,否则返回 INVALID_SOCKET
SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen );
参数: s Socket 的识别码;
addr :存放来连接的客户端的地址;
addrlen addr 的长度
6 )结束 socket 连接
结束服务器和客户端的通信连接是很简单的,这一过程可以由服务器或客户机的任一端启动,只要调用 closesocket() 就可以了,而要关闭 Server 端监听状态的 socket ,同样也是利用此函数。另外,与程序启动时调用 WSAStartup() 憨数相对应,程式结束前,需要调用 WSACleanup() 来通知 Winsock Stack 释放 Socket 所占用的资源。这两个函数都是调用成功返回 0 ,否则返回 SOCKET_ERROR
int PASCAL FAR closesocket( SOCKET s );
数: s Socket 的识别码;
int PASCAL FAR WSACleanup( void );
数:
 
二、客户端 Socket 的操作
 
1 )建立客户端的 Socket
客户端应用程序首先也是调用 WSAStartup() 函数来与 Winsock 的动态连接库建立关系,然后同样调用 socket() 来建立一个 TCP UDP socket (相同协定的 sockets 才能相通, TCP TCP UDP UDP )。与服务器端的 socket 不同的是,客户端的 socket 可以调用 bind() 函数,由自己来指定 IP 地址及 port 号码;但是也可以不调用 bind() ,而由 Winsock 来自动设定 IP 地址及 port 号码。
 
2 )提出连接申请
客户端的 Socket 使用 connect() 函数来提出与服务器端的 Socket 建立连接的申请,函数调用成功返回 0 ,否则返回 SOCKET_ERROR
int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
数: s Socket 的识别码;
name Socket 想要连接的对方地址;
namelen name 的长度
 
三、数据的传送
虽然基于 TCP/IP 连接协议(流套接字)的服务是设计客户机 / 服务器应用程序时的主流标准,但有些服务也是可以通过无连接协议(数据报套接字)提供的。先介绍一下 TCP socket UDP socket 在传送数据时的特性: Stream (TCP) Socket 提供双向、可靠、有次序、不重复的资料传送。 Datagram (UDP) Socket 虽然提供双向的通信,但没有可靠、有次序、不重复的保证,所以 UDP 传送数据可能会收到无次序、重复的资料,甚至资料在传输过程中出现遗漏。由于 UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以绝大多数应用程序都是采用 TCP 处理 Socket ,以保证资料的正确性。一般情况下 TCP Socket 的数据发送和接收是调用 send() recv() 这两个函数来达成,而 UDP Socket 则是用 sendto() recvfrom() 这两个函数,这两个函数调用成功发挥发送或接收的资料的长度,否则返回 SOCKET_ERROR
int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags );
参数: s Socket 的识别码
buf :存放要传送的资料的暂存区
len buf :的长度
flags :此函数被调用的方式
对于 Datagram Socket 而言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对 Stream Socket 言, Blocking 模式下,若是传送系统内的储存空间不够存放这些要传送的资料, send() 将会被 block 住,直到资料送完为止;如果该 Socket 被设定为 Non-Blocking 模式,那么将视目前的 output buffer 空间有多少,就送出多少资料,并不会被 block 住。 flags 的值可设为 0 MSG_DONTROUTE MSG_OOB 的组合。
int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );
参数: s Socket 的识别码
buf :存放接收到的资料的暂存区
len buf :的长度
flags :此函数被调用的方式
Stream Socket 言,我们可以接收到目前 input buffer 内有效的资料,但其数量不超过 len 的大小。
 
四、自定义的 CMySocket 类的实现代码:
根据上面的知识,我自定义了一个简单的 CMySocket 类,下面是我定义的该类的部分实现代码:
CMySocket::CMySocket() : //file:// 类的构造函数
{
     WSADATA wsaD;
     memset( m_LastError, 0, ERR_MAXLENGTH );
     // m_LastError 是类内字符串变量,初始化用来存放最后错误说明的字符串;  
     // 初始化类内sockaddr_in结构变量,前者存放客户端地址,后者对应于服务器端地址;
     memset( &m_sockaddr, 0, sizeof( m_sockaddr ) );
     memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
    
     int result = WSAStartup((WORD)((1<<8|1) , &wsaD);//初始化WinSocket动态连接库;
     if( result != 0 ) // 初始化失败;
     {
         set_LastError( "WSAStartup failed!", WSAGetLastError());
        
         return;
     }
}
 
 
CMySocket::~CMySocket() // 类的析构函数;
{
     WSACleanup();
}
 
int CMySocket::Create( void )
{
     // m_hSocket 是类内Socket对象,创建一个基于TCP/IP的Socket变量,并将值赋给该变量;
     if ( (m_hSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )) == INVALID_SOCKET )
     {
         set_LastError( "socket() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ERR_SUCCESS;
}
 
int CMySocket::Close( void )// 关闭Socket对象;
{  
     if ( closesocket( m_hSocket ) == SOCKET_ERROR )
     {
         set_LastError( "closesocket() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
     //file:// 重置sockaddr_in 结构变量;
     memset( &m_sockaddr, 0, sizeof( sockaddr_in ) );
     memset( &m_rsockaddr, 0, sizeof( sockaddr_in ) );
    
     return ERR_SUCCESS;
}
 
 
int CMySocket::Connect( char* strRemote, unsigned int iPort )// 定义连接函数;
{
     if( strlen( strRemote ) == 0 || iPort == 0 )
         return ERR_BADPARAM;
    
     hostent *hostEnt = NULL;
     long lIPAddress = 0;
     hostEnt = gethostbyname( strRemote );// 根据计算机名得到该计算机的相关内容;
    
     if( hostEnt != NULL )
     {
         lIPAddress = ((in_addr*)hostEnt->h_addr)->s_addr;
         m_sockaddr.sin_addr.s_addr = lIPAddress;
     }
     else
     {
         m_sockaddr.sin_addr.s_addr = inet_addr( strRemote );
     }
    
     m_sockaddr.sin_family = AF_INET;
     m_sockaddr.sin_port = htons( iPort );
    
     if( connect( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
     {
         set_LastError( "connect() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ERR_SUCCESS;
}
 
 
int CMySocket::Bind( char* strIP, unsigned int iPort )// 绑定函数;
{
     if( strlen( strIP ) == 0 || iPort == 0 )
         return ERR_BADPARAM;
    
     memset( &m_sockaddr,0, sizeof( m_sockaddr ) );
     m_sockaddr.sin_family = AF_INET;
     m_sockaddr.sin_addr.s_addr = inet_addr( strIP );
     m_sockaddr.sin_port = htons( iPort );
    
     if ( bind( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
     {
         set_LastError( "bind() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ERR_SUCCESS;
}
 
int CMySocket::Accept( SOCKET s )// 建立连接函数,S为监听Socket对象名;
{
     int Len = sizeof( m_rsockaddr );
     memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
     if( ( m_hSocket = accept( s, (SOCKADDR*)&m_rsockaddr, &Len ) ) == INVALID_SOCKET )
     {
         set_LastError( "accept() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ERR_SUCCESS;
}
 
int CMySocket::asyncSelect( HWND hWnd, unsigned int wMsg, long lEvent )//file:// 事件选择函数;
{
     if( !IsWindow( hWnd ) || wMsg == 0 || lEvent == 0 )
         return ERR_BADPARAM;
    
     if( WSAAsyncSelect( m_hSocket, hWnd, wMsg, lEvent ) == SOCKET_ERROR )
     {
         set_LastError( "WSAAsyncSelect() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ERR_SUCCESS;
}
 
int CMySocket::Listen( int iQueuedConnections )// 监听函数;
{
     if( iQueuedConnections == 0 )
         return ERR_BADPARAM;
    
     if( listen( m_hSocket, iQueuedConnections ) == SOCKET_ERROR )
     {
         set_LastError( "listen() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ERR_SUCCESS;
}
 
int CMySocket::Send( char* strData, int iLen )// 数据发送函数;
{
     if( strData == NULL || iLen == 0 )
         return ERR_BADPARAM;
    
     if( send( m_hSocket, strData, iLen, 0 ) == SOCKET_ERROR )
     {
         set_LastError( "send() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ERR_SUCCESS;
}
 
int CMySocket::Receive( char* strData, int iLen )// 数据接收函数;
{
     if( strData == NULL )
         return ERR_BADPARAM;
    
     int len = 0;
     int ret = 0;
    
     ret = recv( m_hSocket, strData, iLen, 0 );
     if ( ret == SOCKET_ERROR )
     {
         set_LastError( "recv() failed", WSAGetLastError() );
        
         return ERR_WSAERROR;
     }
    
     return ret;
}
 
void CMySocket::set_LastError( char* newError, int errNum ) //file://WinSock API 操作错误字符串设置函数;
{
     memset( m_LastError, 0, ERR_MAXLENGTH );
     memcpy( m_LastError, newError, strlen( newError ) );
     m_LastError[strlen(newError)+1] = '/0';
}
有了上述类的定义,就可以在 网络 程序的服务器和客户端分别定义 CMySocket 对象,建立连接,传送数据了。例如,为了在服务器和客户端发送数据,需要在服务器端定义两个 CMySocket 对象 ServerSocket1 ServerSocket2 ,分别用于监听和连接,客户端定义一个 CMySocket 对象 ClientSocket ,用于发送或接收数据,如果建立的连接数大于一,可以在服务器端再定义 CMySocket 对象,但要注意连接数不要大于五。
由于 Socket API 函数还有许多,如获取远端服务器、本地客户机的 IP 地址、主机名等等,读者可以再此基础上对 CMySocket 补充完善,实现更多的功能。
 
 
TCP/IP Winsock编程 要点
利用 Winsock编程 由同步和异步方式,同步方式逻辑清晰, 编程 专注于应用,在抢先式的多任务操作系统中 (WinNt Win2K) 采用多线程方式效率基本达到异步方式的水平,应此以下为同步方式 编程 要点。
 
1 、快速通信

Winsock Nagle 算法将降低小数据报的发送速度,而系统默认是使用 Nagle 算法 , 使用 int setsockopt(SOCKET s,  int level,  int optname,  const char FAR *optval,  int optlen ); 函数关闭它
例子:
SOCKET sConnect;
sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
int bNodelay = 1;
int err; err = setsockopt( sConnect, IPPROTO_TCP, TCP_NODELAY, (char *)&bNodelay, sizoeof(bNodelay));// 不采用延时算法
if (err != NO_ERROR)
TRACE ("setsockopt failed for some reason/n");
 
2 SOCKET SegMentSize 和收发缓冲
TCPSegMentSize 是发送接受时单个数据报的最大长度,系统默认为 1460 ,收发缓冲大小为 8192
SOCK_STREAM 方式下,如果单次发送数据超过 1460 ,系统将分成多个数据报传送,在对方接受到的将是一个数据流,应用程序需要增加断帧的判断。当然可以采用修改注册表的方式改变 1460 的大小,但 MicrcoSoft 认为 1460 是最佳效率的参数,不建议修改。
在工控系统中,建议关闭 Nagle 算法,每次发送数据小于 1460 个字节(推荐 1400 ),这样每次发送的是一个完整的数据报,减少对方对数据流的断帧处理。
 
3 、同步方式中减少断网时 connect 函数的阻塞时间
同步方式中的断网时 connect 的阻塞时间为 20 秒左右,可采用 gethostbyaddr 事先判断到服务主机的路径是否是通的,或者先 ping 一下对方主机的 IP 地址。
A 、采用 gethostbyaddr 阻塞时间不管成功与否为 4 秒左右。
例子:
LONG lPort=3024;
struct sockaddr_in ServerHostAddr;// 服务主机地址
ServerHostAddr.sin_family=AF_INET;
ServerHostAddr.sin_port=::htons(u_short(lPort));
ServerHostAddr.sin_addr.s_addr=::inet_addr("192.168.1.3");
HOSTENT* pResult=gethostbyaddr((const char *) & (ServerHostAddr.sin_addr. s_addr), 4, AF_INET);
if(NULL==pResult)
{
int nErrorCode=WSAGetLastError();
TRACE("gethostbyaddr errorcode=%d",nErrorCode);
}
else
{
TRACE("gethostbyaddr %s/n",pResult->h_name);;
}   
 
B 、采用 PING 方式时间约 2 秒左右
暂略

4 、同步方式中解决 recv send 阻塞问题
采用 select 函数解决,在收发前先检查读写可用状态。
 
A 、读
例子:
TIMEVAL tv01 = {0, 1};//1ms 钟延迟,实际为0-10毫秒
int nSelectRet;
int nErrorCode;
FD_SET fdr = {1, sConnect};
 
nSelectRet=::select(0, &fdr, NULL, NULL, &tv01);// 检查可读状态
if (SOCKET_ERROR==nSelectRet)
{
     nErrorCode=WSAGetLastError();
     TRACE("select read status errorcode=%d",nErrorCode);
     ::closesocket(sConnect);
    
     goto // 重新连接(客户方),或服务线程退出(服务方);
}
if (nSelectRet==0)// 超时发生,无可读数据
{
     继续查读状态或向对方主动发送
}
else
{
     // 读数据
}
B 、写
TIMEVAL tv01 = {0, 1};//1ms 钟延迟,实际为9-10毫秒
int nSelectRet;
int nErrorCode;
FD_SET fdw = {1, sConnect};
nSelectRet=::select(0, NULL, NULL,&fdw, &tv01);// 检查可写状态
if (SOCKET_ERROR==nSelectRet)
{
     nErrorCode=WSAGetLastError();
     TRACE("select write status errorcode=%d",nErrorCode);
     ::closesocket(sConnect);
    
     //goto 重新连接(客户方),或服务线程退出(服务方);
}
if (nSelectRet==0)// 超时发生,缓冲满或网络忙
{
     // 继续查写状态或查读状态
}
else
{
     // 发送
}
 
5 、改变TCP收发缓冲区大小
     系统默认为8192,利用如下方式可改变。
     SOCKET sConnect;
     sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
 
     int nrcvbuf=1024*20;
     int err=setsockopt(sConnect, SOL_SOCKET, SO_SNDBUF,/* 写缓冲,读缓冲为SO_RCVBUF*/ (char *)&nrcvbuf, sizeof(nrcvbuf));
     if (err != NO_ERROR)
     {
         TRACE("setsockopt Error!/n");
     }
 
     // 在设置缓冲时,检查是否真正设置成功用
     int getsockopt(SOCKET s, int level, int optname, char FAR *optval, int FAR *optlen );
 
6 、服务方同一端口多 IP 地址的 bind listen
在可靠性要求高的应用中,要求使用双网和多 网络 通道,再服务方很容易实现,用如下方式可建立客户对本机所有 IP 地址在端口 3024 下的请求服务。
/ SOCKET hServerSocket_DS = INVALID_SOCKET;
struct sockaddr_in HostAddr_DS;// 服务器主机地址
LONG lPort = 3024;
HostAddr_DS.sin_family = AF_INET;
HostAddr_DS.sin_port = ::htons(u_short(lPort));
HostAddr_DS.sin_addr.s_addr = htonl(INADDR_ANY);
hServerSocket_DS = ::socket( AF_INET, SOCK_STREAM,IPPROTO_TCP);
 
if (hServerSocket_DS == INVALID_SOCKET)
{
     AfxMessageBox(" 建立数据服务器SOCKET 失败!");
     return FALSE;
}
if (SOCKET_ERROR == ::bind(hServerSocket_DS,(struct sockaddr *)(&(HostAddr_DS)),sizeof(SOCKADDR)))
{
     int nErrorCode = WSAGetLastError ();
     TRACE("bind error=%d/n",nErrorCode);
     AfxMessageBox("Socket Bind 错误!");
    
     return FALSE;
}
 
if (SOCKET_ERROR == ::listen(hServerSocket_DS,10))//10 个客户
{
     AfxMessageBox("Socket listen 错误!");
    
     return FALSE;
}
 
AfxBeginThread(ServerThreadProc,NULL,THREAD_PRIORITY_NORMAL);
在客户方要复杂一些,连接断后,重联不成功则应换下一个 IP 地址连接。也可采用同时连接好后备用的方式。
 
7 、用 TCP/IP Winsock 实现变种 Client/Server
传统的 Client/Server 为客户问、服务答,收发是成对出现的。而变种的 Client/Server 是指在连接时有客户和服务之分,建立好通信连接后,不再有严格的客户和服务之分,任何方都可主动发送,需要或不需要回答看应用而言,这种方式在工控行业很有用,比如 RTDB 作为 I/O Server 的客户,但 I/O Server 也可主动向 RTDB 发送开关状态变位、随即事件等信息。在很大程度上减少了 网络 通信负荷、提高了效率。
采用 1-6 TCP/IP编程 要点,在 Client Server 方均已接收优先,适当控制时序就能实现。
 
 
Windows Sockets API 实现 网络 异步通讯
摘要:本文对如何使用面向连接的流式套接字实现对网卡的 编程 以及如何实现异步 网络 通讯等问题进行了讨论与阐述。
一、 引言
80 年代初,美国加利福尼亚大学伯克利分校的研究人员为 TCP/IP网络 通信开发了一个专门用于 网络 通讯开发的 API 。这个 API 就是 Socket 接口(套接字) -- 当今在 TCP/IP网络 最为通用的一种 API ,也是在互联网上进行应用开发最为通用的一种 API 。在微软联合其它几家公司共同制定了一套 Windows 下的 网络编程 接口 Windows Sockets 规范后,由于在其规范中引入了一些异步函数,增加了对 网络 事件异步选择机制,因此更加符合 Windows 的消息驱动特性,使 网络 开发人员可以更加方便的进行高性能 网络 通讯程序的设计。本文接下来就针对 Windows Sockets API 进行面向连接的流式套接字 编程 以及对异步 网络 通讯的 编程 实现等问题展开讨论。
 
二、 面向连接的流式套接字 编程 模型的设计
本文在方案选择上采用了在 网络编程 中最常用的一种模型 -- 客户机 / 服务器模型。这种客户 / 服务器模型是一种非对称式 编程 模式。该模式的基本思想是把集中在一起的应用划分成为功能不同的两个部分,分别在不同的计算机上运行,通过它们之间的分工合作来实现一个完整的功能。对于这种模式而言其中一部分需要作为服务器,用来响应并为客户提供固定的服务;另一部分则作为客户机程序用来向服务器提出请求或要求某种服务。
本文选取了基于 TCP/IP 的客户机 / 服务器模型和面向连接的流式套接字。其通信原理为:服务器端和客户端都必须建立通信套接字,而且服务器端应先进入监听状态,然后客户端套接字发出连接请求,服务器端收到请求后,建立另一个套接字进行通信,原来负责监听的套接字仍进行监听,如果有其它客户发来连接请求,则再建立一个套接字。默认状态下最多可同时接收 5 个客户的连接请求,并与之建立通信关系。因此本程序的设计流程应当由服务器首先启动,然后在某一时刻启动客户机并使其与服务器建立连接。服务器与客户机开始都必须调用 Windows Sockets API 函数 socket() 建立一个套接字 sockets, 然后服务器方调用 bind() 将套接字与一个本地 网络 地址捆扎在一起,再调用 listen() 使套接字处于一种被动的准备接收状态,同时规定它的请求队列长度。在此之后服务器就可以通过调用 accept() 来接收客户机的连接。
相对于服务器,客户端的工作就显得比较简单了,当客户端打开套接字之后,便可通过调用 connect() 和服务器建立连接。连接建立之后,客户和服务器之间就可以通过连接发送和接收资料。最后资料传送结束,双方调用 closesocket() 关闭套接字来结束这次通讯。整个通讯过程的具体流程框图可大致用下面的流程图来表示:
面向连接的流式套接字 编程 流程示意图   
 
三、 软件 设计要点以及异步通讯的实现
根据前面设计的程序流程,可将程序划分为两部分:服务器端和客户端。而且整个实现过程可以大致用以下几个非常关键的 Windows Sockets API 函数将其惯穿下来:
服务器方:
socket()->bind()->listen->accept()->recv()/send()->closesocket()
客户机方:
socket()->connect()->send()/recv()->closesocket()
有鉴于以上几个函数在整个 网络编程 中的重要性,有必要结合程序实例对其做较深入的剖析。服务器端应用程序在使用套接字之前,首先必须拥有一个 Socket ,系统调用 socket() 函数向应用程序提供创建套接字的手段。该套接字实际上是在计算机中提供了一个通信埠,可以通过这个埠与任何一个具有套接字接口的计算机通信。应用程序在 网络 上传输、接收的信息都通过这个套接字接口来实现的。在应用开发中如同使用文件句柄一样,可以对套接字句柄进行读写操作:
sock=socket(AF_INET,SOCK_STREAM,0);
函数的第一个参数用于指定地址族,在 Windows 下仅支持 AF_INET(TCP/IP 地址 ) ;第二个参数用于描述套接字的类型,对于流式套接字提供有 SOCK_STREAM ;最后一个参数指定套接字使用的协议,一般为 0 。该函数的返回值保存了新套接字的句柄,在程序退出前可以用 closesocket(sock); 函数来将其释放。服务器方一旦获取了一个新的套接字后应通过 bind() 将该套接字与本机上的一个端口相关联:
sockin.sin_family=AF_INET;
sockin.sin_addr.s_addr=0;
sockin.sin_port=htons(USERPORT);
bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin)));
该函数的第二个参数是一个指向包含有本机 IP 地址和端口信息的 sockaddr_in 结构类型的指针,其成员描述了本地端口号和本地主机地址,经过 bind() 将服务器进程在 网络 上标识出来。需要注意的是由于 1024 以内的埠号都是保留的埠号因此如无特别需要一般不能将 sockin.sin_port 的埠号设置为 1024 以内的值。然后调用 listen() 函数开始侦听,再通过 accept() 调用等待接收连接以完成连接的建立:
// 连接请求队列长度为 1 ,即只允许有一个请求 , 若有多个请求 ,
// 则出现错误,给出错误代码 WSAECONNREFUSED
listen(sock,1);
// 开启线程避免主程序的阻塞
AfxBeginThread(Server,NULL);
……
UINT Server(LPVOID lpVoid)
{
……
int nLen=sizeof(SOCKADDR);
pView->newskt=accept(pView->sock,(LPSOCKADDR)& pView->sockin,(LPINT)& nLen);
……
WSAAsyncSelect(pView->newskt,pView->m_hWnd,WM_SOCKET_MSG,FD_READ|FD_CLOSE);
return 1;
}
这里之所以把 accept() 放到一个线程中去是因为在执行到该函数时如没有客户连接服务器的请求到来,服务器就会停在 accept 语句上等待连接请求的到来,这势必会引起程序的阻塞,虽然也可以通过设置套接字为非阻塞方式使在没有客户等待时可以使 accept ()函数调用立即返回,但这种轮询套接字的方式会使 CPU 处于忙等待方式,从而降低程序的运行效率大大浪费系统资源。考虑到这种情况,将套接字设置为阻塞工作方式,并为其单独开辟一个子线程,将其阻塞控制在子线程范围内而不会造成整个应用程序的阻塞。对于 网络 事件的响应显然要采取异步选择机制,只有采取这种方式才可以在由 网络 对方所引起的不可预知的 网络 事件发生时能马上在进程中做出及时的响应处理,而在没有 网络 事件到达时则可以处理其他事件,这种效率是很高的,而且完全符合 Windows 所标榜的消息触发原则。前面那段代码中的 WSAAsyncSelect() 函数便是实现 网络 事件异步选择的核心函数。
通过第四个参数注册应用程序感兴取的 网络 事件,在这里通过 FD_READ|FD_CLOSE 指定了 网络 读和 网络 断开两种事件,当这种事件发生时变会发出由第三个参数指定的自定义消息 WM_SOCKET_MSG ,接收该消息的窗口通过第二个参数指定其句柄。在消息处理函数中可以通过对消息参数低字节进行判断而区别出发生的是何种 网络 事件:
void CNetServerView::OnSocket(WPARAM wParam,LPARAM lParam)
{
     int iReadLen=0;
     int message=lParam & 0x0000FFFF;
    
     switch(message)
     {
     case FD_READ:// 读事件发生。此时有字符到达,需要进行接收处理
         char cDataBuffer[MTU*10];
         // 通过套接字接收信息
         iReadLen = recv(newskt,cDataBuffer,MTU*10,0);
         // 将信息保存到文件
        
         if(!file.Open("ServerFile.txt",CFile::modeReadWrite))
              file.Open("E:ServerFile.txt",CFile::modeCreate|CFile::modeReadWrite);
         file.SeekToEnd();
         file.Write(cDataBuffer,iReadLen);
         file.Close();
         break;
    
     case FD_CLOSE:// 网络断开事件发生。此时客户机关闭或退出。
         ……//进行相应的处理
              break;
     default:
         break;
     }
}
 
// 在这里需要实现对自定义消息WM_SOCKET_MSG的响应,需要在头文件和实现文件中分别添加其消息映射关系:
// 头文件:
 
//{{AFX_MSG(CNetServerView)
//}}AFX_MSG
void OnSocket(WPARAM wParam,LPARAM lParam);
DECLARE_MESSAGE_MAP()
// 实现文件:
 
BEGIN_MESSAGE_MAP(CNetServerView, CView)
//{{AFX_MSG_MAP(CNetServerView)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_SOCKET_MSG,OnSocket)
END_MESSAGE_MAP()
 
在进行异步选择使用 WSAAsyncSelect() 函数时,有以下几点需要引起特别的注意:
 
1 连续使用两次 WSAAsyncSelect() 函数时,只有第二次设置的事件有效,如:
WSAAsyncSelect(s,hwnd,wMsg1,FD_READ);
WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE);
这样只有当 FD_CLOSE 事件发生时才会发送 wMsg2 消息。
 
2 .可以在设置过异步选择后通过再次调用 WSAAsyncSelect(s,hwnd,0,0); 的形式取消在套接字上所设置的异步事件。
3 Windows Sockets DLL 在一个 网络 事件发生后,通常只会给相应的应用程序发送一个消息,而不能发送多个消息。但通过使用一些函数隐式地允许重发此事件的消息,这样就可能再次接收到相应的消息。
4 .在调用过 closesocket() 函数关闭套接字之后不会再发生 FD_CLOSE 事件。
以上基本完成了服务器方的程序设计,下面对于客户端的实现则要简单多了,在用 socket() 创建完套接字之后只需通过调用 connect() 完成同服务器的连接即可,剩下的工作同服务器完全一样:用 send()/recv() 发送 / 接收收据,用 closesocket() 关闭套接字:
sockin.sin_family=AF_INET; // 地址族
sockin.sin_addr.S_un.S_addr=IPaddr; // 指定服务器的 IP 地址
sockin.sin_port=m_Port; // 指定连接的端口号
int nConnect=connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin));
本文采取的是可靠的面向连接的流式套接字。在数据发送上有 write() writev() send() 等三个函数可供选择,其中前两种分别用于缓冲发送和集中发送,而 send() 则为可控缓冲发送,并且还可以指定传输控制标志为 MSG_OOB 进行带外数据的发送或是为 MSG_DONTROUTE 寻径控制选项。在信宿地址的 网络 号部分指定数据发送需要经过的 网络 接口,使其可以不经过本地寻径机制直接发送出去。这也是其同 write() 函数的真正区别所在。由于接收数据系统调用和发送数据系统调用是一一对应的,因此对于数据的接收,在此不再赘述,相应的三个接收函数分别为: read() readv() recv() 。由于后者功能上的全面,本文在实现上选择了 send()-recv() 函数对,在具体 编程 中应当视具体情况的不同灵活选择适当的发送 - 接收函数对。
小结: TCP/IP 协议是目前各 网络 操作系统主要的通讯协议,也是 Internet 的通讯协议,本文通过 Windows Sockets API 实现了对基于 TCP/IP 协议的面向连接的流式套接字 网络 通讯程序的设计,并通过异步通讯和多线程等手段提高了程序的运行效率,避免了阻塞的发生。用 VC++6.0 Sockets API 实现一个聊天室程序

 
1.VC++网络编程 Windows Sockets API 简介
VC++ 网络编程 的支持有 socket 支持, WinInet 支持, MAPI ISAPI 支持等。其中, Windows Sockets API TCP/IP网络 环境里,也是 Internet 上进行开发最为通用的 API 。最早美国加州大学 Berkeley 分校在 UNIX 下为 TCP/IP 协议开发了一个 API ,这个 API 就是著名的 Berkeley Socket 接口 ( 套接字 ) 。在桌面操作系统进入 Windows 时代后,仍然继承了 Socket 方法。在 TCP/IP网络 通信环境下, Socket 数据传输是一种特殊的 I/O ,它也相当于一种文件描述符,具有一个类似于打开文件的函数调用 -socket() 。可以这样理解: Socket 实际上是一个通信端点,通过它,用户的 Socket 程序可以通过 网络 和其他的 Socket 应用程序通信。 Socket 存在于一个 " 通信域 "( 为描述一般的线程如何通过 Socket 进行通信而引入的一种抽象概念 ) 里,并且与另一个域的 Socket 交换数据。 Socket 有三类。第一种是 SOCK_STREAM( 流式 ) ,提供面向连接的可靠的通信服务,比如 telnet,http 。第二种是 SOCK_DGRAM( 数据报 ) ,提供无连接不可靠的通信,比如 UDP 。第三种是 SOCK_RAW( 原始 ) ,主要用于协议的开发和测试,支持通信底层操作,比如对 IP ICMP 的直接访问。
 
2.Windows Socket 机制分析
2.1 一些基本的 Socket 系统调用
主要的系统调用包括: socket()- 创建 Socket bind()- 将创建的 Socket 与本地端口绑定; connect() accept()- 建立 Socket 连接; listen()- 服务器监听是否有连接请求; send()- 数据的可控缓冲发送; recv()- 可控缓冲接收; closesocket()- 关闭 Socket
 
2.2Windows Socket 的启动与终止
启动函数 WSAStartup() 建立与 Windows Sockets DLL 的连接,终止函数 WSAClearup() 终止使用该 DLL ,这两个函数必须成对使用。
 
2.3 异步选择机制
Windows 是一个非抢占式的操作系统,而不采取 UNIX 的阻塞机制。当一个通信事件产生时,操作系统要根据设置选择是否对该事件加以处理, WSAAsyncSelect() 函数就是用来选择系统所要处理的相应事件。当 Socket 收到设定的 网络 事件中的一个时,会给程序窗口一个消息,这个消息里会指定产生 网络 事件的 Socket ,发生的事件类型和错误码。
 
2.4 异步数据传输机制
WSAAsyncSelect() 设定了 Socket 上的须响应通信事件后,每发生一个这样的事件就会产生一个 WM_SOCKET 消息传给窗口。而在窗口的回调函数中就应该添加相应的数据传输处理代码。
 
3. 聊天室程序的设计说明
 
3.1 实现思想
Internet 上的聊天室程序一般都是以服务器提供服务端连接响应,使用者通过客户端程序登录到服务器,就可以与登录在同一服务器上的用户交谈,这是一个面向连接的通信过程。因此,程序要在 TCP/IP 环境下,实现服务器端和客户端两部分程序。

 
3.2 服务器端工作流程
服务器端通过 socket() 系统调用创建一个 Socket 数组后 ( 即设定了接受连接客户的最大数目 ) ,与指定的本地端口绑定 bind() ,就可以在端口进行侦听 listen() 。如果有客户端连接请求,则在数组中选择一个空 Socket ,将客户端地址赋给这个 Socket 。然后登录成功的客户就可以在服务器上聊天了。
 
3.3 客户端工作流程
客户端程序相对简单,只需要建立一个 Socket 与服务器端连接,成功后通过这个 Socket 来发送和接收数据就可以了。
 
4. 核心代码分析
限于篇幅,这里仅给出与 网络编程 相关的核心代码,其他的诸如聊天文字的服务器和客户端显示读者可以自行添加。
 
4.1 服务器端代码
 
开启服务器功能 :
 
void OnServerOpen() // 开启服务器功能
{
     WSADATA wsaData;
     int iErrorCode;
     char chInfo[64];
    
     if (WSAStartup(WINSOCK_VERSION, &wsaData)) // 调用Windows Sockets DLL
     {
         MessageBeep(MB_ICONSTOP);
         MessageBox("Winsock 无法初始化!", AfxGetAppName(), MB_OK|MB_ICONSTOP);
         WSACleanup();
         return;
     }
     else
         WSACleanup();
     if (gethostname(chInfo, sizeof(chInfo)))
     {
         ReportWinsockErr("/n 无法获取主机!/n ");
         return;
     }
    
     CString csWinsockID = "/n==>> 服务器功能开启在端口:No. ";
     csWinsockID += itoa(m_pDoc->m_nServerPort, chInfo, 10);
     csWinsockID += "/n";
    
     PrintString(csWinsockID); // 在程序视图显示提示信息的函数,读者可自行创建
     m_pDoc->m_hServerSocket=socket(PF_INET, SOCK_STREAM, DEFAULT_PROTOCOL);
     // 创建服务器端Socket,类型为SOCK_STREAM,面向连接的通信
    
     if (m_pDoc->m_hServerSocket == INVALID_SOCKET)
     {
         ReportWinsockErr(" 无法创建服务器socket!");
         return;
     }
    
     m_pDoc->m_sockServerAddr.sin_family = AF_INET;
     m_pDoc->m_sockServerAddr.sin_addr.s_addr = INADDR_ANY;
     m_pDoc->m_sockServerAddr.sin_port = htons(m_pDoc->m_nServerPort);
    
     if (bind(m_pDoc->m_hServerSocket, (LPSOCKADDR)&m_pDoc->m_sockServerAddr,sizeof(m_pDoc->m_sockServerAddr)) == SOCKET_ERROR) // 与选定的端口绑定
     {
         ReportWinsockErr(" 无法绑定服务器socket!");
        
         return;
     }
    
     iErrorCode=WSAAsyncSelect(m_pDoc->m_hServerSocket,m_hWnd, WM_SERVER_ACCEPT, FD_ACCEPT);
     // 设定服务器相应的网络事件为FD_ACCEPT,即连接请求,
     // 产生相应传递给窗口的消息为WM_SERVER_ACCEPT
    
     if (iErrorCode == SOCKET_ERROR)
     {
         ReportWinsockErr("WSAAsyncSelect 设定失败!");
         return;
     }
    
     if (listen(m_pDoc->m_hServerSocket, QUEUE_SIZE) == SOCKET_ERROR) // 开始监听客户连接请求
     {
         ReportWinsockErr(" 服务器socket监听失败!");
         m_pParentMenu->EnableMenuItem(ID_SERVER_OPEN, MF_ENABLED);
        
         return;
     }
    
     m_bServerIsOpen = TRUE; // 监视服务器是否打开的变量
    
     return;
}
 
响应客户发送聊天文字到服务器:
 
ON_MESSAGE(WM_CLIENT_READ, OnClientRead)
LRESULT OnClientRead(WPARAM wParam, LPARAM lParam)
{
     int iRead;
     int iBufferLength;
     int iEnd;
     int iRemainSpace;
     char chInBuffer[1024];
     int i;
    
     for(i=0;(i<MAXCLIENT)&&(M_ACLIENTSOCKET[I]!=WPARAM);I++)  //MAXClient是服务器可响应连接的最大数目
     {
 
     }
    
     if(i==MAXClient)
         return 0L;
 
     iBufferLength = iRemainSpace = sizeof(chInBuffer);
     iEnd = 0;
    
     iRemainSpace -= iEnd;
    
     iBytesRead = recv(m_aClientSocket[i], (LPSTR)(chInBuffer+iEnd), iSpaceRemaining, NO_FLAGS);   //用可控缓冲接收函数recv()来接收字符
     iEnd+=iRead;
    
     if (iBytesRead == SOCKET_ERROR)
         ReportWinsockErr("recv 出错!");
    
     chInBuffer[iEnd] = '/0';
    
     if (lstrlen(chInBuffer) != 0)
     {
         PrintString(chInBuffer); // 服务器端文字显示
         OnServerBroadcast(chInBuffer); // 自己编写的函数,向所有连接的客户广播这个客户的聊天文字 
     }
    
     return(0L);
}
 
对于客户断开连接,会产生一个FD_CLOSE消息,只须相应地用closesocket()关闭相应的Socket即可,这个处理比较简单。
 
4.2 客户端代码
 
连接到服务器:
void OnSocketConnect()
{
     WSADATA wsaData;
     DWORD dwIPAddr;
     SOCKADDR_IN sockAddr;
     if(WSAStartup(WINSOCK_VERSION,&wsaData)) // 调用Windows Sockets DLL
     {
         MessageBox("Winsock 无法初始化!",NULL,MB_OK);
         return;
     }
    
     m_hSocket=socket(PF_INET,SOCK_STREAM,0); // 创建面向连接的socket
     sockAddr.sin_family=AF_INET; // 使用TCP/IP协议
     sockAddr.sin_port=m_iPort; // 客户端指定的IP地址
     sockAddr.sin_addr.S_un.S_addr=dwIPAddr;
    
     int nConnect=connect(m_hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)); // 请求连接
    
     if(nConnect)
         ReportWinsockErr(" 连接失败!");
     else
         MessageBox(" 连接成功!",NULL,MB_OK);
     int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,FD_READ); // 指定响应的事件,为服务器发送来字符
    
     if(iErrorCode==SOCKET_ERROR)
         MessageBox("WSAAsyncSelect 设定失败!");
}
接收服务器端发送的字符也使用可控缓冲接收函数 recv() ,客户端聊天的字符发送使用数据可控缓冲发送函数 send() ,这两个过程比较简单,在此就不加赘述了。
 
5. 小结
通过聊天室程序的编写,可以基本了解 Windows Sockets API编程 的基本过程和精要之处。本程序在 VC++6.0 下编译通过,在使用 windows 98/NT 的局域网里运行良好。
VC++ 制作一个简单的局域网消息发送工程
本工程类似于 oicq 的消息发送机制,不过他只能够发送简单的字符串。虽然简单,但他也是一个很好的 VC网络 学习例子。
本例通过 VC 带的 SOCKET 类,重载了他的一个接受类 mysock 类,此类可以吧接收到的信息显示在客户区理。以下是实现过程:
建立一个 MFC 单文档工程,工程名为 oicq, 在第四步选取 WINDOWS SOCKetS 支持,其它取默认设置即可。为了简单,这里直接把 about 对话框作些改变,作为发送信息界面。
这里通过失去对话框来得到发送的字符串、获得焦点时把字符串发送出去。创建 oicq 类的窗口,获得 VIEW 类指针,进而可以把接收到的信息显示出来。
extern CString bb;
void CAboutDlg::OnKillFocus(CWnd* pNewWnd)
{
     // TODO: Add your message handler code here
     CDialog::OnKillFocus(pNewWnd);
     bb=m_edit;
}
 
对于OICQVIEW类
char aa[100];
CString mm;
CDC* pdc;
class mysock:public CSocket // 派生mysock类,此类既有接受功能
{
public :
     void OnReceive(int nErrorCode) // 可以随时接收信息
     {
         CSocket::Receive((void*)aa,100,0);
         mm=aa;
         CString ll=" ";// 在显示消息之前,消除前面发送的消息
        
         pdc->TextOut(50,50,ll);
         pdc->TextOut(50,50,mm);
     }
};
 
mysock sock1;
CString bb;
 
BOOL COicqView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
     CView::OnSetFocus(pOldWnd);
     // TODO: Add your message handler code here and/or call default
     bb="besting:"+bb; // 确定发送者身份为besting
     sock1.SendTo(bb,100,1060,"192.168.0.255",0); // 获得焦点以广播形式发送信息,端口号为1060
    
     return CView::OnSetCursor(pWnd, nHitTest, message);
}
 
int COicqView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
     if (CView::OnCreate(lpCreateStruct) == -1)
         return -1;
    
     sock1.Create(1060,SOCK_DGRAM,NULL);// 以数据报形式发送消息
     static CClientDC wdc(this); // 获得当前视类的指针
     pdc=&wdc;
    
     // TODO: Add your specialized creation code here
    
     return 0;
}
运行一下,打开 ABOUT 对话框,输入发送信息, enter 键就可以发送信息了,是不是有点像 qq 啊?
 
 
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值