一、网络编程四种IO模型。
1、什么是IO模型?
网络编程对于数据输出输入方法: 阻塞IO,非阻塞IO,多路复用,信号驱动。
2、这几种IO模型原理是如何?
1)阻塞IO: 一直阻塞等待某一个套接字的数据到达,如果有数据,则读取并返回,如果无数据,就会一直等待。
read(fd);
recv(fd);
accept(sockfd);
以上的例子,在过去,我们都会觉得是函数阻塞,例如read函数,accept函数阻塞,但是这样想是错误,因为真正阻塞的是套接字/文件描述符,并不是函数本身具有阻塞的属性。
因为fd是文件描述符 -> fd默认就是阻塞属性 -> read(fd) -> 阻塞
2)非阻塞IO:读取一个套接字数据,如果有数据,就会读取返回,如果没有数据,直接返回。
因为fd是文件描述符 -> fd默认就是阻塞属性 -> 添加非阻塞属性给fd -> read(fd) -> 非阻塞读取。
3)
4)
二、非阻塞IO模型。
1、如果要使用非阻塞IO,思路是如何?
1)先创建一个文件描述符/套接字 -> 默认都是阻塞属性。
2)设置非阻塞的属性给文件描述符 -> 这时候,文件描述符就是非阻塞的。
3)再调用read()/accept()/recv()去处理这个文件描述符时,就会非阻塞。
2、如何设置非阻塞属性给文件描述符? -> fcntl() -> man 2 fcntl
头文件:
#include <unistd.h>
#include <fcntl.h>
原型:
int fcntl(int fd, int cmd, ... /* arg */ );
参数:
fd: 需要设置属性的文件描述符
cmd:
F_GETFL (void) -> void代表第三个参数不需要填
Get the file access mode and the file status flags; arg is ignored.
//获取当前文件描述符的属性
F_SETFL (int) -> int代表后面那个参数要填
Set the file status flags to the value specified by arg.
O_NONBLOCK -> 非阻塞属性
返回值:
成功:
F_GETFL 返回文件的属性
F_SETFL 返回0
失败:
-1
例题1: 设置非阻塞属性给监听套接字,看看这个套接字还会不会阻塞等待客户端连接?
listen(sockfd);
accept(sockfd); -> 阻塞 -> 有人连接,accept()返回
没有人连接,就会一直等待
listen(sockfd);
设置非阻塞属性给sockfd
accept(sockfd); -> 非阻塞 -> 有人连接,accept()返回成功
-> 没有人连接,就会返回失败
#include "head.h"
int main(int argc,char *argv[])
{
//1. 创建一个TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
//2. 绑定IP地址,端口号
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[1]));
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//sockfd -> 默认是阻塞属性
//4. 添加非阻塞属性给sockfd
int state;
state = fcntl(sockfd,F_GETFL); //state就是sockfd当前的属性。
state |= O_NONBLOCK; //state就是新属性了
fcntl(sockfd,F_SETFL,state);
//sockfd -> 具有非阻塞属性
//5. 等待客户端连接
struct sockaddr_in cliaddr;
bzero(&cliaddr,len);
int connfd;
while(1)
{
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
if(connfd > 0) //有人连接,就会返回成功
{
printf("connfd = %d\n",connfd);
printf("new connection:%s\n",inet_ntoa(cliaddr.sin_addr));
break;
}
else{ //没有人连接,就会返回失败
printf("connect error!\n");
}
}
close(sockfd);
close(connfd);
return 0;
}
练习1: 使用非阻塞IO读取TCP套接字上数据。
-> 有数据就打印出来,没有返回失败。
参考p1目录。
三、多路复用。
1、什么是多路复用?
就是先将需要监听的文件描述符加入到一个集合中,然后在规定的时间内/无限等待去监听这个集合,如果规定的时间内/无限等待过程中有数据到达,则其他没有数据到达的文件描述符就会被自动剔除到集合之外,我们用户只需要观察集合中有哪个文件描述符存在就可以。
2、如何实现多路复用?
1)定义一个集合。 -> man 2 select
fd_set set;
2)删除、添加、清空,判断集合?
void FD_CLR(int fd, fd_set *set); -> 把集合中的fd删除掉
int FD_ISSET(int fd, fd_set *set); -> 判断fd是不是在集合中
void FD_SET(int fd, fd_set *set); -> 将fd添加到集合中
void FD_ZERO(fd_set *set); -> 清空集合中所有的文件描述符
fd: 需要处理的文件描述符。
set:集合的地址
返回值:
在集合中: 1
不在集合中:0
3)如何监听一个集合中? -> select() -> man 2 select
头文件:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
nfds:所有正在监测的套接字的最大值加1
readfds:读就绪文件描述符集合 -> 把所有需要监听的文件描述符加入到这个集合中即可。
writefds:写就绪文件描述符集合 -> NULL
exceptfds:异常就绪文件描述符集合 -> NULL
timeout:超时控制 -> NULL (代表无限等待)
返回值:
成功:就绪文件描述符总数
失败:-1
例题2: 实现客户端与服务器互相收发,不能实现线程,要求使用多路复用。
服务器端:
#include "head.h"
int main(int argc,char *argv[])
{
//1. 创建一个TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
//2. 绑定IP地址,端口号
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[1]));
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//4. 等待对方的连接
int connfd;
struct sockaddr_in cliaddr;
bzero(&cliaddr,len);
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
if(connfd > 0)
{
printf("connfd = %d\n",connfd);
printf("new connection:%s\n",inet_ntoa(cliaddr.sin_addr));
}
//5. 定义一个集合
fd_set set;
char buf[100];
int max_fd = connfd > STDIN_FILENO ? connfd : STDIN_FILENO;
//6. 不断监听这个集合,看看谁留下来了。
while(1)
{
FD_ZERO(&set);
FD_SET(connfd,&set);
FD_SET(STDIN_FILENO,&set);
//7. 一直阻塞等待
select(max_fd+1,&set,NULL,NULL,NULL);
//8. 只要集合中任意一个文件描述符有数据到达,那么select就会返回。
if(FD_ISSET(connfd,&set)) //connfd还在,说明我要收数据
{
bzero(buf,sizeof(buf));
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
if(FD_ISSET(STDIN_FILENO,&set)) //STDIN_FILENO还在,说明要发送数据给对方。
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
send(connfd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
}
close(connfd);
close(sockfd);
return 0;
}
客户端:
#include "head.h"
int main(int argc,char *argv[]) // ./jack 192.168.14.4 50000
{
//1. 创建TCP套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
//printf("sockfd = %d\n",sockfd);
//2. 准备rose的IP地址。
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //地址族
srvaddr.sin_port = htons(atoi(argv[2])); //端口号
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //设置好Rose的IP地址
//3. 发起连接
//connect调用之前: sockfd -> 待连接套接字
int ret = connect(sockfd,(struct sockaddr *)&srvaddr,len);
if(ret == 0)
{
printf("connect success!\n");
}
//connect调用之后: sockfd -> 已连接套接字
//5. 定义一个集合
fd_set set;
char buf[100];
int max_fd = sockfd > STDIN_FILENO ? sockfd : STDIN_FILENO;
//6. 不断监听这个集合,看看谁留下来了。
while(1)
{
FD_ZERO(&set);
FD_SET(sockfd,&set);
FD_SET(STDIN_FILENO,&set);
//7. 一直阻塞等待
select(max_fd+1,&set,NULL,NULL,NULL);
//8. 只要集合中任意一个文件描述符有数据到达,那么select就会返回。
if(FD_ISSET(sockfd,&set)) //sockfd还在,说明我要收数据
{
bzero(buf,sizeof(buf));
recv(sockfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
if(FD_ISSET(STDIN_FILENO,&set)) //STDIN_FILENO还在,说明要发送数据给对方。
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
send(sockfd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
}
//5. 回收TCP套接字的资源
close(sockfd);
return 0;
}