设想一个情景一个服务器在等待任意客户的到来,并接受客服传来的数据,然后对数据处理后再返还给客户。然后客户离开。服务器继续等待。
但是如果某一时刻服务器响应了一个客户A的服务请求,但是由于一些原因,该客户A确不能立刻将数据传递给服务器。于是导致服务器阻塞在等待
该客户的A数据到来。如果这个时候另一个客户B 携带着数据来请求响应。但是服务器正阻塞在等待客户A 的数据到来上,于是客户B 只能在队列中等待,
知道服务器处理完A 的请求。
当然这不是我们期望的,如果B 是携带着数据的,那么应该立刻响应B的请求。而不是阻塞在一个不知道什么时候才能传来数据的客户A上
就像排队买东西,如果轮到你时,突然你的手机响了。那么你需要接电话。但是后面的人不会一直等你打完电话,所以这个时候,你会站在旁边,而后面的
人会依次去买东西,等你打完电话。你就可以去买东西了。
下面的这个例子就是一个简单的 多客服-服务器 模型。两个客户分别传送一个数据给服务器,然后在接收服务器处理后的数据后便结束。服务器的设置,
只是简单的循环等待一个客服的链接请求,当一个客户发出请求后,服务器会便会响应,将传来的数据加一后,再会送给客户。直到这个客户的请求处理完了
服务器才会去等待下一个请求,如果在服务器处理一个客户的请求过程中别的客户也发出了请求,那么就会在队列中等待。
下面是服务器的 代码 server1.c
int main(void){
int socketfd;
if((socketfd=socket(AF_INET,SOCK_STREAM,0))==-1){
perror("create a new socket error");
exit(1);
}
struct sockaddr_in server_address;
server_address.sin_family=AF_INET;
server_address.sin_port=htons(8000); //随便选的大于1024的端口号
if(inet_pton(AF_INET,"127.0.0.1",&server_address.sin_addr.s_addr)==-1){ //使用本地回路
perror("inet_pton error");
exit(1);
}
if(bind(socketfd,(struct sockaddr *)&server_address,
sizeof(server_address))==-1){ //把套接字绑定到上面设置的地址
perror("bind error");
exit(1);
}
if(listen(socketfd,5)==-1){
perror("listen error");
exit(1);
}
int client;
int data;
while(1){ //循环等待客户的请求
if((client=accept(socketfd,NULL,NULL))==-1){
perror("accepr error");
exit(1);
}
read(client,&data,1); //当有一个请求后,读客户发送的数据
data++;
write(client,&data,1); //处理后返回给客户
}
exit(0);
}
然后是两个客户1的代码。
client1.c
int main(void){
int server_fd;
server_fd=socket(AF_INET,SOCK_STREAM,0);
if(server_fd==-1){
perror("create a new socket error");
exit(1);
}
//指定要链接的服务器的地址
struct sockaddr_in server_address;
server_address.sin_family=AF_INET;
server_address.sin_port=htons(8000);
if(inet_pton(AF_INET,"127.0.0.1",&server_address.sin_addr.s_addr)==-1){ //将字符串转化为ip地址格式
perror("inet_pton error error");
exit(1);
}
if(connect(server_fd,(struct sockaddr *)&server_address,
sizeof(server_address))==-1){ //链接请求
perror("connect error");
exit(1);
}
int data='a';
sleep(5);
printf("after 5 secondes\n"); //服务器响应后,休眠五秒后,该客户才发送数据
write(server_fd,&data,1);
printf("\nclient 1 send data(%c) to server\n",data);
read(server_fd,&data,1);
printf("client 1 read data(%c) from server\n",data);
close(server_fd);
exit(0);
}
客户2 的代码
client2.c
int main(void){
int server_fd;
server_fd=socket(AF_INET,SOCK_STREAM,0);
if(server_fd==-1){
perror("create a new socket error");
exit(1);
}
struct sockaddr_in server_address;
server_address.sin_family=AF_INET;
server_address.sin_port=htons(8000);
if(inet_pton(AF_INET,"127.0.0.1",&server_address.sin_addr.s_addr)==-1){
perror("inet_pton error error");
exit(1);
}
if(connect(server_fd,(struct sockaddr *)&server_address,
sizeof(server_address))==-1){
perror("connect error");
exit(1);
}
int data='A';
write(server_fd,&data,1);
printf("\nclient 2 send data(%c) to server\n",data);
read(server_fd,&data,1);
printf("client2 read data(%c) from server\n",data);
close(server_fd);
exit(0);
}
如上面注释的。客户1 运行后会向服务器请求响应。服务器响应该请求后,客户1 却在五秒后才发送数据。
客服2 运行后向服务器请求响应。 但是客户2 会立刻发送数据。
下面让两个客户以不用是顺序运行,来看下输出结果。先让客户2 先运行,而客户1后运行
feng@ubuntu:~/learn_linux_c_second/chapter_14$ ./client2 & ./client1 &
[2] 4629
[3] 4630
feng@ubuntu:~/learn_linux_c_second/chapter_14$
client 2 send data(A) to server
client2 read data(B) from server
after 5 secondes
client 1 send data(a) to server
client 1 read data(b) from server
客户2运行后立刻得到响应并获得处理后的数据。客户1得到响应五秒后才会发送数据然后获得处理后的数据。这里并没有什么问题
现在让客户1 先运行,客户2后运行。再来看看输出情况
feng@ubuntu:~/learn_linux_c_second/chapter_14$ ./client1 & ./client2 &
[2] 4798
[3] 4799
feng@ubuntu:~/learn_linux_c_second/chapter_14$
client 2 send data(A) to server
after 5 secondes
client2 read data(B) from server
client 1 send data(a) to server
client 1 read data(b) from server
这个时候问题出现了。客户1 先运行,所以他先得到服务器的响应。但是五秒后才会发送数据给服务器。所以服务器就会阻塞在等待客户1的数据到来上
而此时,客户2 却已经发送数据给服务器了。但是正如输出所示,客户2确不得不等到 五秒后 才能得到服务器的请求。
这就是我们需要解决的问题。我们需要让服务器应该立刻服务携带数据的客户2 而不是阻塞在客户1中使 客户2 即使有数据也无法得到处理。
I/O多路转换提供了一种解决这种问题的方法。
#include
int select(int maxfd,fd_set *restrict readfds,
fd_set *restrict writefds,fd_set *restrict exceptfds,
struct timeval *restrict tvptr);
maxfd参数可以为FD_SETSIZE,他说明了最大的描述符。
select调用会在 0到maxfdp-1 范围内的文件描述符中测试,readfds文件描述符集中设置的位中是否有可读的,writefds文件描述符集中设置的位中是否有
可写的,exceptfds文件描述符集中设置的位中是否有异常状态。(如果某个描述符集为NULL,表示不关心该描述符集)
tvptr指示了select调用愿意等待的时间。为NULL是表示永远等待。为零表示不等待。
select成功返回时,会返回准备好的描述符数,并且在各个描述符集中设置准备好的描述符所在的位。
可以用FD_ISSET(int fd, fd_set *fdset) 在select返回后测试某个描述符在 某个描述符集(读集,写集,异常集)中是否被置位,被置位则说明该描述符
现在可以非阻塞的可读或可写或异常(根据所在描述符集而定)。
void FD_SET(int fd,fd_set *fdset); 设置一个描述符集中关心的描述符。select调用时会检查该位是否可读或可写或有异常
void FD_CLR(int fd,fd_set *fdset); 清除一个描述符集中的某个位。select调用时不会检查该位是否可读或可写或有异常
void FD_ZERO(fd_set *fdset); 初始化一个描述符集,将该描述符集中的所有位置零
那么select函数就提供了一个解决上面的多客服—服务器的问题。我们可以把 服务器套接字添加到 测试是否可读的描述符集中。然后调用select等待可读描述符集中是否有描述符可读。
这时如果一个客户发送请求,会导致服务器的套接可读,然后select调用返回,然后使用FD_ISSET测试如果真是服务器套接字在可读描述符中被设置,
那么就调用 accept获得 为客户服务的套接字,但是我们并不是使用该套接字去读客户的数据,而是将它添加到 测试可读描述集中。
然后回到select中继续等待可读描述符集中是否有数据可读。
如果这时候select调用返回,那么再测试是否是服务器套接字可读,如果是那说明又一个客服发送了请求,那么依旧向上面说的一样处理。
否则,说明之前添加的 一个为客户服务的套接字 可读,也就是说客户发送数据过来了。这个时候我们再调用read去读数据,那么read绝不会再阻塞。
下面是修改后的服务器代码 server1.c
int main(void){
int server_fd;
if((server_fd=socket(AF_INET,SOCK_STREAM,0))==-1){
perror("create a new socket error");
exit(1);
}
//设置服务器地址和端口
struct sockaddr_in server_address;
server_address.sin_family=AF_INET;
server_address.sin_port=htons(8000);
if(inet_pton(AF_INET,"127.0.0.1",&server_address.sin_addr.s_addr)==-1){
perror("inet_pton error");
exit(1);
}
if(bind(server_fd,(struct sockaddr *)&server_address,
sizeof(server_address))==-1){ //绑定套接字到设置的服务器地址上
perror("bind error");
exit(1);
}
if(listen(server_fd,5)==-1){ //创建监听队列
perror("listen error");
exit(1);
}
//设置 可读描述符集,并添加服务器套接字
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(server_fd,&readfds);
fd_set testfds;
int client;
int i;
int nreads;
while(1){
testfds=readfds; //使用临时变量,防止后面添加的 为客户服务的套接字对本次测试产生影响
if(select(FD_SETSIZE,&testfds,(fd_set*)0,(fd_set*)0,(struct timeval *)0)==-1){
perror("select error");
exit(1);
}
for(i=0;i
if(FD_ISSET(i,&testfds)){
if(i==server_fd){ //如果该描述符可读并且是 服务器套接字那么说明一个 客户请求到来
client=accept(server_fd,NULL,NULL); //获得 为客服服务的套接字
if(client==-1){
perror("accept error");
exit(1);
}
FD_SET(client,&readfds); //不去读客户数据而是把 为客户服务的套接字添加到 可读描述符集中
/*
如果不是服务器套接字可读,那么就是之前添加的 为客户服务的套接字可读,
也就是说有客户发送数据过来
*/
}else{
ioctl(i,FIONREAD,&nreads);
if(nreads==0){ //发现 读数据返回0 表示客户离开
close(i);
FD_CLR(i,&readfds); //将该描述符移除 测试可读描述符集 ,应为该客户已经离开了
}else{ //如果数据不为0,说明是真是数据,那么就处理数据。
int data;
read(i,&data,1);
data++;
write(i,&data,1);
}
}
}
}
}
exit(0);
}
现在再来看看 客户1 和客户2 不同的运行顺序产生的结果
feng@ubuntu:~/learn_linux_c_second/chapter_14$ ./client2 & ./client1 &
[2] 5230
[3] 5231
feng@ubuntu:~/learn_linux_c_second/chapter_14$
client 2 send data(A) to server
client2 read data(B) from server
after 5 secondes
client 1 send data(a) to server
client 1 read data(b) from server
[2]- Done ./client2
[3]+ Done ./client1
feng@ubuntu:~/learn_linux_c_second/chapter_14$ ./client1 & ./client2 &
[2] 5233
[3] 5234
feng@ubuntu:~/learn_linux_c_second/chapter_14$
client 2 send data(A) to server
client2 read data(B) from server
after 5 secondes
client 1 send data(a) to server
client 1 read data(b) from server
[2]- Done ./client1
[3]+ Done ./client2
现在无论是 哪一个客户先运行。服务器不会阻塞在一个客户上,而另一个客户有数据却得不到处理。因为使用了select后,服务器不在关心是谁在请求,
他只是响应请求,然后把 与客户联系的套接字添加到 测试可读描述符上。然后继续等待 测试可读描述符集 中的一个描述符可读,如果是与客户联系的
描述符可读,那么说明又数据,那么服务器就能立刻得到数据,而不会有任何阻塞
所以无论客户如何到达,如何发送数据。服务器都不理会。他只关系,是否有请求,是否有数据,有就做相应处理。所以无论如何,一个客户绝不会应为另一个
客户的数据还未发送而导致自己的数据得不到处理。
最后需要注意的是:我们在上面使用了一个临时中间变量 testfds来测试可读描述符集中 是否有可读描述符。然后在循环中也是用 testfds 来测试
应为,如果直接使用readfds 会使本次的 添加与服务器联系的套接字操作对 后面的测试产生影响。
因为 FD_SET(client,&readfds); 把 与客户联系的套接字 添加到 测试可读描述符集中后,如果使用readfds 来进行后续的测试,服务器会以为被设置的
与 服务器联系的套接字 是由select 调用返回设置的,服务器便会认为有数据可读,而 实际上 可能根本无数据传送过来。
阅读(2540) | 评论(0) | 转发(0) |