欢迎交流 QQ 2431173627 微信 ccc17862701790
存在问题
设计思路
工作过程
第一步
第二步
第三步
特点分析
程序实例
用socket基本的一套api已经可以构建一个简单的tcp服务器了
思路就是 加一个while循环 在循环里面服务器程序用socket bind listen accept recv send 一条龙
具体可以看我写的这一篇博客 socket网络编程基础篇-------如何写一个简单的TCP服务器
但是这样简单的tcp服务器在只有单个客户端请求的时候是可以应付的,但是在有多个客户端请求的时候是存在很大问题的
以下面的场景作为例子
----->进入while循环
--->服务器用socket创建监听套接字(比如该套接字的文件描述符号为4)
-->调用listen函数监听该套接字
-->新来一个客户端调用connect请求连接
--->服务器调用accpet函数接受客户端请求并创建了针对这个客户端的数据传输套接字(比如文件描述符号为5)
--->在这个套接字5上调用read函数读客户端发过来的数据
-->新来一个客户端调用connect请求连接
--->服务器调用accpet函数接受客户端请求并创建了针对这个客户端的数据传输套接字(比如文件描述符号为6)
--->在这个套接字6上调用read函数读客户端发过来的数据
-->新来一个客户端调用connect请求连接
--->服务器调用accpet函数接受客户端请求并创建了针对这个客户端的数据传输套接字(比如文件描述符号为7)
--->在这个套接字7上调用read函数读客户端发过来的数据
........(上述过程在服务器端用while循环 多个客户端请求的时候就不断延续上述过程)
上述的这样一个过程存在一个严重的问题 在多个客户端有连接请求或者读写请求的时候
当使用accpet函数的时候发现没有客户端连接时会阻塞,
同样当使用read/write函数的时候发现客户端没有数据传过来时也会阻塞
所以在上述多个客户端请求的过程中,
(1)假如在某个循环中调用read函数阻塞了,这个时候假如又来了一个客户端连接
由于现在服务器端程序卡在了read这里
就没办法进入下一次循环到accept那里 ,这样的环这个想建立连接的客户端就一直阻塞在那里
(2)假如在某一个循环调用accept阻塞了,服务端程序由于卡在accept这里不能往下执行到read函数
故也响应不了前面已经建立连接的客户端套接字发过来的数据,这样根本就不能处理多个客户端的服务请求,
导致客户端的请求大大延迟
那怎么办呢?所以就引入了多路io复用,
他有三种实现机制----select函数,poll函数,epoll函数 这里先介绍select
对于select函数工作过程的理解
select函数是怎么工作的呢?我是这么理解的
上面这个服务器客户端交互过程本质上是文件io函数(accept/read)对文件描述符(4,5,6,7)的操作过程.
出现问题的罪魁祸首就是,
accep/read函数"自作主张"就直接去连接/读取文件描述符对应的套接字,
丝毫不管对应文件描述符的套接字是否可以连接/读取
万一对应文件描述符的套接字没有连接/数据请求传输过来,这两个函数自然就会阻塞.
理想的情况应该是请一个"监控员" ,
在执行read/accept之前 先让这个监管员依次检查一番
看哪个文件描述符有连接过来了或者有数据传输过来了
这时候再调用accept/read函数就不会阻塞了
如果监管员这一轮查找 没看到哪个文件描述符有连接过来了或者有数据传输过来了
就在进入下一轮的检查 就不调用accept或者read
虽然在这个监管员工作也会耽搁一定的时间(select每次检查都是阻塞查询) 但是比起accept/read函数盲目的就上来读写一个文件描述符 然后卡在那大半天 效率明显来的高
select就起了这样一个"监管员"的作用,
select缺点:
(1)每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
(2)单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,
可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低
(3)select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能
(4)对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,
每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。
这会浪费很多CPU时间。如果能给套接字注册某个回调函数,
当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
(5)后面会讲到 其实select还是同步阻塞的模型
在select设定的时间timeval内,进程会阻塞在select那条语句上
此时内核在遍历select所监管的套接字上
在此期间只要有一个套接字就就绪了(客户端connect过来 或者客户端send数据过来了 )
select就会返回停止阻塞 然后调用read /accept同步读写或者建立连接
或者在内核遍历期间没有套接字就绪 select会超时返回
然后通过while进入下一轮的select遍历
select优点:
(1)从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,
甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。
但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。
用户可以注册多个socket,然后不断地调用select读取被激活的socket,
即可达到在同一个线程内同时处理多个IO请求的目的。
而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
(2)目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
#include
#include //for souockaddr_in
#include
#include
#include
#include
#include
//for select
#include
#include
#include
#include
#include //for bzero
#include
#define BUFF_SIZE 1024
#define backlog 7
#define ser_port 8000
#define CLI_NUM 3
int client_fds[CLI_NUM];
int main(int agrc,char **argv)
{
int ser_souck_fd;
int i;
char input_message[BUFF_SIZE];
char resv_message[BUFF_SIZE];
struct sockaddr_in ser_addr;
ser_addr.sin_family= AF_INET; //IPV4
ser_addr.sin_port = htons(ser_port);
ser_addr.sin_addr.s_addr = INADDR_ANY; //指定的是所有地址
//creat socket
if( (ser_souck_fd = socket(AF_INET,SOCK_STREAM,0)) < 0 )
{
perror("creat failure");
return -1;
}
//bind soucket
if(bind(ser_souck_fd, (const struct sockaddr *)&ser_addr,sizeof(ser_addr)) < 0)
{
perror("bind failure");
return -1;
}
//listen
if(listen(ser_souck_fd, backlog) < 0)
{
perror("listen failure");
return -1;
}
//fd_set
fd_set ser_fdset;
int max_fd=1;
struct timeval mytime;
printf("wait for client connnect!\n");
while(1)
{
mytime.tv_sec=27;
mytime.tv_usec=0;
FD_ZERO(&ser_fdset);
//add standard input
FD_SET(0,&ser_fdset);
if(max_fd < 0)
{
max_fd=0;
}
//add serverce
FD_SET(ser_souck_fd,&ser_fdset);
if(max_fd < ser_souck_fd)
{
max_fd = ser_souck_fd;
}
//add client
for(i=0;i 0)
{
int flags=-1;
//一个客户端到来分配一个fd,CLI_NUM=3,则最多只能有三个客户端,超过4以后跳出for循环,flags重新被赋值为-1
for(i=0;i= 0)
{
printf("new user client[%d] add sucessfully!\n",flags);
}
else //flags=-1
{
char full_message[]="the client is full!can't join!\n";
bzero(input_message,BUFF_SIZE);
strncpy(input_message, full_message,100);
send(client_sock_fd, input_message, BUFF_SIZE, 0);
}
}
}
}
//deal with the message
for(i=0; i 0)
{
printf("message form client[%d]:%s\n", i, resv_message);
}
else if(byte_num < 0)
{
printf("rescessed error!");
}
//某个客户端退出
else //cancel fdset and set fd=0
{
printf("clien[%d] exit!\n",i);
FD_CLR(client_fds[i], &ser_fdset);
client_fds[i] = 0;
// printf("clien[%d] exit!\n",i);
continue; //这里如果用break的话一个客户端退出会造成服务器也退出。
}
}
}
}
}
return 0;
}