/*
2009-11-17 20:47:46
Author:BY.Feng
*/
关于学习阻塞模式和非阻塞模式是收集的资料
Socket Modes
As we mentioned, Windows sockets perform I/O operations in two socket operating modes:
blocking and non-blocking.
In blocking mode, Winsock calls that perform I/O—such as send and recv—wait
until the operation is complete before they return to the program.
In non-blocking mode, the Winsock functions return immediately.
Applications running on the Windows CE and Windows 95 (with Winsock 1) platforms,
which support few of the I/O models, require you to take certain steps with blocking
and non-blocking sockets to handle a variety of situations.
Blocking Mode
Blocking sockets cause concern
because any Winsock API call on a blocking socket can do just that—block for some period of time.
Most Winsock applications follow a producer-consumer model
in which the application reads
(or writes) a specified number of bytes and performs some computation on that data.
The following code snippet illustrates this model:
SOCKET sock;
char buff[256];
int done = 0,
nBytes;
...
while(!done)
{
nBytes = recv(sock, buff, 65);
if (nBytes == SOCKET_ERROR)
{
printf("recv failed with error %d/n",
WSAGetLastError());
Return;
}
DoComputationOnData(buff);
}
...
The problem with this code is that the recv function might
never return if no data is pending because the statement
says to return only after reading some bytes from the system's
input buffer.
Some programmers might be tempted to peek for the necessary number of bytes
in the system's buffer by using the MSG_PEEK flag in recv or
by calling ioctlsocket with the FIONREAD option. Peeking for data without
actually reading it is considered bad programming practice and
should be avoided at all costs (reading the data actually removes it
from the system's buffer). The overhead associated with peeking is
great because one or more system calls are necessary just to check the number of bytes available.
Then, of course, there is the overhead of making the recv call that removes the data from the system buffer.
To avoid this, you need to prevent the application from totally freezing
because of lack of data (either from network problems or from client problems)
without continually peeking at the system network buffers. One method is to separate
the application into a reading thread and a computation thread.
Both threads share a common data buffer.
Access to this buffer is protected with a synchronization object,
such as an event or a mutex.
The purpose of the reading thread is to continually
read data from the network and place it in the shared buffer.
When the reading thread has read
the minimum amount of data necessary for the computation thread to do its work,
it can signal an event that notifies the computation thread to begin.
The computation thread then removes a chunk of data from the buffer and performs the necessary calculations.
The following section of code illustrates this approach by providing two functions: one responsible for reading network data (ReadThread) and one for performing the computations on the data (ReadThread).
#define MAX_BUFFER_SIZE 4096
// Initialize critical section (data) and create
// an auto-reset event (hEvent) before creating the
// two threads
CRITICAL_SECTION data;
HANDLE hEvent;
SOCKET sock;
TCHAR buff[MAX_BUFFER_SIZE];
int done=0;
// Create and connect sock
...
// Reader thread
void ReadThread(void)
{
int nTotal = 0,
nRead = 0,
nLeft = 0,
nBytes = 0;
while (!done)
{
nTotal = 0;
nLeft = NUM_BYTES_REQUIRED;
// However many bytes constitutes
// enough data for processing
// (i.e. non-zero)
while (nTotal != NUM_BYTES_REQUIRED)
{
EnterCriticalSection(&data);
nRead = recv(sock, &(buff[MAX_BUFFER_SIZE - nBytes]),
nLeft, 0);
if (nRead == -1)
{
printf("error/n");
ExitThread();
}
nTotal += nRead;
nLeft -= nRead;
nBytes += nRead;
LeaveCriticalSection(&data);
}
SetEvent(hEvent);
}
}
// Computation thread
void ProcessThread(void)
{
WaitForSingleObject(hEvent);
EnterCriticalSection(&data);
DoSomeComputationOnData(buff);
// Remove the processed data from the input
// buffer, and shift the remaining data to
// the start of the array
nBytes -= NUM_BYTES_REQUIRED;
LeaveCriticalSection(&data);
}
One drawback of blocking sockets is that communicating via more than one
connected socket at a time becomes difficult for the application.
Using the foregoing scheme, the application could be modified to
have a reading thread and a data processing thread per connected socket.
This adds quite a bit of housekeeping overhead, but it is a feasible solution.
The only drawback is that the solution does not scale well once you start dealing with a large number of sockets.
Non-blocking Mode
The alternative to blocking sockets is non-blocking sockets.
Non-blocking sockets are a bit more challenging to use,
but they are every bit as powerful as blocking sockets,
with a few advantages.
The following example illustrates how to create a socket and put it into non-blocking mode.
SOCKET s;
unsigned long ul = 1;
int nRet;
s = socket(AF_INET, SOCK_STREAM, 0);
nRet = ioctlsocket(s, FIONBIO, (unsigned long *) &ul);
if (nRet == SOCKET_ERROR)
{
// Failed to put the socket into non-blocking mode
}
Once a socket is placed in non-blocking mode, Winsock API calls that deal with sending and receiving data or connection management return immediately. In most cases, these calls fail with the error WSAEWOULDBLOCK, which means that the requested operation did not have time to complete during the call. For example, a call to recv returns WSAEWOULDBLOCK if no data is pending in the system's input buffer. Often additional calls to the same function are required until it encounters a successful return code. Table 5-2 describes the meaning of WSAEWOULDBLOCK when returned by commonly used Winsock calls.
Table 5-2 WSAEWOULDBLOCK Errors on Non-blocking Sockets Function Name
Description
WSAAccept and accept
The application has not received a connection request. Call again to check for a connection.
closesocket
In most cases, this means that setsockopt was called with the SO_LINGER option and a nonzero timeout was set.
WSAConnect and connect
The connection is initiated. Call again to check for completion.
WSARecv, recv, WSARecvFrom, and recvfrom
No data has been received. Check again later.
WSASend, send, WSASendTo, and sendto
No buffer space available for outgoing data. Try again later.
Because non-blocking calls frequently fail with the WSAEWOULDBLOCK error,
you should check all return codes and be prepared for failure at any time.
The trap many programmers fall into is that of continually calling a function until it returns a success.
For example, placing a call to recv in a tight loop
to read 200 bytes of data is no better
than polling a blocking socket
with the MSG_PEEK flag mentioned previously.
Winsock's socket I/O models can help an application determine
when a socket is available for reading and writing.
Each socket mode—blocking and non-blocking—has advantages and disadvantages.
Blocking sockets are easier to use from a conceptual
standpoint but become difficult to manage when dealing with multiple
connected sockets or when data is sent and received in varying amounts
and at arbitrary times. On the other hand, non-blocking sockets are more
difficult because more code needs to be written
to handle the possibility of receiving a WSAEWOULDBLOCK error on every Winsock call.
Socket I/O models help applications manage communications on one or more sockets
at a time in an asynchronous fashion
socket阻塞和非阻塞的区别
简单点说:
阻塞就是干不完不准回来,
非组赛就是你先干,我现看看有其他事没有,完了告诉我一声
我们拿最常用的send和recv两个函数来说吧...
比如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不代表数据已经成功的发送出去了,如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话...这时候就体现出阻塞和非阻塞的不同之处了:对于阻塞模式的socket send函数将不返回直到系统缓冲区有足够的空间把你要发送的数据Copy过去以后才返回,而对于非阻塞的socket来说send会立即返回WSAEWOULDDBLOCK告诉调用者说:"发送操作被阻塞了!!!你想办法处理吧..."
对于recv函数,同样道理,该函数的内部工作机制其实是在等待TCP/IP协议栈的接收缓冲区通知它说:嗨,你的数据来了.对于阻塞模式的socket来说如果TCP/IP协议栈的接收缓冲区没有通知一个结果给它它就一直不返回:耗费着系统资源....对于非阻塞模式的socket该函数会马上返回,然后告诉你:WSAEWOULDDBLOCK---"现在没有数据,回头在来看看"
扩展:
在进行网络编程时,我们常常见到同步、异步、阻塞和非阻塞四种调用方式。这些方式彼此概念并不好理解。下面是我对这些术语的理解。
同步
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。
异步
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以 CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是起功能已经由异步转化为同步),当一个客户端通过调用 Connect函数发出一个连接请求后,调用者线程立刻可以朝下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行 部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部 件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重 的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同 步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数 据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操 作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。
阻塞通信
--------------------------------------------------------------------------------
通过重叠通信和计算在许多系统能提高性能。由一个智能通信控制器自动地执行通信的系统是真实的。轻-重线索是取得这种重叠的一种机制。导致好性能的 一个可选的机制是使用非阻塞通信。一个阻塞发送开始调用初始化这个发送操作,但不完成它。在这个消息被从这个发送缓存拷出以前,这个发送开始调用将返回。 需要一个独立的“发送完成”调用完成这个通信,例如,检验从发送缓存拷出的数据。用适当的硬件,在发送被初始化后和它完成以前,来自发送者存储的数据转换 可以和在发送者完成的计算同时进行。类似地,一个非阻塞“接收开始调用”初始化这个接收操作, 但不完成它。在一个消息被存入这个接收缓存以前,这个调用将返回。须要一个独立的“接收完成”调用完成这个接收操作,并检验被接收到这个接收缓存的数据。 用适当的硬件,在接收操作初始化后和它完成以前,到接收者存储的数据转换可以和计算同时进行。非阻塞接收的使用虽着信息较早地在接收缓存位置被提供,也可 以避免系统缓存和存储器到存储器拷贝。
非阻塞发送开始调用能使用与阻塞发送一样的四种模式: 标准, 缓存, 同步和准备好模式。这些具有同样的意义。无论一个匹配接收是否已登入,能开始除“准备好”以外的所有模式的发送;只要一个匹配接收已登入,就能开始一个非 阻塞“准备好”发送。在所有情况下,发送开始调用是局部的:无论其它进程的状态如何,它立刻返回。如果这个调用使得一些系统资源用完,那么它将失败并返回 一个错误代码。高质量的MPI实现应保证这种情况只在“病态”时发生。即,一个MPI实现将能支持大数量挂起非阻塞操作。
当数据已被从发送缓存拷出时,这个发送完成调用返回。它可以带有附加的意义,这取决于发送模式。
如果发送模式是“同步的”,那么只有一个匹配接收已开始这个发送才能完成。即,一个接收已被登入,并已和这个发送匹配。这时,这个发送完成调用是非 局部的。注意,在接收完成调用发生以前,如果一个同步、非阻塞发送和一个非阻塞接收匹配, 它可以完成。(发送者一“知道”转换将结束,它就能完成,但在接收者“知道”转换将结束以前)。
如果发送模式是“缓存”,并没有挂起接收,那么消息必须被缓存。这时,发送完成调用是局部的,而且无论一个匹配接收的状态如何,它必须成功。
如果发送模式是标准的,同时这个消息被缓存,那么在一个匹配接收发生以前,发送结束调用可以返回。另一方面,发送完成直到一个匹配接收发生才可以完成,并且这个消息已被拷到接收缓存。
非阻塞发送能被用阻塞接收匹配,反过来也可以。
给用户的建议. 一个发送操作的完成, 对于标准模式可以被延迟, 对于同部模式必须延迟, 直到一个匹配接收登入。这两种情况下非阻塞发送的使用允许发送者提前于接收者进行,以便在两进程的速度方面,计算更容忍波动。
缓存和准备好模式中的非阻塞发送有一个更有限的影响。一可能一个非阻塞发送将返回,而一个阻塞发送将在数据被从发送者存储拷出后返回。只要在数据拷贝能和计算同时的情况下,非阻塞发送的使用有优点。
消息发送模式隐含着由发送者初始化通信。当发送者初始化通信(数据被直接移到接收缓存, 并不要求排队一个挂起发送请求) 时,如果一个接收已登入,这个通信一般将有较低的额外负担。但是,只在匹配发送已发生后,一个接收操作能完成。当非阻塞接收等待发送时,没有阻塞接收,它 的使用允许得到较低的通信额外负担。(给用户的建议结束)。
说明:
对于send函数的功能是将数据拷贝到套接字缓冲区,如果套接字缓冲区不空则阻塞。
recv函数是从套接字缓冲区拷贝数据到我们的缓冲区,如果套接字缓冲区为空则阻塞。
send/recv的缓冲区是分开的,不是一个。缓冲区大小是可以改变的。
如:
系统提供的socket缓冲区大小为8K,你可以将之设置为64K,尤其在传输实时视频时。
//设置发送和接收缓冲区
int rcvbuf;
int rcvbufsize=sizeof(int);
if(getsockopt(m_Socket,SOL_SOCKET,SO_RCVBUF,(char*)
&rcvbuf,&rcvbufsize)!=SOCKET_ERROR)
{
if(rcvbuf<65536)
rcvbuf=65536;
setsockopt(m_Socket,SOL_SOCKET,SO_RCVBUF,(char*)
&rcvbuf,rcvbufsize);
}
if(getsockopt(m_Socket,SOL_SOCKET,SO_SNDBUF,(char*)
&rcvbuf,&rcvbufsize)!=SOCKET_ERROR)
{
if(rcvbuf<65536)
rcvbuf=65536;
setsockopt(m_Socket,SOL_SOCKET,SO_SNDBUF,(char*)
&rcvbuf,rcvbufsize);
}
关于缓冲区大小的讨论问题:
一个包没有固定长度,以太网限制在46-1500字节,1500就是以太网的MT
U,超过这个量,TCP会为IP数据报设置偏移量进行分片传输,现在一般可
允许应用层设置8k(NTFS系统)的缓冲区,8k的数据由底层分片,而应用
层看来只是一次发送。
windows的缓冲区经验值是4k
Socket本身分为两种,流(TCP)和数据报(UDP),你的问题针对这两种不同
使用而结论不一样。甚至还和你是用阻塞、还是非阻塞Socket来编程有关
。
1、通信长度,这个是你自己决定的,没有系统强迫你要发多大的包,实
际应该根据需求和网络状况来决定。对于TCP,这个长度可以大点,但要
知道,Socket内部默认的收发缓冲区大小大概是8K,你可以用SetSockOpt
来改变。但对于UDP,就不要太大,一般在1024至10K。注意一点,你无论
发多大的包,IP层和链路层都会把你的包进行分片发送,一般局域网就是
1500左右,广域网就只有几十字节。分片后的包将经过不同的路由到达接
收方,对于UDP而言,要是其中一个分片丢失,那么接收方的IP层将把整
个发送包丢弃,这就形成丢包。显然,要是一个UDP发包佷大,它被分片
后,链路层丢失分片的几率就佷大,你这个UDP包,就佷容易丢失,但是
太小又影响效率。最好可以配置这个值,以根据不同的环境来调整到最佳
状态。
send()函数返回了实际发送的长度,在网络不断的情况下,它绝不会返回
(发送失败的)错误,最多就是返回0。对于TCP你可以字节写一个循环发送
。当send函数返回SOCKET_ERROR时,才标志着有错误。
但对于UDP,你不要写循环发送,否则将给你的接收带来极大的麻烦。所
以UDP需要用SetSockOpt来改变Socket内部Buffer的大小,以能容纳你的
发包。明确一点,TCP作为流,发包是不会整包到达的,而是源源不断的
到,那接收方就必须组包。而UDP作为消息或数据报,它一定是整包到达
接收方。
2、关于接收,一般的发包都有包边界,首要的就是你这个包的长度要让
接收方知道,于是就有个包头信息,对于TCP,接收方先收这个包头信息
,然后再收包数据。一次收齐整个包也可以,可要对结果是否收齐进行验
证。这也就完成了组包过程。UDP,那你只能整包接收了。要是你提供的
接收Buffer过小,TCP将返回实际接收的长度,余下的还可以收,而UDP不
同的是,余下的数据被丢弃并返回WSAEMSGSIZE错误。注意TCP,要是你提
供的Buffer佷大,那么可能收到的就是多个发包,你必须分离它们,还有
就是当Buffer太小,而一次收不完Socket内部的数据,那么Socket接收事
件(OnReceive),可能不会再触发,使用事件方式进行接收时,密切注意
这点。这些特性就是体现了流和数据包的区别。
补充一点,接收BuffSize >= 发送BuffSize >= 实际发送Size,对于内外
部的Buffer都适用,上面讲的主要是Socket内部的Buffer大小关系。
3、TCP是有多少就收多少,如果没有当然阻塞Socket的recv就会等,直到
有数据,非阻塞Socket不好等,而是返回WSAEWOULDBLOCK。UDP,如果没
有数据,阻塞Socket就会等,非阻塞Socket也返回WSAEWOULDBLOCK。如果
有数据,它是会等整个发包到齐,并接收到整个发包,才返回。