1. 概念理解
在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:
同步/异步主要针对C端:
同步:
所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步:
异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕
阻塞/非阻塞主要针对S端:
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。
快递的例子:比如到你某个时候到A楼一层(假如是内核缓冲区)取快递,但是你不知道快递什么时候过来,你又不能干别的事,只能死等着。但你可以睡觉(进程处于休眠状态),因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
还是等快递的例子:如果用忙轮询的方法,每隔5分钟到A楼一层(内核缓冲区)去看快递来了没有。如果没来,立即返回。而快递来了,就放在A楼一层,等你去取。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。
1. 同步,就是我客户端(c端调用者)调用一个功能,该功能没有结束前,我(c端调用者)死等结果。
2. 异步,就是我(c端调用者)调用一个功能,不需要知道该功能结果,该功能有结果后通知我(c端调用者)即回调通知。
同步/异步主要针对C端, 但是跟S端不是完全没有关系,同步/异步机制必须S端配合才能实现.同步/异步是由c端自己控制,但是S端是否阻塞/非阻塞, C端完全不需要关心.
3. 阻塞, 就是调用我(s端被调用者,函数),我(s端被调用者,函数)没有接收完数据或者没有得到结果之前,我不会返回。
4. 非阻塞, 就是调用我(s端被调用者,函数),我(s端被调用者,函数)立即返回,通过select通知调用者
同步IO和异步IO的区别就在于:数据访问的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!
同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。
阻塞和非阻塞是指当server端的进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪;
而同步和异步是指client端访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。(等待"通知")
阻塞socket:
阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有得到结果之后才会返回。
对于文件操作read和fread函数调用会将线程阻塞,accept、recv和recvfrom函数调用会将线程阻塞
为避免整个进程被阻塞后挂起,所以在阻塞模式下,往往需要采用多线程技术。
但一个进程中的可并发的线程数总是有限的,在处理大量客户端socket连接(上万个)通过线程并发并不方便,效率不高。
非阻塞socket:
非阻塞调用是指调用立刻返回。在非阻塞模式下,accept、recv和recvfrom函数会立刻返回。
在非阻塞状态下调用accept函数,如果没有客户端socket连接请求,那么accpet函数返回-1,同时errno置为11
在非阻塞状态下调用recv、recvfrom函数,如果没有数据,函数返回-1,同时errno置为11,如果socket已经关闭,函数返回0
在非阻塞状态下对于一个已经关闭的socket调用send函数,将引发一个SIGPIPE信号,进程必须捕捉这个信号,因为SIGPIPE在系统默认的处理方式是关闭状态。
fcntl函数调用:
fcntl函数可以将文件或者socket描述符设置为阻塞或者非阻塞状态
int fcntl(int fd,int cmd,.../*arg*/);
参数:fd为要设置的文件描述符或者socket
参数:F_GETFL为得到目前状态,F_SETFL为设置状态
宏定义O_NONBLOCK代表非阻塞,0代表阻塞
返回值为描述符当前状态,失败小于0。
sercer端socket设置为非阻塞状态实例1:
函数实现功能:若没有client连接,则server端关闭,不再等待来自client的连接。
server.c
void setnonblocking(int st)
{
int opts = fcntl(st, F_GETFL);
if (opts < 0)
{
printf("fcntl failed %s\n", strerror(errno));
}
opts = opts | O_NONBLOCK;
if (fcntl(st, F_SETFL, opts) < 0)
{
printf("fcntl failed %s\n", strerror(errno));
}
}
int main()
{
int st = socket(AF_INET,SOCK_STREAM,0);
setnonblocking(st);//设置为非阻塞状态
int on = 1;
if(setsockopt(st,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) == -1)
{
printf("setsockopt failed %s\n",strerror(errno));
return 0;
}
struct sockaddr_in addr;//定义一个套接字地址的结构
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);//将本地字节顺序转化为网络字节顺序
addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY代表这个server上的所有地址
//服务器端程序需要将IP与server程序绑定
if(bind(st,(struct sockaddr *) &addr,sizeof(addr)) == -1)
{
printf("bind failed %s\n",strerror(errno));
return 0;
}
//服务器端开始监听listen,监听指定端口的客户端连接
if(listen(st,20) == -1)
{
printf("listen failed %s\n", strerror(errno));
return 0;
}
char s[1024];
int client_st = 0;//client端socket
struct sockaddr_in client_addr;//表示client端的IP地址
int i = 0;
for(i = 0;i < 5;i++)
{
memset(&client_addr,0,sizeof(client_addr));
socklen_t len = sizeof(client_addr);
//accept会阻塞,直到有新的客户端连接起来,accept返回一个client的socket描述符
//同时原来的套接口继续监听指定端口号
//若没有客户端与服务器端连接,程序则会一直等待,直到有客户端连接才继续执行
client_st = accept(st,(struct sockaddr *) &client_addr,&len);//accept返回一个client的socket描述符
if(client_st == -1)
{
if(errno == EAGAIN)
{
sleep(1);
continue;
}
else
printf("accept failed %s\n", strerror(errno));
}
while(1)
{
memset(s,0,sizeof(1024));
int rc = recv(client_st,s,sizeof(s),0);//recv是阻塞调用,若没有收到消息,则会挂起,此处接收的是来自客户端的套接字描符的数据
if(rc > 0)//接收了来自client的消息
{
printf("recv is %s\n",s);
memset(s,0,sizeof(s));
}
else
{
if(rc == 0)
{
printf("client socket closed \n");
}else
{
printf("recv failed %s\n", strerror(errno));
}
break;
}
}
close(client_st);//关闭client端socket
}
close(st);//关闭server端listen的socket
return 0;
}
client.c
//客户端
int main(void)
{
//分配套接口和初始化socket函数,成功返回一个客户端的套接字描述符
int st = socket(AF_INET,SOCK_STREAM,0);//初始化socket
//定义一个套接字地址的结构
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;//设置结构地址类型为TCP/IP地址
addr.sin_port = htons(8080);//指定一个端口号:8080,htons函数:将short类型从host字节类型到net类型的转化
addr.sin_addr.s_addr = inet_addr("192.168.1.103");//将字符串类型的IP地址转化为int,赋值给addr结构成员
//客户端只需要connect,不需要bind
//调用connect连接到结构addr指定的IP地址和端口号
//即:客户端调用connect与服务器端进行连接
if(connect(st,(struct sockaddr *) &addr,sizeof(addr)) == -1)
{
printf("connect failed %s\n",strerror(errno));
return 0;
}
//与服务器端已经建立了连接,接下来可以实现发送和接收操作了
char buf[1024];
memset(buf,0,sizeof(buf));
while(1)
{
read(STDIN_FILENO,buf,sizeof(buf));//从键盘中读取用户输入
if(send(st,buf,strlen(buf),0) == -1)//发送buf的数据
{
printf("send failed %s\n",strerror(errno));
return 0;
}
}
//发送和接收后,需要断开连接
close(st);//关闭socket
return EXIT_SUCCESS;
}
连接到server端的client的socket设置为非阻塞状态实例2:
函数实现:server端等待来自客户端的连接,若客户端连接但未发送消息,将不再等待客户端的消息,