在理解这个概念前,你要知道在linux系统中,一切皆是文件
,不管是普通文件、输入输出设备、目录,或者是套接字,都被linux当做文件处理。
1)阻塞是指,当试图对某个文件描述符进行读写时,如果当前没有东西可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止。
而对于非阻塞状态,如果没有东西可读,或者不可写,读写函数马上返回,而不会等待(这也与设置的超时时间有关)。
2)非阻塞,就是进程或线程执行某个函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。
①阻塞好控制,不发送完数据程序不会往下走,但是对性能有影响;
非阻塞不太好控制,可能和能力有关,但是性能会得到很大提升。
②阻塞式的编程方便,非阻塞的编程不方便,需要开发人员处理各种返回;
③阻塞处理简单,非阻塞处理复杂;
④阻塞效率低,非阻塞效率高;
⑤阻塞模式,常见的通信模型为多线程模型,服务端accept之后,对每个socket创建一个线程去recv。逻辑上简单,适用于并发量小(客户端数目少),连续传输大数据量的情况下,比如文件服务器。还有就是在客户端接收服务器消息的时候也经常用,因为客户端就一个socket,用阻塞模式不影响效率,而且编程逻辑上要简单得多。
非阻塞模式,常见的通信模型为select模型和IOCP模型,适用于高并发,数据量小的情况,比如聊天室;客户端多的情况下,如果采用阻塞模式,需要开很多线程,影响效率。
注意,不要和同步、异步的概念搞混了。下面的解释来源于网络:
I.所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。
II.异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以 CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是其功能已经由异步转化为同步),当一个客户端通过调用 Connect函数发出一个连接请求后,调用者线程立刻可以向下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
同步/异步和阻塞/非阻塞的区别
1.阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回;
对同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已
2.非阻塞是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
好了,对这些有了了解后,可以帮助你更好的理解select模型。
函数原型:
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
返回值:成功返回做好准备的文件描述符的个数;超时为0;错误为 -1.
参数:
maxfdp:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。+1的原因:[0,maxfd],描述符是从0开始的,因此如果最大的描述符为n的话,共有n+1个描述符
。
fd_set *readset:指向fd_set结构的指针,监视这些文件描述符的读变化;
fd_set *writeset:监视集合中文件描述符的写变化,只要有一个文件可写,函数就返回一个大于0的值;
fd_set *exceptset:同上,监视集合中错误异常文件;
struct timeval *timeout:select的超时时间。这个参数至关重要,它可以使select处于三种状态,
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一直等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
I.传递给select函数的参数会告诉内核:
II.从select函数返回后,内核会告诉我们:
对fd_set类型的变量,可以使用以下几个宏控制它
int FD_ZERO(int fd, fd_set *set); //将一个 fd_set类型变量的所有位都置为 0
int FD_CLR(int fd, fd_set *set); //清除某个位
int FD_SET(int fd, fd_set *set); //将变量的某个位置位
int FD_ISSET(int fd, fd_set *set); //测试某个位是否被置位
理解select模型,关键是理解fd_set
。为了方便说明,以fd_set长度为1B(1字节)为例,fd_set的每一位(bit)可以对应一个文件描述符(fd)。则1B的fd_set可以对应8个fd.
1)执行fd_set set,FD_ZERO(&set);则set用位表示是0000,0000
2)若fd=3,执行FD_SET(fd,&set);后set变为0000,0100(第3位为1)
3)若再加入fd=2,fd=1,则set变为0000,0111
4)执行select(5,&set,0,0,0)阻塞等待
5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=3位置被清空。
注意,
a.可监控的文件描述符个数取决与sizeof(fd_set)的值;
b.将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于在select返回后,把array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
c.必须在select前循环array(加入fd,取maxfd),select返回后循环array(使用FD_ISSET判断是否有(读/写/异常)事件发生)。
在阻塞的TCP server&client中,加入select函数
即在listen之后,将套接字描述符全部加入fd_set,然后按照下面的顺序编写代码
1)FD_ZERO()清空fd_set;
2)FD_SET()将要测试的fd加入fd_set;
3)select()测试fd_set中所有的fd;
4)FD_ISSET()测试是否有符合条件的描述符
server.cpp
/*
* server.cpp --非阻塞TCP server
*
* Created on: Nov 23, 2019
* Author: xb
*/
#include
#include
#include
#include
#include
#include
#include
#include
#define CONCURRENT_MAX 3 //服务端同时支持的最大连接数
#define SERVER_PORT 9999 //端口
#define BUFFER_SIZE 1024 //缓冲区大小
int main(int argc, char* argv[]) {
int client_fd[CONCURRENT_MAX] = {0};//用于存放客户端套接字描述符
int server_sock_fd;//服务器端套接字描述符
char send_msg[BUFFER_SIZE];//数据传输缓冲区
char recv_msg[BUFFER_SIZE];
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//限定只接受本地连接请求
server_addr.sin_addr.s_addr = INADDR_ANY;
/*测试*/
/*for(int a = 0; a < CONCURRENT_MAX; a++){
printf("client_fd[%d] = %d\n",a,client_fd[a]);
}*/
//1.创建socket
server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock_fd < 0) {
printf("create socket error:%s(errno:%d)\n",strerror(errno),errno);
return -1;
}
//2.绑定socket和端口号
if (bind(server_sock_fd, (struct sockaddr *) &server_addr,sizeof(server_addr)) < 0) {
printf("bind socket error:%s(errno:%d)\n",strerror(errno),errno);
return -1;
}
//3.监听listen
if (listen(server_sock_fd, 5) < 0) {
printf("listen socket error:%s(errno:%d)\n",strerror(errno),errno);
return -1;
}
//fd_set
fd_set server_fd_set; //文件描述符集合
int max_fd = -1;
struct timeval tv;
while (1) {
tv.tv_sec = 10;/* 超时时间10s */
tv.tv_usec = 0;
/*
1)FD_ZERO()清空fd_set;
2)FD_SET()将要测试的fd加入fd_set;
3)select()测试fd_set中所有的fd;
4)FD_ISSET()测试是否有符合条件的描述符
*/
FD_ZERO(&server_fd_set);
//STDIN_FILENO:接收键盘输入
FD_SET(STDIN_FILENO, &server_fd_set);
if (max_fd < STDIN_FILENO) {
max_fd = STDIN_FILENO;
}
//printf("STDIN_FILENO=%d\n", STDIN_FILENO);//STDIN_FILENO = 0
//服务器端socketfd
FD_SET(server_sock_fd, &server_fd_set);
// printf("server_sock_fd=%d\n", server_sock_fd);
if (max_fd < server_sock_fd) {
max_fd = server_sock_fd;
}
//客户端连接
for (int i = 0; i < CONCURRENT_MAX; i++) {
//printf("client_fd[%d]=%d\n", i, client_fd[i]);
if (client_fd[i] != 0) {
FD_SET(client_fd[i], &server_fd_set);
if (max_fd < client_fd[i]) {
max_fd = client_fd[i];
}
}
}
/*
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
maxfdp1:集合中所有文件描述符的范围,即最大值+1
readset:监视文件描述符的读变化
writeset:监视写变化
exceptset:监视错误异常文件
timeout:超时时间
函数返回值:<0:发生错误;
=0:没有满足条件的文件描述符,等待超时;
>0:文件描述符满足条件
*/
int result = select(max_fd + 1, &server_fd_set, NULL, NULL, &tv);
if (result < 0) {
printf("select error:%s(errno:%d)\n",strerror(errno),errno);
continue;
} else if (result == 0) {
printf("select 超时\n");
continue;
} else {
//result为位状态发生变化的文件描述符的个数
//STDIN_FILENO:系统API接口库,是打开文件的句柄
/* 服务器端输入信息 */
if (FD_ISSET(STDIN_FILENO, &server_fd_set)) {
bzero(send_msg, BUFFER_SIZE);//清空
scanf("%s",&send_msg);
//输入"q"则关闭服务端
if (strcmp(send_msg, "q") == 0) {
close(server_sock_fd);
exit(0);
}
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fd[i] != 0) {
printf("发送消息给客户端:");
printf("client_fd[%d]=%d\n", i, client_fd[i]);
send(client_fd[i], send_msg, strlen(send_msg), 0);
}
}
}
/* 处理新的连接请求 */
if (FD_ISSET(server_sock_fd, &server_fd_set)) {
struct sockaddr_in client_address;
socklen_t address_len;
int client_sock_fd = accept(server_sock_fd,(struct sockaddr *) &client_address, &address_len);
printf("new connection client_sock_fd = %d\n", client_sock_fd);
if (client_sock_fd > 0) {
int index = -1;//判断连接数量是否达到最大值
for (int i = 0; i < CONCURRENT_MAX; i++) {
//如果还有空闲的连接数量,就分配给新的连接
if (client_fd[i] == 0) {
index = i;
client_fd[i] = client_sock_fd;
break;
}
}
if (index >= 0) {
printf("新客户端[%d] [%s:%d]连接成功\n", index,
inet_ntoa(client_address.sin_addr),
ntohs(client_address.sin_port));
} else {
bzero(send_msg, BUFFER_SIZE);
strcpy(send_msg, "服务器已连接的客户端数量达到最大值,连接失败!\n");
send(client_sock_fd, send_msg, strlen(send_msg), 0);
printf("客户端连接数量达到最大值,新客户端[%s:%d]连接失败\n",
inet_ntoa(client_address.sin_addr),
ntohs(client_address.sin_port));
}
}
}
/* 处理某个客户端发过来的消息 */
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fd[i] != 0) {
if (FD_ISSET(client_fd[i], &server_fd_set)) {
bzero(recv_msg, BUFFER_SIZE);
int n = recv(client_fd[i], recv_msg,BUFFER_SIZE, 0);
// >0,接收消息成功
if (n > 0) {
if (n > BUFFER_SIZE) {
n = BUFFER_SIZE;
}
recv_msg[n] = '\0';
printf("收到客户端[%d]发来的消息:%s\n", i, recv_msg);
} else if (n < 0) {
printf("从客户端[%d]接收消息出错!\n", i);
} else { //=0,对端连接关闭
FD_CLR(client_fd[i], &server_fd_set);
client_fd[i] = 0;
printf("客户端[%d]断开连接\n", i);
}
}
}
}
}
}
return 0;
}
client.cpp
/*
* client.cpp -- 非阻塞 TCP client
*
* Created on: Nov 23, 2019
* Author: xb
*/
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_SIZE 1024
int main(int argc, char* argv[]) {
char recv_msg[BUFFER_SIZE];
char send_msg[BUFFER_SIZE];//数据收发缓冲区
struct sockaddr_in server_addr;
int server_sock_fd;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//创建套接字
if ((server_sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
return -1;
}
//连接服务器
if (connect(server_sock_fd, (struct sockaddr *) &server_addr,sizeof(struct sockaddr_in)) < 0) {
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
return -1;
}
fd_set client_fd_set;
struct timeval tv;
while (1) {
tv.tv_sec = 2;
tv.tv_usec = 0;
FD_ZERO(&client_fd_set);
FD_SET(STDIN_FILENO, &client_fd_set);
FD_SET(server_sock_fd, &client_fd_set);
select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
if (FD_ISSET(STDIN_FILENO, &client_fd_set)) {
bzero(send_msg, BUFFER_SIZE);
fgets(send_msg, BUFFER_SIZE, stdin);
if (send(server_sock_fd, send_msg, BUFFER_SIZE, 0) < 0) {
printf("send message error: %s(errno:%d)\n",strerror(errno),errno);
}
}
if (FD_ISSET(server_sock_fd, &client_fd_set)) {
bzero(recv_msg, BUFFER_SIZE);
int n = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0);
if (n > 0) {
printf("recv %d byte\n",n);
if (n > BUFFER_SIZE) {
n = BUFFER_SIZE;
}
recv_msg[n] = '\0';
printf("收到服务器发送的信息:%s\n", recv_msg);
} else if (n < 0) {
printf("接收消息出错!\n");
} else {
printf("服务器已关闭!\n");
close(server_sock_fd);
exit(0);
}
}
}
return 0;
}
使用g++编译:
g++ server.cpp -o server
g++ client.cpp -o client
程序运行结果:
1)启动server
2)启动client
3)发送信息给server
4)发送信息给client
第4个client尝试连接server时
相比较阻塞的TCP server&client,非阻塞的可以连接更多客户端。阻塞的则只能连接一个,新的连接请求会被阻塞,直到上一个连接关闭。