zoukankan      html  css  js  c++  java
  • nginx&http 第五章 https non-fd 读写检测

    EPOLL的LT/ET 模式下的读写

      从一个非阻塞的socket上调用recv/send函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
    从字面上看, 意思是:EAGAIN: 再试一次,EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block,error输出: Resource temporarily unavailable,这个错误表示资源暂时不够,能read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种情况,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同时errno设置为EAGAIN。所以,对于阻塞socket,read/write返回-1代表网络出错了。但对于非阻塞socket,read/write返回-1不一定网络真的出错了。可能是Resource temporarily unavailable。这时应该继续尝试,直到Resource available。

    综上,对于non-blocking的socket,正确的读写操作为:
    读:忽略掉errno = EAGAIN的错误,下次继续读
    写:忽略掉errno = EAGAIN的错误,下次继续写

    epoll 的LT和ET二者的差异在于

    level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;

    edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。

    epoll的ET模式下,正确的读写方式:
    读:只要可读,就一直读,直到返回0,或者ret = -1, errno = EAGAIN
    写:只要可写,就一直写,直到数据发送完,或者ret = -1, errno = EAGAIN

    static int do_raw_read(int fd, void *buf, int *bufsize, void *userdata)
    {
        ssize_t len = 0, res;
        int is_first_recv = 0;
        while(len < *bufsize) {
            res = recv(fd, buf + len, *bufsize - len, 0);
            if (res < 0) {
                do_warn("errno:%d, %s
    ", cross_sock_errno, strerror(errno));
                if (sock_errno == EINTR)
                    continue;
                else if (sock_errno == EAGAIN)
                    break;
                else
                    return -1;
            }
            if (res == 0) {
                if (is_first_recv == 0)
                    return 1;       //EOF of fd
                else
                    break;
            }
            len += res;
            is_first_recv = 1;
        }
        *bufsize = len;
        return 0;
    }

       

    int nwrite, data_size = strlen(buf);
     n = data_size;
     while (n > 0) {
         nwrite = write(fd, buf + data_size - n, n);
         if (nwrite < n) {
             if(nwrite == -1 && errno != EINTR) 
                          continue;
             if (nwrite == -1 && errno != EAGAIN) {
                do_debug("errno:%d, %s
    ", sock_errno, strerror(errno));
             }
       do_warn("errno:%d, %s
    ", sock_errno, strerror(errno));    
             break;
         }
         n -= nwrite;
    }

    socket的accept,accept 要考虑 2 个问题
    (1) 阻塞模式 accept 存在的问题
    考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,
    如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单
    纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。

    解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的
    实现会在内核中处理该事件,并不会将该事件通知给epoll,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。

    (2)ET模式下accept存在的问题
    考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理
    一个连接,导致TCP就绪队列中剩下的连接都得不到处理。

    解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept
    返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

     do { 
      /*
                针对非阻塞I/O执行的系统调用则总是立即返回,而不管事件足否已经发生。如果事件没有眭即发生,这些系统调用就
            返回—1.和出错的情况一样。此时我们必须根据errno来区分这两种情况。对accept、send和recv而言,事件未发牛时errno
            通常被设置成EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期待阻塞”):对conncct而言,errno则被
            设置成EINPROGRESS(意为“在处理中")。
              */
            s = accept(lc->fd, (struct sockaddr *) sa, &socklen);
            if (s == (socket_t) -1) {
                err = socket_errno;
                /* 如果要去一次性读取所有的accept信息,当读取完毕后,通过这里返回。所有的accept事件都读取完毕 */
                if (err == NGX_EAGAIN) { //如果event{}开启multi_accept,则在accept完该listen ip:port对应的ip和端口连接后,会通过这里返回
                    return;
                }
    
               if (err == NGX_EMFILE || err == NGX_ENFILE) {
                        do_warn("");// Too many descriptors are in use by this process.
                        return;
                }
                if (err == NGX_ECONNABORTED || err= EPROTO) {
                   continue;
                }
                return;
            }
            process_new_fd(s);
        } while (flag); //一次性读取所有当前的accept,直到accept返回NGX_EAGAIN,然后退出
    }

    使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理?????????????????

        开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN,把socket加入epoll,在epoll的
    驱动下写数据,全部数据发送完毕后,再移出epoll。这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。

    int main(){
         
         if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
             do_debug("sockfd
    ");
             exit(1);
         }
         setnonblocking(listenfd);
         bzero(&local, sizeof(local));
         local.sin_family = AF_INET;
         local.sin_addr.s_addr = htonl(INADDR_ANY);;
         local.sin_port = htons(PORT);
         if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) {
             do_debug("bind
    ");
             exit(1);
         }
         listen(listenfd, 20);
     
         epfd = epoll_create(MAX_EVENTS);
         if (epfd == -1) {
             do_debug("epoll_create");
             exit(EXIT_FAILURE);
         }
     
         ev.events = EPOLLIN;
         ev.data.fd = listenfd;
         if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {
             do_debug("epoll_ctl: listen_sock");
             exit(EXIT_FAILURE);
         }
     
         for (;;) {
             nfds = epoll_wait(epfd, events, MAX_EVENTS, timer); //timer为-1表示无限等待
             if (nfds == -1) {
                 do_debug("epoll_pwait");
            
    if (errno == NGX_EINTR) {
            }
                 exit(EXIT_FAILURE);
             }
          if (0 == nfds){
    }
    for (i = 0; i < nfds; ++i) { fd = events[i].data.fd; if (fd == listenfd) { while ((sock_fd = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) { setnonblocking(sock_fd); ev.events = EPOLLIN | EPOLLET; ev.data.fd = sock_fd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ev) == -1) { do_debug("epoll_ctl: add"); exit(EXIT_FAILURE); } } if (sock_fd == -1) { if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) do_debug("accept"); } continue; }
            if (events[i].events & (EPOLLERR|EPOLLHUP)) {
             
                       process------
    }
    if (events[i].events & EPOLLIN) { n = 0; while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { n += nread; } if (nread == -1 && errno != EAGAIN) { do_debug("read error"); } ev.data.fd = fd; ev.events = events[i].events | EPOLLOUT; if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) { } } if (events[i].events & EPOLLOUT) { sprintf_n(buf, buf_len, "HTTP/1.1 200 OK Content-Length: %d Hello World"); int nwrite, data_size = strlen(buf); n = data_size; while (n > 0) { nwrite = write(fd, buf + data_size - n, n); if (nwrite < n) { if (nwrite == -1 && errno != EAGAIN) { do_debug("write error"); } break; } n -= nwrite; } close(fd); } } } return 0; }

       调用connect方法向上游服务器发起TCP连接,作为非阻塞套接字,connect方法可能立刻返回连接建立成功,也可能告诉用户继续等待上游服务器的响应对connect连接是否建立.

      针对非阻塞I/O执行的系统调用则总是立即返回,而不管事件足否已经发生。如果事件没有眭即发生,这些系统调用就返回—1.和出错的情况一样。此时我们必须根据errno来区分这两种情况。对accept、send和recv而言,事件未发牛时errno通常被设置成EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期待阻塞”):对conncct而言,errno则被
    设置成EINPROGRESS(意为“在处理中")。connect的时候返回成功后使用的sock就是socket创建的sock,这和服务器端accept成功返回一个新的sock不一样.

    rc = connect(s, pc->sockaddr, pc->socklen); //connect返回值可以<linux高性能服务器开发> 9.5节
        if (rc == -1) {
            err = socket_errno;
            if (err != NGX_EINPROGRESS)
            {
                if (err == NGX_ECONNREFUSED
                    /*
                     * Linux returns EAGAIN instead of ECONNREFUSED
                     * for unix sockets if listen queue is full
                     */
                    || err == EAGAIN
                    || err == ECONNRESET
                    || err == ENETDOWN
                    || err == ENETUNREACH
                    || err == EHOSTDOWN
                    || err == EHOSTUNREACH)
                {
                    do_err();
    
                } else {
                    do_warn();
                }
                return ;
            }
        }
    static int do_in_progress(xx *clidata)
    {
    
        clidata->in_progress = 0;
    
        if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len) == 0 
            && error == 0) {
            /* success connecting */
            do_process_con_server();
        } else {
            /* failed connecting */
        }
        return 1;
    
    }

    connect 链接成功后:fd 应该是可读同时可写。

    ssl连接建立--ssl握手

    SSL协议是基于TCP、位于应用层、创数层之间,提供数据加密、用户验证和保证数据完整性的一种网络协议;

    SSL/TLS 加密方式:

    对称加密和非对称加密结合;

     加密算法一般分为两种: '对称加密' 和 '非对称加密'。
     '对称加密': 也叫'密钥加密',就是指加密和解密使用的是相同的密钥。
     '非对称加密': 也叫'公钥加密',就是指加密和解密使用的是不同的密钥。
    do_ssl_init( )
    {
        SSL_library_init();
        SSL_load_error_strings();
        OpenSSL_add_all_algorithms();
    
    }
    
    do_ssl_create(ssl_t *ssl)
    {
        ssl->ctx = SSL_CTX_new(SSLv23_method());
    }
    
    //server端需要初始化证书与私钥
    string cert = "server.pem", key = "server.pem";
    r = SSL_CTX_use_certificate_file(g_sslCtx, cert.c_str(), SSL_FILETYPE_PEM);
    r = SSL_CTX_use_PrivateKey_file(g_sslCtx, key.c_str(), SSL_FILETYPE_PEM);
    r = SSL_CTX_check_private_key(g_sslCtx);
    
    // 使用已建立连接的socket初始化ssl
    sc->connection = SSL_new(ssl->ctx);
    SSL_set_fd(sc->connection, c->fd)
    
    if (flags & NGX_SSL_CLIENT) {//客户端
            SSL_set_connect_state(sc->connection);
        } else {//服务器端
            SSL_set_accept_state(sc->connection);
    }
    
    //epoll_wait后,如果SSL相关的socket有读写事件需要处理则进行SSL握手,直到握手完成
    int r = SSL_do_handshake(sc->connection_);
    if (r == 1) { // 若返回值为1,则SSL握手已完成
      process_add_epoll_fd();
      return;
    }
    int err = SSL_get_error(sc->connection, r););
    if (err == SSL_ERROR_WANT_WRITE) { //SSL需要在非阻塞socket可写时写入数据
        c->events_ |= EPOLLIN; //等待socket可读
        c->events_ &= ~EPOLLOUT; //暂时不关注socket可写状态
        c->write->ready = 0;
        c->read->handler = ssl_handshake_handler;
        c->write->handler = ssl_handshake_handler;
    
        if (ngx_handle_read_event(c->read, 0, NGX_FUNC_LINE) != NGX_OK) {
            return NGX_ERROR;
        }
    
        if (ngx_handle_write_event(c->write, 0, NGX_FUNC_LINE) != NGX_OK) {
            return NGX_ERROR;
        }
    
        return NGX_AGAIN; //需要继续握手
    
    } else if (err == SSL_ERROR_WANT_READ) { //SSL需要在非阻塞socket可读时读入数据
      
        c->events_ |= EPOLLOUT; 
          c->events_ &= ~EPOLLIN;
        c->read->ready = 0;
        c->read->handler = ssl_handshake_handler;
        c->write->handler = ssl_handshake_handler;
    
        if (ngx_handle_read_event(c->read, 0, NGX_FUNC_LINE) != NGX_OK) {
            return NGX_ERROR;
         }
    
         if (ngx_handle_write_event(c->write, 0, NGX_FUNC_LINE) != NGX_OK) {
             return NGX_ERROR;
         }
          return NGX_AGAIN; //需要继续握手
    } else { //错误
      ERR_print_errors(errBio);
    }
    
    5. 握手完成后,进行SSL数据的读写
    SSL_write(ssl->connection, data, size);
    SSL_read(ssl->connection, data, size);
    
    以ngx 为例:
    accept new fd 后 :
    ngx_http_init_connection 中调用
        rev->handler = ngx_http_ssl_handshake;
    tcp 数据到服务端---ssl开始:
        服务器处理数据---ngx_http_ssl_handshake----> rc = ngx_ssl_handshake(c);;ssl握手
    如果握手完成:
    //ssl单向认证四次握手完成后执行该handler
                    c->ssl->handler = ngx_http_ssl_handshake_handler;
    如果握手失败:设置fd 回调函数--- c->read->handler = ngx_ssl_handshake_handler;
    
    // TLS单向认证 协议握手过程参考http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html 
    //tls单向认证四次握手过程,都会调用该函数处理,返回NGX_AGAIN表示握手还没有完成,需要再次进行后续握手过程
    ngx_int_t
    ngx_ssl_handshake(ngx_connection_t *c)
    {
        int        n, sslerr;
        ngx_err_t  err;
    
        ngx_ssl_clear_error(c->log);
    
        //这里会试着握手
        n = SSL_do_handshake(c->ssl->connection); //改函数内部会调用ngx_http_ssl_alpn_select执行
    
        //0x80:SSLv2  0x16:SSLv3/TLSv1 
        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n);
    
        if (n == 1) { //握手完成
            if (ngx_handle_read_event(c->read, 0, NGX_FUNC_LINE) != NGX_OK) {
                return NGX_ERROR;
            }
            if (ngx_handle_write_event(c->write, 0, NGX_FUNC_LINE) != NGX_OK) {
                return NGX_ERROR;
            }
    
            return NGX_OK;//握手完成
        }
    
        sslerr = SSL_get_error(c->ssl->connection, n);
        
        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_get_error: %d", sslerr);
        //这里应该再重新接收一次和NGINX一样,等待下一次循环(epoll)再进行,同时设置读写句柄,以便下次读取的时候直接进行握手
        //单向认证四次握手过程还没有完成,需要继续握手
        if (sslerr == SSL_ERROR_WANT_READ) {  //# define SSL_ERROR_WANT_READ             2
            c->read->ready = 0;
            c->read->handler = ngx_ssl_handshake_handler;
            c->write->handler = ngx_ssl_handshake_handler;
    
            if (ngx_handle_read_event(c->read, 0, NGX_FUNC_LINE) != NGX_OK) {
                return NGX_ERROR;
            }
    
            if (ngx_handle_write_event(c->write, 0, NGX_FUNC_LINE) != NGX_OK) {
                return NGX_ERROR;
            }
    
            return NGX_AGAIN;//需要继续握手
        }
    
        if (sslerr == SSL_ERROR_WANT_WRITE) {
            c->write->ready = 0;
            c->read->handler = ngx_ssl_handshake_handler;
            c->write->handler = ngx_ssl_handshake_handler;
    
            if (ngx_handle_read_event(c->read, 0, NGX_FUNC_LINE) != NGX_OK) {
                return NGX_ERROR;
            }
    
            if (ngx_handle_write_event(c->write, 0, NGX_FUNC_LINE) != NGX_OK) {
                return NGX_ERROR;
            }
    
            return NGX_AGAIN; //需要继续握手
        }
    
    
        return NGX_ERROR; //握手失败
    }
     
  • 相关阅读:
    学习WEB基础知识(2)
    HTMLform表单的学习
    osg渲染到纹理的代码,把读入的节点当成纹理渲染到一个正方形上
    提取旋转矩阵
    osg选取
    osg,由eye,center,up生成的左乘,右手坐标系的矩阵
    相机沿着场景旋转
    贝塞尔曲线递归
    贝赛尔曲线,四点控制
    得到相交的三角面片的三个顶点坐标
  • 原文地址:https://www.cnblogs.com/codestack/p/12393030.html
Copyright © 2011-2022 走看看