一、在使用select时,我们需要了解linux的五种IO模型和TCP的11种状态:
1、阻塞IO:recv接收缓冲区有数据后,就会解除阻塞。
2、非阻塞IO:忙等待 fcntl(fd,F_SETFL,flag|O_NONBLOCK);内核中没有数据时,recv会返回-1,不会阻塞
3、IO复用(select和poll):一旦有一个文件描述符检测到有文件过来,select就返回,(阻塞提前到selete处)recv就可以直接从内核空间得到数据。
4、信号驱动IO:以信号方式通知应用进程,有数据到来(信号是异步处理一种方式)应用进程调用recv将数据从内核空间拉到用户空间--效率没有异步IO高
5、异步IO :aio_read没有数据到来,这个函数也会立刻返回,有数据到来,内核则将数据拷贝到应用层缓冲区,拷贝完成通过信号通知用户。
TCP的11种状态:
还有一种叫CLOSING状态,产生原因是双方同时关闭,客户端会处于FIN_WAIT_1状态,服务器端也处于FIN_WAIT_1状态,双方均在等待的状态就是CLOSING状态,收到对方ACK后,就会处于TIME_WAIT状态,
二、select也会阻塞,相比于阻塞IOselect优点在哪里?
当我们kill掉服务端的连接进程后,发现服务端处于FIN_WAIT2,不能立刻结束。
原因是客户端程序阻塞在了标准输入位置,没有机会调用close,因此导致服务端不能立刻结束。本质就是因为从键盘接收数据和从网络接收数据没有办法同时处理。这时用selete来进行管理,管理标准输入IO和套接口IO。
用select便可以管理多个IO,一旦其中一个IO或者多个IO检测到我们所感兴趣的时间,select函数返回,返回值是检测到的事件个数,并且返回那些IO发生了事件。这样用户可以遍历这些事件去处理这些事件。
其次,服务端使用多个进程处理多个客户端连接,能不能使用一个进程来处理?
三、select使用
参数含义:
nfds:读、写、异常集合中最大文件描述符值+1
readfds 可读的集合,输入输出参数
writefds 可写的集合,输入输出参数
exceptfds 异常集合,输入输出参数
timeout 超时时间结构体 填NULL,只有检测到某个事件才返回,填写超时时间,没有事件到来,超时时间到后就返回事件个数0,失败返回-1,输入输出参数。
FD_CLR:将文件描述符从集合中移除
FD_ISSET:判断fd是否在集合中
FD_SET:将fd添加到集合中
FD_ZERO:清空集合
四、代码
客户端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>//信号
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while (0)
//ssize_t 有符号整数
//size_t 无符号整数
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft = count;//剩余字节数
ssize_t nread;//已接收字节数
char *bufp = (char*) buf;
while(nleft>0)
{
if((nread = read(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信号中断
{
continue;
}
else
return -1;
}
else if(nread == 0)//对等方关闭
{
count = count - nleft;//已经读取的字节数
break;
}
else
{
bufp += nread;
nleft -= nread;
}
}
return count;
}
ssize_t writen(int fd,const void *buf,size_t count)
{
size_t nleft = count;//剩余字节数
ssize_t nwritten;//已接收字节数
char *bufp = (char*) buf;
while(nleft>0)
{
if((nwritten = write(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信号中断
{
continue;
}
else
return -1;
}
else if(nwritten == 0)//对等方关闭
{
continue;
}
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
return count;
}
//从套接口接收数据,但并不把数据从缓冲区清除
ssize_t recv_peek(int sockfd,void *buf,int len)
{
while(1)
{
int ret = recv(sockfd,buf,len,MSG_PEEK);
if((ret == -1) && (errno == EINTR))
continue;
printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
return ret;
}
}
//偷窥方案:
ssize_t readline(int sockfd,void *buf,size_t maxlen)//一行最大的字节数
{//只要遇到/n就返回
int ret;
int nread;
char *bufp = (char *)buf;
int nleft = maxlen;
while(1)
{
ret = recv_peek(sockfd,bufp,nleft);
if(ret<0)
{
return ret;
}
else if(ret == 0)//表示对方关闭了套接口
{
return ret;
}
nread = ret;
int i;
for(i =0; i<nread;i++)
{
if(bufp[i] == '\n')//如果找到了结束符就将数据读取出来
{
ret = readn(sockfd,bufp,i+1);//下标i,总共有i+1个字符
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//如果没有找到结束符,就读出来先缓存起来
if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
exit(EXIT_FAILURE);
nleft -= nread;//剩余的字节数
ret = readn(sockfd,bufp,nread);
if(ret != nread)//偷窥到的数据是可以全部读取出来的
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void echo_cli(int sock)
{
/* char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
writen(sock,sendbuf,strlen(sendbuf));//发送
int ret = readline(sock,recvbuf,sizeof(recvbuf));
if(ret == -1)
{
ERR_EXIT("readline");
}
else if(ret == 0)
{
printf("client_close\n");
break;
}
fputs(recvbuf,stdout);//打印
memset(recvbuf,0,sizeof(recvbuf));
memset(sendbuf,0,sizeof(sendbuf));
}
close(sock);
*/
//用select统一管理标准输入IO与套接口IO
fd_set rset;
FD_ZERO (&rset);
int nready;
int fd_stdin = fileno(stdin);
//标准输入的文件描述符,通过fileno获取,
//不能直接用STD_FILENO这个宏,
//因为不能确保标准输入不被重定向
//还有一个文件描述符为sock
int maxfd = fd_stdin?fd_stdin>sock:sock;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(1)
{
FD_SET(fd_stdin,&rset);
FD_SET(sock,&rset);
nready = select(maxfd+1,&rset,NULL,NULL,NULL);
if(nready == -1)
ERR_EXIT("select");
if(nready == 0)
continue;
if(FD_ISSET(sock,&rset))
{
int ret = readline(sock,recvbuf,sizeof(recvbuf));
if(ret == -1)
{
ERR_EXIT("readline");
}
else if(ret == 0)
{
printf("srv_close\n");
break;
}
fputs(recvbuf,stdout);//打印
memset(recvbuf,0,sizeof(recvbuf));
}
if(FD_ISSET(fd_stdin,&rset))
{
if(fgets(sendbuf,sizeof(sendbuf),stdin)==NULL)
break;
writen(sock,sendbuf,strlen(sendbuf));//发送
memset(sendbuf,0,sizeof(sendbuf));
}
}
close(sock);//显示关闭套接口
}
void handle_sigpipe(int sig)
{
printf("recv a sig = %d\n",sig);
}
int main(void)
{
/*
signal(SIGPIPE,handle_sigpipe);
*/
signal(SIGPIPE,SIG_IGN);//当服务端down掉,客户端发送数据后tcp协议栈会产生该信号
int sock;//被动套接字
if( (sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//指定服务器端地址
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("connect");
echo_cli(sock);
return 0;
}
服务端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>//信号
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while (0)
//ssize_t 有符号整数
//size_t 无符号整数
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft = count;//剩余字节数
ssize_t nread;//已接收字节数
char *bufp = (char*) buf;
while(nleft>0)
{
if((nread = read(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信号中断
{
continue;
}
else
return -1;
}
else if(nread == 0)//对等方关闭
{
count = count - nleft;//已经读取的字节数
break;
}
else
{
bufp += nread;
nleft -= nread;
}
}
return count;
}
ssize_t writen(int fd,const void *buf,size_t count)
{
size_t nleft = count;//剩余字节数
ssize_t nwritten;//已接收字节数
char *bufp = (char*) buf;
while(nleft>0)
{
if((nwritten = write(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信号中断
{
continue;
}
else
{
return -1;
}
}
else if(nwritten == 0)//对等方关闭
{
continue;
}
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
return count;
}
//从套接口接收数据,但并不把数据从缓冲区清除
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
while(1)
{
int ret = recv(sockfd,buf,len,MSG_PEEK);
if((ret == -1) && (errno == EINTR))
continue;
printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
return ret;
}
}
//偷窥方案:
ssize_t readline(int sockfd,void *buf,size_t maxline)//一行最大的字节数
{//只要遇到/n就返回
int ret;
int nread;
char *bufp = (char *)buf;
int nleft = maxline;
while(1)
{
ret = recv_peek(sockfd,bufp,nleft);
if(ret<0)
return ret;
else if(ret == 0)//表示对方关闭了套接口
return ret;
nread = ret;
int i;
for(i =0; i<nread;i++)
{
if(bufp[i] == '\n')//如果找到了结束符就将数据读取出来
{
ret = readn(sockfd,bufp,i+1);//下标i,总共有i+1个字符
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//如果没有找到结束符,就读出来先缓存起来
if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
exit(EXIT_FAILURE);
nleft -= nread;//剩余的字节数
ret = readn(sockfd,bufp,nread);
if(ret != nread)//偷窥到的数据是可以全部读取出来的
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void echo_srv(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));
int ret = readline(conn,recvbuf,1024);//按行接收
if(ret == -1)
{
ERR_EXIT("readline");
}
if(ret == 0)
{
printf("client_close\n");
break;
}
fputs(recvbuf,stdout);//打印
writen(conn,recvbuf,strlen(recvbuf));//回射-这里!!
}
}
int main(void)
{
signal(SIGCHLD,SIG_IGN);//处理僵尸进程
int listenfd;//被动套接字
if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
/* if( (listenfd = socket(PF_INET,SOCK_STREAM,0))<0);*///让内核自己选定协议
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示本机的任意地址
/*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
int on = 1;//开启地址重复利用
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsockopt");
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("bind");
if(listen(listenfd,SOMAXCONN)<0)//监听后变为被动套接字
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn;//主动套接字
/*
//父子进程可以共享文件描述符
pid_t pid;
while(1)
{
if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept");
printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
//也可以使用select实现并发服务器
pid = fork();//创建进程实现并发处理
if(pid == -1)
ERR_EXIT("frok");
if(pid == 0)
{//子进程不需要处理监听套接字
close(listenfd);
echo_srv(conn);
exit(EXIT_SUCCESS);//如果通信结束(客户端关闭)直接结束进程,否则子进程也会去accept
}
else
{//父进程不需要处理连接套接字
close(conn);
}
}
*/
//select单进程处理并发
int i;
//select中fd_set集合的限制FD_SETSIZE
int client[FD_SETSIZE];//缓存select返回的有时间到来的套接口
for(i = 0;i<FD_SETSIZE;i++)
{
client[i] = -1;//空闲
}
int nready;
/*文件描述符
0 标准输入
1 标准输出
2 标准错误*/
int maxfd = listenfd;//第3个套接字就是监听套接字,也是最大的套接字
fd_set rset;//读的集合
fd_set allset;//所有集合
FD_ZERO(&rset);
FD_ZERO(&allset);
//将监听套接口放到allset中
FD_SET(listenfd,&allset);
while(1)
{
rset = allset;
nready = select(maxfd+1,&rset,NULL,NULL,NULL);//写、异常、超时均不关心//超时时间为NULL不可能返回零
if(nready == -1)
{
if(errno == EINTR)
continue;
ERR_EXIT("select");
}
if(nready == 0)
continue;
if(FD_ISSET(listenfd,&rset))//是否是监听套接口产生事件
{//起初集合中只有一个监听套接口,不用循环
peerlen = sizeof(peeraddr);//一定要有初始值
//这里有个问题,就是虽然可以接收多个conn,但是多个conn会覆盖
conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);
if(conn == -1)
ERR_EXIT("accept");
for(i = 0;i<FD_SETSIZE;i++)
{
if(client[i]<0)//找到空闲位置将conn存储进去
{
client[i] = conn;
break;
}
}
if(i == FD_SETSIZE)
{
fprintf(stderr,"too many clients\n");
exit(EXIT_FAILURE);
}
printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
//获得了套接口conn,下次循环我们也要关心conn的可读事件
FD_SET(conn,&allset);//不断运行,添加到数组中的套接口就会越来越多
if(conn>maxfd)
maxfd = conn;
if(--nready <= 0)
continue;
}
for(i = 0;i<FD_SETSIZE;i++)
{
conn = client[i];//已连接套接口
if(conn == -1)
continue;
if(FD_ISSET(conn,&rset))//产生可读事件
{
char recvbuf[1024] = {0};
int ret = readline(conn,recvbuf,1024);//按行接收
if(ret == -1)
{
ERR_EXIT("readline");
}
if(ret == 0)
{
printf("client_close\n");
//如果对方关闭,则从集合中清除,不再关心它的可读事件
FD_CLR(conn,&allset);
client[i] = -1;
}
fputs(recvbuf,stdout);//打印
writen(conn,recvbuf,strlen(recvbuf));//回射-这里!!
if(--nready <= 0)//所有事件都处理完毕退出。
break;
}
}
}
return 0;
}
我们改造客户端的echo_cli函数使得客户端可以同时处理多个IO事件,改造服务端,使得服务端可以以一个进程处理多个客户端连接,最终