8.并发服务器 以及 与之对应的客户端三 select函数

8.并发服务器 以及 与之对应的客户端3 select函数

    之前代码仍然存在一些比较重要的问题,当我们客户端进程阻塞于从标准输入获取数据的时候n = read(connfd, buf, 100)。 如果出现了意外,比如说我们手动kill掉服务器对客户端的服务进程,此时服务器就会向客户端接口发送fin字节,表示服务器连接已经关闭.其意思就是服务器不会再向客户端发送数据,当然在这里进程已经被KILL,客户端阻塞在标准输入出,不会去对套接口的FIN字节做出处理,这就会引发不特定情况出现。我这里环境是fedora,当我KILL掉服务器进程后,再从客户端输入数据,仍然可以发送,并正确回显(这让我很意外,至今没找出原因)。当我再次输入时候,进程会终止(原因是第2次输入会接受SIGPIPE信号,此信号默认操作会终止进程)。

 

  为了避免以上所说的情况,我们需要有这样一个能力:如果一个或者多个I/O条件满足时,我们就要被通知到,也就是I/O复用,下面的select函数就可以帮助我们实现这种功能。(请先看代码后面的内容,如果有兴趣,再回头看代码,直接看代码比较累)。

 

[root@liumengli net]# cat chat_client.c
#include "/programe/net/head.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "sys/select.h"

#define MAXSIZE 100

int main(int argc, char ** argv) {
        int sockfd;
        struct sockaddr_in serv_socket;
        int maxfdpl;
        char send[MAXSIZE], recv[MAXSIZE];

        if(argc != 2) {
                printf("please input port");
                exit(1);
        }

        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        bzero(&serv_socket, sizeof(serv_socket));
        serv_socket.sin_family = AF_INET;
        serv_socket.sin_port = htons(atoi(argv[1]));
        inet_pton(AF_INET, "192.168.1.235", &serv_socket.sin_addr);
        connect(sockfd, (struct sockaddr *)&serv_socket, sizeof(serv_socket));
        for(;;) {
                fd_set rset;
                FD_ZERO(&rset);
                FD_SET(1, &rset);
                FD_SET(sockfd, &rset);
                maxfdpl = sockfd + 1;
                select(maxfdpl, &rset, NULL, NULL, NULL); //标记1
                if(FD_ISSET(sockfd, &rset)) {
                        int n = read(sockfd, recv, MAXSIZE);

                        if(!n) {
                                printf("server closed\n");
                                break;
                        }
                        recv[n] = '\0';
                        printf("get message from server:%s\n", recv);
                }
                if(FD_ISSET(1, &rset)) {
                        int n = read(1, send, MAXSIZE);
                        send[n] = '\0';
                        write(sockfd, send, n);
                }
        }
        close(sockfd);
        exit(0);
}

 

 

[root@liumengli net]# cat chat_server.c
#include "/programe/net/head.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "unistd.h"
#include "sys/wait.h"
#include "sys/select.h"
#include "sys/time.h"

#define MAXSIZE 100
#define LISTENQ 10

int main(int argc, char ** argv) {
        int listenfd, connfd;
        socklen_t client_len;
        struct sockaddr_in client_socket, serv_socket;
        char send[MAXSIZE + 1], recv[MAXSIZE + 1];


        listenfd = socket(AF_INET, SOCK_STREAM, 0);

        bzero(&serv_socket, sizeof(serv_socket));
        serv_socket.sin_family = AF_INET;
        serv_socket.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_socket.sin_port = htons(atoi(argv[1]));
        bind(listenfd, (struct sockaddr *)&serv_socket, sizeof(serv_socket));
        listen(listenfd, LISTENQ);
        client_len = sizeof(client_socket);
        connfd = accept(listenfd, (struct sockaddr *)&client_socket, &client_len);
        for(;;) {
                fd_set rest;
                FD_ZERO(&rest);
                FD_SET(1, &rest);
                FD_SET(connfd, &rest);
                int maxfdpl = 2;
                if(maxfdpl < connfd + 1)
                        maxfdpl = connfd + 1;
                int flag = select(maxfdpl, &rest, NULL, NULL, NULL); //标记2
                if(flag <= 0) {
                        printf("some error happend, sorry");
                } else {
                        if(FD_ISSET(1, &rest)) {
                                int n = read(1, send, MAXSIZE);
                                send[n] = '\0';
                                write(connfd, send, n);
                        }
                        if(FD_ISSET(connfd, &rest)) {
                                int n = read(connfd, recv, MAXSIZE);
                                if(n == 0) {
                                        printf("client closed\n");
                                        break;
                                }
                                recv[n] = '\0';
                                printf("get message:%s", recv);
                        }
                }

        }
}

 

这里我们关心的是代码

fd_set rest;

FD_ZERO(&rest);
FD_SET(1, &rest);
FD_SET(connfd, &rest);
int maxfdpl = 2;
if(maxfdpl < connfd + 1)
          maxfdpl = connfd + 1;

int flag = select(maxfdpl, &rest, NULL, NULL, NULL); //标记2

对于select函数,我们从代码的过程来解释比较简单。

首先我们定义了一个fd_set的对象rest(引用了面向对象的语句).FD_ZERO是一个宏操作,是将该对象清0(不清0会有不可预料的情况发生)。然后我们FD_SET(1, &rest)表示我们关心1号描述字,具体关心1号描述字什么,后面会看到。以此类推FD_SET(connfd, &rest)表示我们关心通信套接口connfd。暂时不看maxfdpl,后面就是 int flag = select(maxfdpl, &rest, NULL, NULL, NULL);之前我们说过rest关注1号和connfd号描述字,1号代表的是标准输入,connfd当然就是我们的通信套接口。select第2个参数的意思是关注的内容是否可读。总体解释下就是:我们现在关注标注输入文件是否可读或者通信套接口8.并发服务器 以及 与之对应的客户端三 select函数8.并发服务器 以及 与之对应的客户端三 select函数 是否可读。

 

所以上面的select语句执行后效果就是,当前进程挂起,一旦标准输入可以读,或者套接口可以读,则进程继续执行,也就是从select处返回。

 

再来总体解释下select函数,最后一个参数就是等待时间,如果时间到进程继续执行,返回值为0。倒数第2个参数,就是我们关心的描述字是否有异常情况处理,有则返回,无则挂起,返回大于0。倒数第3个参数是我们关系的描述字是否可写,可以则返回,不可以则挂起,返回大于0.倒数第4个参数则是我们关心的描述字是否可读,可以则返回,不可以则挂起,返回0,至于第一个参数,由于我们可能关心很多事情,操作系统为我们定义了1024个描述字,但我们常常用不了这么多,因此你必须告诉操作系统,你关心的描述字最大是多少,一提高效率,所以它就是你关系的描述字的最大值+1,至于+1则很好理解,因为如果你关心的是0号描述字(也就是标准输入),则你必须加1,因为数组长度是1.

 

对于套接口可读,只要满足下面条件就表示可读:

1.套接口接收缓冲区的数据大于接受缓冲区低潮限度当前值(默认是1,可以设置)。

2.链接的一方关闭(接受到了FIN的TCP链接)(这也就是为什么我们代码中要检测接受到的字符是否长度为0.个人觉得这应该属于异常情况,应该将该套接口的描述字加入到异常既倒数第2个描述字,可惜不是)。

3.如果套接口是监听套接口,则当完成链接数非0时候,就可读,因此我们可以将前面的accpet代码改成,

fd_set temp;
FD_ZERO(&temp);
FD_SET(listenfd, &temp);
int temp1 = listenfd + 1;
int flag = select(temp1, &temp, NULL, NULL, NULL);
if(flag <= 0) {
        printf("some error happen\n");
        exit(1);
}
printf("some connect to me\n");
connfd = accept(listenfd, (struct sockaddr *)&client_socket, &client_len);
printf("get connect\n");

可能有人觉得多次一举,当你要检测多个I/O接口时候,重要性就会凸显出来。代码运行时候可以看出2条打印语句很快被执行,accept处不会阻塞。当然不会,因为之前的select已经检测。

4.如果套接口有错误需要处理,此时select不阻塞,返回一个-1

 

对于网络套接口可写,满足下面情况即可:

1.套接口发送缓冲区可用字节数大于等于发送缓冲去低潮限度的当前值(默认是2048,也是可以设置的)。

2.链接写的一方关闭(和上面一样就是就是受到了FIN),此时再对此套接口写就会出现SIGPIPE信号。

3.套接口有错误需要处理。

 

再者和之前一样,进程在调用select被阻塞后,仍然会被信号唤醒(这是当然的,即使你没有捕获此信号),当然大多数信号默认操作会终止进程,但某些信号不会,比如你自己定义的,而且不终止进程的信号,此时也会从select返回,虽然信号提供了重启系统调用的操作,但不是所有的内核都对此有实现,因此在select返回时候,请务必检查返回值。再去检测你设置的fd_set.