在windows socket api 下:
异步方式指的是发送方不等接收方响应,便接着发下个数据包的通信方式;而同步指发送方发出数据后,等收到接收方发回的响应,才发下一个数据包的通信方式。
阻塞套接字是指执行此套接字的网络调用时,直到成功才返回,否则一直阻塞在此网络调用上,比如调用recv()函数读取网络缓冲区中的数据,如果没有数据到达,将一直挂在recv()这个函数调用上,直到读到一些数据,此函数调用才返回;而非阻塞套接字是指执行此套接字的网络调用时,不管是否执行成功,都立即返回。比如调用recv()函数读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。在实际Windows网络通信软件开发中,异步非阻塞套接字是用的最多的。
(同步阻塞、异步非阻塞)
1、默认用作同步阻塞方式,那就是当你从不调用WSAIoctl()和ioctlsocket()来改变Socket IO模式,也从不调用WSAAsyncSelect()和WSAEventSelect()来选择需要处理的Socket事件。正是由于函数accept(),WSAAccept(),connect(),WSAConnect(),send(),WSASend(),recv(),WSARecv()等函数被用作阻塞方式,所以可能你需要放在专门的线程里,这样以不影响主程序的运行和主窗口的刷新。
2、如果作为异步非阻塞方式用,那么程序主要就是要处理事件。它有两种处理事件的办法:
第一种,它常关联一个窗口,也就是异步Socket的事件将作为消息发往该窗口,这是由WinSock扩展规范里的一个函数WSAAsyncSelect()来实现和窗口关联。最终你只需要处理窗口消息,来收发数据。
第二种,用到了扩展规范里另一个关于事件的函数WSAEventSelect(),它是用事件对象的方式来处理Socket事件,也就是,你必须首先用WSACreateEvent()来创建一个事件对象,然后调用WSAEventSelect()来使得Socket的事件和这个事件对象关联。最终你将要在一个线程里用WSAWaitForMultipleEvents()来等待这个事件对象被触发。这个过程也稍显复杂。
要点一、UNIIX BSD下SOCKET
【UNIIX BSD下SOCKET主要是同步的,但有阻塞和非阻塞两种方式。阻塞方式定义与前面定义相同,要解决阻塞有两种方法:
一种是设置SOCKET属性,设置为非阻塞(fcntl()函数),
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
通过设置套接字为非阻塞,你能够有效地"询问"套接字以获得信息。如果尝试着从一个非阻塞的套接字读信息并且没有任何数据,它不允许阻 塞,它将返回 -1 并将 errno 设置为 EWOULDBLOCK。 但是一般说来,这种询问不是个好主意。如果让程序在忙等状 态查询套接字的数据,将浪费大量的 CPU 时间。更好的解决之道是用 下一章讲的 select() 去查询是否有数据要读进来。
另一种是使用select()函数,
同步方式中解决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
{
//发送
}
对于Windows这种非抢先多任务操作系统来说,这两种工作方式都是很难以接受的,为此,WINSOCK在尽量与BSD Socket保持一致外,又对它作了必要的扩充
】
附:
改变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
);
服务方同一端口多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);
要点二、windows socket api
WINSOCK对BSD Socket的扩充主要是在基于消息、对网络事件的异步存取接口上。下表列出了WINSOCK扩充的函数功能。
函 数 名 | 功 能 |
WSAAsyncGetHostByAddr() | 标准Berkeley函数getXbyY的异步版本,例 |
WSAAsyncGetHostByName() | 如:函数WSAAsyncGetHostByName()就是提 |
WSAAsyncGetProtoByName() | 供了标准Berkeley函数gethostbyname的一 |
WSAAsyncGetProtoByNumber() | 种基于消息的异步实现。 |
WSAAsyncGetServByName() |
|
WSAAsyncGetServByPort() |
|
WSAAsyncSelect() | 函数select()的异步版本 |
WSACancelAsyncRequest() | 取消函数WSAAsyncGetXByY执行中的实例 |
WSACancelBlockingCall() | 取消一个执行中的“阻塞”API调用 |
WSACleanup() | 终止使用隐含的Windows Sockets DLL |
WSAGetLastError() | 获取Windows Sockets API的最近错误号 |
WSAIsBlocking() | 检测隐含的Windows Sockets DLL是否阻塞了一个当前线索的调用 |
WSASetBlockingHook() | 设置应用程序自己的“阻塞”处理函数 |
WSASetLastError() | 设置Windows Sockets API的最近错误号 |
WSAStartup() | 初始化隐含的Windows Sockets DLL |
WSAUnhookBlockingHook() | 恢复原来的“阻塞”处理函数 |
从表1可以看出,WINSOCK的扩充功能可以分为如下几类:
(1)异步选择机制:
异步选择函数WSAAsyncSelect()允许应用程序提名一个或多个感兴趣的网络事件,所有阻塞的网络I/O例程(如send()和resv()),不管它是已经使用还是即将使用,都可作为WSAAsyncSelect()函数选择的候选。当被提名的网络事件发生时,Windows应用程序的窗口函数将收到一个消息,消息附带的参数指示被提名过的某一网络事件。
(2)异步请求例程:
异步请求例程允许应用程序用异步方式获取请求的信息,如WSAAsyncGetXByY()类函数允许用户请求异步服务,这些功能在使用标准Berkeley函数时是阻塞的。函数WSACancelAsyncRequest()允许用户终止一个正在执行的异步请求。
(3)阻塞处理方法:
WINSOCK在调用处于阻塞时进入一个叫“Hook”的例程,它负责处理Windows消息,使得Windows的消息循环能够继续。WINSOCK还提供了两个函数(WSASetBlockingHook()和WSAUnhookBlockingHook())让用户能够设置和取消自己的阻塞处理例程。另外,函数WSAIsBlocking()可以检测调用是否阻塞,函数WSACancelBlockingCall()可以取消一个阻塞的调用。
(4)出错处理:
为了和以后的多线索环境(如Windows/NT)兼容,WINSOCK提供了两个出错处理函数WSAGetLastError()和WSASetLastError()来获取和设置本线索的最近错误号。
(5)启动与终止:
WINSOCK的应用程序在使用上述WINSOCK函数前,必须先调用WSAStartup()函数对Windows Sockets DLL进行初始化,以协商WINSOCK的版本支持,并分配必要的资源。在应用程序退出之前,应该先调用函数WSAClearnup()终止对Windows Sockets DLL的使用,并释放资源,以利下一次使用。
在这些函数中,实现Windows网络实时通信的关键是异步选择函数WSAAsyncSelect()的使用,其原型如下:
int PASCAL FAR WSAAsyncSelect(SOCTET s,HWND hWnd,unsigned int wMsg,long lEvent);
它请求Windows Sockets DLL在检测到在套接字s上发生的lEvent事件时,向窗口hWnd发送一个消息wMsg。它自动地设置套接字s处于非阻塞工作方式。参数lEvent由下列事件的一个或多个组成:
值 含 义
FD_READ 希望在套接字s收到数据(即读准备好)时接到通知
FD_WRITE 希望在套接字s可发送数据(即写准备好)时接到通知
FD_OOB 希望在套接字s上有带外数据到达时接到通知
FD_ACCEPT 希望在套接字s上有外部连接到来时接到通知
FD_CONNECT 希望在套接字s连接建立完成时接到通知
FD_CLOSE 希望在套接字s关闭时接到通知
表2. 异步选择网络事件表
例如,我们要在套接字s读准备好或写准备好时接到通知,可以使用下面的语句:
rc = WSAAsyncSelect(s, hWnd, wMsg, FD_READ | FD_WRITE);
当套接字s上被提名的一个网络事件发生时,窗口hWnd将收到消息wMsg,变量lParam的低字指示网络发生的事件,高字指示错误码。应用程序就可以通过这些信息来决定自己的下一步动作。
【理解:基于消息驱动的可称之为“异步”。】
[引1:]
熟悉WINSOCK编程的读者一定会觉得奇怪吧,为什么INDY是是完全基于SOCKET阻塞工作模式的呢?异步模式(非阻塞模式)是WINSOCK的一大特点,为什么不用呢?
其实,之所以大多数WINDOWS下的INTERNET程序都使用异步模式,这和WINSOCK的历史有关。当WINSOCK被移植到WINDOWS的时候,当时的WINDOWS操作系统还是WINDOWS 3.1,而WINDOWS 3.1是不支持多线程的,不象UNIX下可以使用FORK来运行多进程。在WINDOWS 3.1下,如果使用阻塞模式,在通讯时会锁定用户界面使程序没有响应,为了避免这种情况,WINSOCK就引入异步模式这个新特性。而使用异步模式来编制INTERNET程序也就成了WINDOWS程序员的经典教条。但是,随着新的WINDOWS操作系统的出现,如WINDOWS 95、NT、98、ME、2000等,这些操作系统开始支持多线程。异步模式这个教条仍然深入人心,使很多程序员会下意识的拒绝使用阻塞模式。
事实上,UNIX下的SOCKET只支持阻塞模式(现在UNXI的SOCKET有了一些新的非阻塞特性,不过绝大多数应用仍然使用阻塞模式)。阻塞模式具有以下几个比异步模式优越的特点:
编程更简单,可以把所有处理SOCKET的代码放在一起,顺序执行,而不用分散在不同的事件处理代码段里。
更容易移植到UNIX,使用INDY的DELPHI程序,可以不做太多(甚至不做)修改,就可以把WINDOWS的DELPHI源代码拿到LINUX下,用Kylix来编译成LINUX下的网络程序。