学习了select之后,也有好一段时间了,但是一直没有提起写一篇关于select的博客,大概也是因为自己那会还没搞懂吧,这段时间在看《linux高性能服务器编程》时,又看到I/O复用对于select,poll,epoll的用法实例和比较,又从头看了一次之前写的代码,虽然是在老师的指导下写的,但是印象还是很深刻的,现在看来,当时一头雾水的写,顶多记得一个写代码的步骤,而并没有明白为什么和原理,甚至当初还因为文件描述符集绕了半天。
如果你只是想看看代码,github链接在这:https://github.com/NICK-DUAN/Three-U/tree/master/select_server
如果一味的讲解原理的话,我自己也会睡着,所以我选择从代码入手,原理其实都隐藏在代码里,一行行的看代码,看到疑惑或者不懂的地方再去看看书,搜搜资料,才会发现自己的想法究竟是贴合了大牛的思想还是根本很愚蠢,一味的看原理就像是“站在岸上学不会游泳”,而一味的敲代码那不就是一个码农了吗,有何意义。
下面是我的代码,我会从头到尾,详细解释代码,和对应的原理:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <time.h>
int fds[1024];
为什么要定义一个全局变量的fds数组?这是因为需要在select里面的文件描述符集中与之对应,将外界有效的文件描述符(套接字)set进select的文件描述符集中,相当于select的一份保存。这个解释也不是很好,往下看或许你就明白了
static void usage(const char* str)
{
printf("Please Enter# ");
printf("%s [local-ip]--[local-port]\n",str);
}
int startup(const char* _ip,const char* _port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(2);
}
AF_INET代表协议族,SOCK_STREAM表示以字节流传递数据,即代表使用TCP协议,最后一个参数代表协议类型,但因为前两个参数已经确定了一个协议,所以默认为0。
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(atoi(_port));
local.sin_addr.s_addr=inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(3);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(4);
}
listen的第二个参数代表最大完全连接的数量参数的值,但是最大的半连接的数量则由内核决定。
我自己的理解是一次只能同时处理backlog个连接,再多的话你先等着。
PS:最大完全连接数为:backlog+1,因为计算机的下标从0开始。backlog表示listen的第二个参数
return sock;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(1);
}
int listen_sock = startup(argv[1],argv[2]);
int i=1;
int maxfd=-1;
maxfd表示当前最大的有效socket,在遍历整个socket数组时,只需要遍历到这个位置就ok了。
后续的socket因为是无效的socket,所以不用进行判断
int nums=sizeof(fds)/sizeof(fds[0]);
for(i=1;i<nums;i++)
{
fds[i]=-1;
}
fds[0]=listen_sock;
将全局数组的所有元素全部初始化为-1,便于在后续判断中不出错,并且把listen_sock(监听套接字)放入数组的第一个位置中,因为监听套接字已经处理号,所以后续并不需要对监听套接字再做什么别的操作。
while(1)
{
struct timeval time={3,0};
fd_set readfds;//读 文件描述符集
FD_ZERO(&readfds);//将"读"文件描述符集中的所有文件描述符设为0
maxfd=-1;
更新文件描述符集的最大值,防止多出来的被无效访问。
例如:第一次while循环,maxfd=1000,此时需要遍历到1000,
第二次时,maxfd=10,表示10后面的socket全部无效,没有读写事件,若不重新设定的话,就浪费时间。
for(i=1;i<nums;i++)//循环检测fds中的文件描述符中的文件描述符,如果有效,则设置入"读"
{
if(fds[i]>0)//如果有效,则设置入"读"文件描述符集中
{
FD_SET(fds[i],&readfds);
if(maxfd<fds[i])//如果存在比maxfd大的文件描述符,则更新maxfd
{
maxfd=fds[i];
}
}
}
遍历所有的fds[i],每个fd[i]表示一个socket,因为我们已经把所有的文件描述符全部初始化为-1,所以如果当前的文件描述符大于0,则必定是监听套接字或者建立号的连接。
switch(select(maxfd+1,&readfds,NULL,NULL,&time))//用switch判断select的等待结果
select的第一个参数为最大有效的maxfd+1,原理也是因为计算机的下表从0开始,所以多一个。第二个参数是readfd读文件描述符集(我们只讨论读事件),第三个参数为写文件描述符集,第四个参数为错误文件描述符集,第五个参数表示等待时间,设置时间表示当超过等待时间后,会返回timeout表示等待时间已到,设置为NULL表示阻塞式,设置为0时,表示立即返回。
{
case -1:
perror("select");
break;
表示select出错
case 0:
printf("select timeout,waitting...\n");
break;
表示等待的time时间内没有一个有效的socket
default:
{
for(i=0;i<nums;i++)//从头到尾遍历fds
{
if(fds[i]<0)
{
continue;
}
小于0的,表示无效,继续遍历下一个,直到遍历到有效的文件描述符,才进行操作
if(i==0&&FD_ISSET(listen_sock,&readfds))
listen_sock就绪,tcp三次握手完成,此时可以开始进行连接
FD_ISSET表示判断第一个参数是否在第二个参数的集合中
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len);//接收到的文件描述符new_fd
if(new_fd<0)
{
perror("accept");
continue;
}
使用accept函数将得到一个连接的socket,再fds数组中找到一个可用的位置,因为有效的socket必大于0,所以原先初始化的数组中为-1的则表示可用的位置,插入此socket,等待下次循环时,进入对连接请求处理的模块
//运行到此处,说明已经有一个client连接
printf("client[%s]--[%d]# ",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
fflush(stdout);
int j=1;
for(j=1;j<nums;j++)
{
if(fds[j]==-1)//找到一个可用的文件描述符
{
break;
}
}
if(j==nums)
文件描述符集已满,没有可用的文件描述符,关闭new_fd,即关闭此client的链接
{
printf("fd_set full...");
fflush(stdout);
sleep(2);
printf("connect broken...\n");
close(new_fd);
}
到此处,有两种情况:
1.从for循环中跳出,表示找到一个可用的位置,也表示当前的服务器还有资源再处理一个连接的请求
2.或因为j==nums,表示资源全部分配,没有“余力”处理其他连接,拒绝接收此连接,不过这种已经返回,不会进入else逻辑。
else
{
fds[j]=new_fd;
将链接此client的文件描述符设置到全局的文件描述符集中,在下一次循环进入后,进行操作
}
}
else if(i>0&&FD_ISSET(fds[i],&readfds))
除listen_sock就绪外的其他文件描述符就绪
{
char buff[1024];
ssize_t s=read(fds[i],buff,sizeof(buff)-1);
从标准输入(键盘)接收数据
if(s>0)
{
buff[s]=0;
printf("client say# %s",buff);
printf("Send to client# ");
fflush(stdout);
ssize_t _s=read(0,buff,sizeof(buff)-1);
if(_s>0)
{
write(fds[i],buff,strlen(buff));
}
else{
}
}
else if(s==0)
s==0表示客户端断开连接
{
printf("client quit...\n");
FD_CLR(fds[i],&readfds);
close(fds[i]);
fds[i]=-1;
}
else
{
perror("read");
}
}else{
}
}
}
break;
}
}
}
至此,关于select的代码已经写完,建议读者在看这篇博客时自己了解过select的架构,因为我并没有很详细的分析select服务器,只是从代码层分析,希望在看完之后,自己动手敲一遍代码,然后对不熟悉的地方或查手册,或从网上找点针对性的博客看看。
我是分割线
上次忘记说在select中何种情况下算作可读,何种情况下算作可写:
下列情况下socket可读:
1,socket内核接收缓冲区中的字节数大于或等于其低水位标记SO_RCVLOWAT,此时可读;
2,socket通信的对方关闭,会返回0;
3,监听socket上有新的连接到来;
4,socket上有未处理的错误,此时可以用getsockopt来读取和清除该错误;
下列情况下socket剋可写:
1,socket内核发送缓冲区中的字节数大于或等于其低水位标记SO_SNDLOWAT,此时可写;
2,socket的写操作被关闭,对写操作被关闭的socket执行写操作将会返回SIGPIPE;
3,socket使用非阻塞connect连接成功或失败之后;
4,socket上有未处理的错误,此时可以用getsockopt来读取和清除该错误。
在select中还可以接收带外数据(即紧急数据),这个带外数据是TCP标志位中URG有效时的数据,而不是PSH有效时的数据,如果还是分不清,可以看看这篇博客:http://blog.youkuaiyun.com/sinat_36118270/article/details/73927628
并且,URG的带外数据有点奇怪,并不是想象中的一串数据,而是偏移量的最后一个数据,假如TCP报头中的URG标志位有效,则必然紧急指针的位置会有一个有效的地址偏移量,然后接收方根据紧急指针解读出带外数据,如下图的,往TCP的缓冲区中写入N个普通数据后,又紧接着写了三个带外数据“a,b,c”,系统将紧急指针指向带外数据的最后一个字节的下一位置,此时在接收端只有c被解读为带外数据,其他数据都被解释为普通数据,所以,不难发现,带外数据一次只能发一个。