1.为什么要有IO复用方法?
之前实现tcp服务器的并发是通过多线程或者多进程来实现的,它每增加一个客户端,就会创建一个进程或者线程去处理客户端的请求,当客户端的数量达到成千上万的时候,不可能通过创建成千上万的进程或者线程去处理客户端的请求,它的效率是非常低的,所以有了IO复用方法,来更好的解决上述问题
2.IO复用方法可以在单个线程或者单个进程内就可以做到同时处理成千上万个客户端的请求
3.当从键盘上输入数据的时候,就会立即打印出输入的数据,如果5秒内没有检测有数据的输入,就会打印出time out
4.代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <string.h>
#include <sys/time.h>
#define STDIN 0
int main()
{
int fd = STDIN;//标准输入 键盘
fd_set fdset;//集合
while(1)
{
FD_ZERO(&fdset);//将集合所有的位清空,并置为0
FD_SET(fd,&fdset);//将fd在集合中对应的位置为1
struct timeval tv = {5,0};//设置超时时间
int n = select(fd+1,&fdset,NULL,NULL,&tv);//返回值是需要处理的描述符的个数,fd+1是指从1下标开始关注,只关注fd+1位,后面的位都不关注了,因为集合很大,后面肯定都是0,只关注前面可能为1的位就可以了
if(n<0)
{
continue;
}
else if(n == 0)//超时了
{
printf("time out\n");
}
else
{
char buff[128] = {0};
if(FD_ISSET(fd,&fdset))//检测描述符上是否有数据
{
read(fd,buff,128);//将数据读到buff中
printf("read:%s\n",buff);//打印buff的数据
}
}
}
}
5.利用单线程处理多个客户端的请求,如果5秒内没有客户端发来的数据,就会打印time out
如果每次recv只接收一个字符,那么每次select都会发现该描述符上都还有数据,就会不断的recv,然后再一次select,再一次recv输出一个字符的数据,直到这个描述符上的所有数据都recv完成,所以每打印出一个字符,就对应着一次select
为什么一个客户端发送了nihao收到了5个ok,另一个客户端发送hello收到了一个ok呢?
答案:hello5个字符也对应着5个ok,只是在recv的时候当时只有一个ok,剩下4个ok才到达,现在4个ok在接收缓冲区中放着呢,当再发送数据的时候,recv就会将4个ok全部接收到
6.为什么客户端Ctrl+C或者end的时候服务器会直到客户端关闭了呢?
答案:无论是Ctrl+C还是end,select都认为它是一个写事件,就会提醒recv去数据,recv返回0,就认为客户端关闭了,recv返回0是唯一能判断客户端是否关闭的依据
7.服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <string.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#define MAX 10
int socket_init();
//将数组元素全部置为-1
void fds_init(int fds[])
{
for(int i = 0;i<MAX;++i)
{
fds[i] = -1;
}
}
//将fd添加到数组中
void fds_add(int fds[],int fd)
{
for(int i = 0;i<MAX;++i)
{
if(fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
//将fd从数组中删除
void fds_del(int fds[],int fd)
{
for(int i = 0;i<MAX;++i)
{
if(fds[i] == fd)
{
fds[i] = -1;
break;
}
}
}
int main()
{
int sockfd = socket_init();
assert(sockfd != -1);
int fds[10];
fds_init(fds);//空的
fds_add(fds,sockfd);
fd_set fdset;
while(1)
{
FD_ZERO(&fdset);
int maxfd = -1;
for(int i = 0;i<MAX;++i)
{
if(fds[i] == -1)
{
continue;
}
FD_SET(fds[i],&fdset);
if(maxfd<fds[i])
{
maxfd = fds[i];//最大描述符
}
}
struct timeval tv = {5,0};
int n = select(maxfd+1,&fdset,NULL,NULL,&tv);
if(n == -1)
{
continue;
}
else if(n == 0)
{
printf("time out\n");
}
else//有n个描述符上有数据
{
for(int i = 0;i<MAX;++i)
{
if(fds[i] == -1)//无效描述符
{
continue;
}
if(FD_ISSET(fds[i],&fdset))//判断fds[i]是否有数据,注意:有描述符不一定有数据
{
//区分是监听套接字还是连接套接字,为什么这样呢?因为他们接受数据的方法不一样
if(fds[i] == sockfd)//是监听套接字
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d\n",c);
fds_add(fds,c);//往数组中添加c
}
else//是连接套接字
{
char buff[128] = {0};
int num = recv(fds[i],buff,127,0);
if(num <= 0)//等于0表示客户端关闭了,小于0表示出错了
{
close(fds[i]);
fds_del(fds,fds[i]);//移除描述符
printf("client close\n");
}
else
{
printf("recv:%s\n",buff);
send(fds[i],"ok",2,0);
}
}
}
}
}
}
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res == -1)
{
return -1;
}
res = listen(sockfd,5);
if(res == -1)
{
return -1;
}
return sockfd;
}
8.客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字,最后一个参数一般都是0
assert(sockfd != -1);//断言是否创建成功
struct sockaddr_in saddr;//ipv4专用的地址结构
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);//这个6000指的是服务器的端口
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//如果服务器没有运行,会失败,或者断网了,也会失败
assert(res != -1);
//执行到这里说明connect成功了,三次握手已经完成了
while(1)
{
char buff[128] = {0};
printf("input\n");
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)//如果输入的是end就推出循环
{
break;
}
send(sockfd,buff,strlen(buff)-1,0);//把'\n'减掉了
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
}
close(sockfd);
}