TCP程序中发送和接收数据

这里咱们来探讨一下在网络编程过程当中,有关read/write 或者send/recv的使用细节。这里有关经常使用的阻塞/非阻塞的解释在网上有不少很好的例子,这里就不说了,还有errno ==EAGAIN 异常等等。首先咱们拿一个简单的实例代码看一下。编程

read/write面临的是什么问题:

字节流套接字上调用read或write的返回值可能比请求的数量少,这并非出错的状态,这种状况发生在内核中的用于套接字缓冲区的空间已经达到了极限,须要再次的调用read/write函数才能将剩余数据读出或写入。那么这里能够看到是内核缓冲区到达极限,那么通常状况下是多大呢?服务器

 CLIENT]$ cat /proc/sys/net/ipv4/tcp_wmem
4096    16384    4194304
CLIENT]$ cat /proc/sys/net/ipv4/tcp_rmem
4096    87380    6291456

第一个数据表示最小,第二个表示默认状况下,第三个表示最大,单位是字节。若是read的缓冲区已经到达极限,那么一次read并不能读出本身想要的数据大小。那么更多的状况咱们并不知道对方发送的数据量是多大,咱们只有一个最大阀值。那么这时该怎样去控制read/write呢?网络

阻塞的read和write的问题:

咱们来看<unix网络编程> 中的read代码以下socket

ssize_t                     /* Read "n" bytes from a descriptor. */  
readn(int fd, void *vptr, size_t n)  //这里的n 是接收数据buffer的空间   真实状况下咱们确实不太清楚客户端到底它会发多少数据 通常是个阀值。
{  
    size_t  nleft;  
    ssize_t nread;  
    char    *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0) {  
        if ( (nread = read(fd, ptr, nleft)) < 0) {  
            if (errno == EINTR)  
                nread = 0;      /* and call read() again */  
            else  
                return(-1);  
        } else if (nread == 0)  
            break;              /* EOF */  
  
        nleft -= nread;  
        ptr   += nread;  
    }  
    return(n - nleft);      /* return >= 0 */  
}

咱们先看这个参数size_t n,由于多数状况下,咱们并不严格规定客户端到底一次要发多少数据,对于常规的服务器来讲都要有一个最大阀值,超过这个阀值就表示是异常数据。想像一下若是没有最大阀值,一个恶意的客户端向一个服务器发送一个超大的文件,那么这个服务器很快就会崩溃! 这里的n的大小其实跟咱们的业务相关了。文件服务器就除外了咱们不谈这种状况。咱们继续看 若此时文件描述符为阻塞模式时,那么当一个链接到达并开始发送一段数据后暂停发送数据(尚未断开),由于客户端并无断开,同时它发送的数据尚未到达阀值 那么势必在read处一直阻塞,那么若是是一个单线程服务器的话就不能处理其余请求了。write的话这种状况咱们通常都知道要发送数据的真实大小通常不发生这种状况。tcp

ssize_t                     /* Write "n" bytes to a descriptor. */  
writen(int fd, const void *vptr, size_t n)  //这里传入的n通常就是数据的实际大小   while循环会正常返回。
{  
    size_t      nleft;  
    ssize_t     nwritten;  
    const char  *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0) {  
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {  
            if (nwritten < 0 && errno == EINTR)  
                nwritten = 0;       /* and call write() again */  
            else  
                return(-1);         /* error */  
        }  
  
        nleft -= nwritten;  
        ptr   += nwritten;  
    }  
    return(n);  
}


非阻塞的read/write:

由上面阻塞模式的状况咱们再分析一下非组塞:ide

仍是以上的代码:readn来讲,若是是非阻塞,咱们仍是假定这里客户端发送了一点数据并无断开。函数

ssize_t                     /* Read "n" bytes from a descriptor. */  
readn(int fd, void *vptr, size_t n)  //这里的n 是接收数据buffer的空间   这种状况咱们确实不太清楚客户端到底它会发多少数据 通常是个阀值。
{  
    size_t  nleft;  
    ssize_t nread;  
    char    *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0) {  
        if ( (nread = read(fd, ptr, nleft)) < 0) {  
            if (errno == EINTR)  
                nread = 0;      /* and call read() again */
       if (errno == EAGAIN)    //发生了这种异常我将它返回了,这里表示文件描述符还不可读,没有准备好,我就直接将其返回,最后IO复用select/poll/epoll就会再读取准备好的数据。
          return n - nleft;
else return(-1); } else if (nread == 0){
        //close(fd);
        break; /* EOF */     } nleft -= nread; ptr += nread; } return(n - nleft); /* return >= 0 */ }

1.  若是客户端发送了一点数据而后没有断开处于暂停状态的话。spa

那么在调用read时就会出现EAGAIN的异常,这里当发生这种异常时表示文件描述符尚未准备好,那么我这里直接将其返回已经读到的size。这样就不会形成一直阻塞在这里其余链接没法处理的现象。.net

2. 若是客户端发送了一点数据而后马上断开链接了线程

好比咱们第一次read的时候读到了最后发来的数据,当再次读取时读到了EOF客户端断开了链接那咱们这个程序仍是有问题阿! 咱们这里看到跳出来while并返回了正确读到的数据  这时readn的返回是正确的,可是咱们有此次返回仍是不知道客户端断开了,虽然说咱们能够向上述代码加入close 可是咱们并不能有readn函数知道客户端主动断开链接。

对于2这种状况就是咱们在开发过程当中经常遇到的状况,这时咱们能够在readn中再加入时当的参数就能够解决,好比咱们传入的是一个包含文件描述符号的结构体,结构体中含有标志状态的字段,再read == 0时将字段赋予一个值。再readn以后再有这个结构体的某个标志知道已将链接断开后续再断开链接和删除其事件便可。下面找到了Nginx关于recv的使用代码:

ssize_t
ngx_unix_recv(ngx_connection_t *c, u_char *buf, size_t size)
{
    ssize_t       n;
    ngx_err_t     err;
    ngx_event_t  *rev;

    rev = c->read;

#if (NGX_HAVE_KQUEUE)

    if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
                       "recv: eof:%d, avail:%d, err:%d",
                       rev->pending_eof, rev->available, rev->kq_errno);

        if (rev->available == 0) {
            if (rev->pending_eof) {
                rev->ready = 0;
                rev->eof = 1;

                if (rev->kq_errno) {
                    rev->error = 1;
                    ngx_set_socket_errno(rev->kq_errno);

                    return ngx_connection_error(c, rev->kq_errno,
                               "kevent() reported about an closed connection");
                }

                return 0;

            } else {
                rev->ready = 0;
                return NGX_AGAIN;
            }
        }
    }

#endif

#if (NGX_HAVE_EPOLLRDHUP)

    if (ngx_event_flags & NGX_USE_EPOLL_EVENT) {
        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
                       "recv: eof:%d, avail:%d",
                       rev->pending_eof, rev->available);

        if (!rev->available && !rev->pending_eof) {
            rev->ready = 0;
            return NGX_AGAIN;
        }
    }

#endif

    do {
        n = recv(c->fd, buf, size, 0);

        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
                       "recv: fd:%d %z of %uz", c->fd, n, size);

        if (n == 0) {
            rev->ready = 0;
            rev->eof = 1;

#if (NGX_HAVE_KQUEUE)

            /*
             * on FreeBSD recv() may return 0 on closed socket
             * even if kqueue reported about available data
             */

            if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
                rev->available = 0;
            }

#endif

            return 0;
        }

        if (n > 0) {

#if (NGX_HAVE_KQUEUE)

            if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
                rev->available -= n;

                /*
                 * rev->available may be negative here because some additional
                 * bytes may be received between kevent() and recv()
                 */

                if (rev->available <= 0) {
                    if (!rev->pending_eof) {
                        rev->ready = 0;
                    }

                    rev->available = 0;
                }

                return n;
            }

#endif

#if (NGX_HAVE_EPOLLRDHUP)

            if ((ngx_event_flags & NGX_USE_EPOLL_EVENT)
                && ngx_use_epoll_rdhup)
            {
                if ((size_t) n < size) {
                    if (!rev->pending_eof) {
                        rev->ready = 0;
                    }

                    rev->available = 0;
                }

                return n;
            }

#endif

            if ((size_t) n < size
                && !(ngx_event_flags & NGX_USE_GREEDY_EVENT))
            {
                rev->ready = 0;
            }

            return n;
        }

        err = ngx_socket_errno;

        if (err == NGX_EAGAIN || err == NGX_EINTR) {
            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
                           "recv() not ready");
            n = NGX_AGAIN;

        } else {
            n = ngx_connection_error(c, err, "recv() failed");
            break;
        }

    } while (err == NGX_EINTR);

    rev->ready = 0;

    if (n == NGX_ERROR) {
        rev->error = 1;
    }

    return n;
}
View Code

其中咱们还要注意在writen的非阻塞中,若是第一次写入返回,当第二次写入时对方断了,再次写入时就会发生EPIPE异常

while (nleft > 0) {  
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {  
            if (nwritten < 0 && errno == EINTR)  
                nwritten = 0;       /* and call write() again */  
            else if(errno == EPIPE)
            {
                    return 0;// 这里我返回了0 由于最后一次发送数据并不保证对面已经收到了数据,这个数据到底有没有被正确接收在这里咱们没法得到。若是对方关闭了就直接形成异常
            }
            else if(errno == EAGAIN)
           { 
                    return n-left;
           }
            else  
                return(-1);         /* error */  
        }  
  
        nleft -= nwritten;  
        ptr   += nwritten;  
    }                      

其实write数据并不表明数据被对方成功接收了,只是往内核缓冲区写,若是写入成功write就返回了,因此就没法知道数据是否被收到,在一些严格要求的数据交互中经常使用应用层的确认机制。至于详细的消息接收和发送的内容推荐下列博客: http://blog.csdn.net/yusiguyuan/article/details/24111289   和 http://blog.csdn.net/yusiguyuan/article/details/24671351

相关文章
相关标签/搜索