套接字模式:阻塞套接字和非阻塞套接字。或者叫同步套接字和异步套接字。
套接字模型:描述如何对套接字的I/O行为进行管理。
Winsock提供的I/O模型一共有五种:
select,WSAAsyncSelect,WSAEventSelect,Overlapped,Completion。今天先讲解select。
1:select模型择模(选型)
先看一下下面的这句代码:
阻塞socket:
int iResult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。
在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
非阻塞 socket:
再看代码:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
//-------------------------
// Initialize Winsock
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != NO_ERROR)
printf("Error at WSAStartup()/n");
//-------------------------
// Create a SOCKET object.
SOCKET m_socket;
m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_socket == INVALID_SOCKET) {
printf("Error at socket(): %ld/n", WSAGetLastError());
WSACleanup();
return;
}
//-------------------------
// Set the socket I/O mode: In this case FIONBIO
// enables or disables the blocking mode for the
// socket based on the numerical value of iMode.
// If iMode = 0, blocking is enabled;
// If iMode != 0, non-blocking mode is enabled.
ioctlsocket(m_socket, FIONBIO, (u_long FAR*) &iMode);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。 看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
多线程来解决使用阻塞套接字存在的问题:
多线程来解决阻塞套接字的方法是为阻塞套接字的IO操作创建单独的线程,阻塞的套接字IO操作放在单独的线程中,而不会因为套接字IO操作的阻塞造成整个主线程的阻塞,但是这样也会造成一定的问题:
1) 如果是多个套接字的场合通过多线程来解决主线程阻塞就会显得不合适了,server端创建一个监听socket来负责监听连接,而为accept函数
为每个client端连接创建一个套接字,这样就会创建很多的套接字。如果是创建不同的套接字则应该创建多个线程,而每个线程的线程函数是
不同的,这样就造成了所谓的扩展性很差。
2)如果不是每个连接创建一个套接字的话,duoxanch方法比较直观,程序非常简单而且可移植性好,但是不能利用平台相关的特性。例如,如果连接数增多的时候(成千上万的连接),那么线程数成倍增长,操作 系统忙于频繁的线程间切换,而且大部分线程在其生命周期内都是处于非活动状态的,这大大浪费了系统的资源。所以,如果你已经知道你的代码只会运行在 Windows平台上,建议采用Winsock I/O模型。
微软提供了select函数来解决这个问题:
int select(
int nfds,
fd_set FAR *readfds,
fd_set FAR *writefds,
fd_set FAR *exceptfds,
const struct timeval FAR *timeout
);
第一个参数不要管,会被系统忽略的。第二个参数是用来检查套接字可读性,也就说检查套接字上是否有数据可读,同样,第三个参数用来检查数据是否可以发出。最后一个是检查是否有带外数据可读取。
最后一个参数是用来设置select等待多久的,是个结构:
struct timeval {
long tv_sec; // seconds
long tv_usec; // and microseconds
};
如果将这个结构设置为(0,0),那么select函数会马上返回。
说了这么久,select的作用到底是什么?
他的作用就是:
1)防止在在阻塞模式的套接字里被锁死
2)避免在非阻塞套接字里重复检查WSAEWOULDBLOCK错误。
他的工作流程如下:
1:用FD_ZERO宏来初始化我们感兴趣的fd_set,也就是select函数的第二三四个参数。
2:用FD_SET宏来将套接字句柄分配给相应的fd_set。
3:调用select函数。
4:用FD_ISSET对套接字句柄进行检查,如果我们所关注的那个套接字句柄仍然在开始分配的那个fd_set里,那么说明马上可以进行相应的IO操作。比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞。
1 #include "stdafx.h"
2 #include <iostream>
3 #include <winsock2.h>
4 #include <windows.h>
5
6 #define TRACE ATLTrace //必须要加上这个宏定义,否则在WIN32的控制台程序中是不能直接用的
7
8 #define InternetAddr "127.0.0.1"
9 #define iPort 5055
10
11 #pragma comment(lib, "ws2_32.lib")
12
13 int _tmain(int argc, _TCHAR* argv[])
14 {
15 WSADATA wsa;
16 WORD wVersionRequested;
17 int err;
18
19 wVersionRequested = MAKEWORD( 2, 2 );
20 err = WSAStartup( wVersionRequested, &wsa);
21 if ( err != 0 ) {
22 //Tell the user that we could not find a usable
23 //WinSock DLL.
24 TRACE("你忘记添加WinSock DLL了/n");
25 WSACleanup();
26 return 1;
27 }
28
29 // Create a SOCKET for listening for incoming connection requests
30 SOCKET fdServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
31
32 sockaddr_in server;
33
34 server.sin_family = AF_INET;
35 server.sin_addr.s_addr = inet_addr(InternetAddr);
36 server.sin_port = htons(iPort);
37 //Bind the socket.
38 int ret = bind(fdServer, (sockaddr*)&server, sizeof(server));
39 ret = listen(fdServer, 4);
40
41 SOCKET AcceptSocket;
42 fd_set fdread;
43 timeval tv;
44 int nSize;
45 //其实也算是轮训,那么对阻塞socket用select和对使用非阻塞socket的优点在哪?
46 //可能的优点就是避免在非阻塞套接字里重复检查WSAEWOULDBLOCK错误。
47 while(1)
48 {
49
50 FD_ZERO(&fdread);//初始化fd_set
51 FD_SET(fdServer, &fdread);//分配套接字句柄到相应的fd_set
52
53 tv.tv_sec = 2;//这里我们打算让select等待两秒后返回,避免被锁死,也避免马上返回
54 tv.tv_usec = 0;
55
56 select(0, &fdread, NULL, NULL, &tv);
57
58 nSize = sizeof(server);
59 //先判断fdServer是否还在fd_set内来判断是否可以读,这样就避免因为 accept在等待
60 //时造成的阻塞
61 if (FD_ISSET(fdServer, &fdread))
62 //如果套接字句柄还在fd_set里,说明客户端已经有connect的请求发过来了,
63 //马上可以accept成功
64 {
65 AcceptSocket = accept(fdServer,( sockaddr*) &server, &nSize);
66 break;
67 }
68 else
69 //还没有客户端的connect请求,我们可以去做别的事,避免像没有用select方式
70 //的阻塞套接字程序被锁死的情况,如果没用select,当程序运行到accept的时候客户
71 //端恰好没有connect请求,那么程序就会被锁死,做不了任何事情
72 {
73 //do something
74 MessageBox(NULL, "waiting", "recv", MB_ICONINFORMATION);
75 //别的事做完后,继续去检查是否有客户端连接请求
76 }
77 }
78
79 char buffer[128];
80 ZeroMemory(buffer, 128);
81
82 ret = recv(AcceptSocket,buffer,128,0);//这里同样可以用select,用法和上面一样
83
84 MessageBox(NULL, buffer, "recv", MB_ICONINFORMATION);
85
86 closesocket(AcceptSocket);
87 WSACleanup();
88 return 0;
89 }
90
select函数的返回值 :
函数失败的返回值:调用失败返回SOCKET_ERROR,超时返回0。
int ret;
if((ret=select(0,&fdread,NULL,NULL,NULL))==SOCKET_ERROR)
{
//Error Condition
}
if(ret > 0)//ret>0这个ret值表示满足条件的socket的数量,不止一个socket满足IO操作的条件
{
if(FD_ISSET(fdServer,&fdread))
{
//A read event has occured on socket fdServer
}
}