TCP程序中发送和接收数据

时间:2021-07-06 06:23:59

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

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

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

 CLIENT]$ cat /proc/sys/net/ipv4/tcp_wmem

CLIENT]$ cat /proc/sys/net/ipv4/tcp_rmem
        

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

阻塞的read和write的问题:

我们来看<unix网络编程> 中的read代码如下

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 > ) {
if ( (nread = read(fd, ptr, nleft)) < ) {
if (errno == EINTR)
nread = ; /* and call read() again */
else
return(-);
} else if (nread == )
break; /* EOF */ nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}

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

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 > ) {
if ( (nwritten = write(fd, ptr, nleft)) <= ) {
if (nwritten < && errno == EINTR)
nwritten = ; /* and call write() again */
else
return(-); /* error */
} nleft -= nwritten;
ptr += nwritten;
}
return(n);
}

非阻塞的read/write:

由上面阻塞模式的情况我们再分析一下非组塞:

还是以上的代码: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 > ) {
if ( (nread = read(fd, ptr, nleft)) < ) {
if (errno == EINTR)
nread = ; /* and call read() again */
       if (errno == EAGAIN)    //发生了这种异常我将它返回了,这里表示文件描述符还不可读,没有准备好,我就直接将其返回,最后IO复用select/poll/epoll就会再读取准备好的数据。
          return n - nleft;

else
return(-);
} else if (nread == ){

        //close(fd);
        break; /* EOF */
    }
nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}

1.  如果客户端发送了一点数据然后没有断开处于暂停状态的话。

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

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, ,
"recv: eof:%d, avail:%d, err:%d",
rev->pending_eof, rev->available, rev->kq_errno); if (rev->available == ) {
if (rev->pending_eof) {
rev->ready = ;
rev->eof = ; if (rev->kq_errno) {
rev->error = ;
ngx_set_socket_errno(rev->kq_errno); return ngx_connection_error(c, rev->kq_errno,
"kevent() reported about an closed connection");
} return ; } else {
rev->ready = ;
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, ,
"recv: eof:%d, avail:%d",
rev->pending_eof, rev->available); if (!rev->available && !rev->pending_eof) {
rev->ready = ;
return NGX_AGAIN;
}
} #endif do {
n = recv(c->fd, buf, size, ); ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, ,
"recv: fd:%d %z of %uz", c->fd, n, size); if (n == ) {
rev->ready = ;
rev->eof = ; #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 = ;
} #endif return ;
} if (n > ) { #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 <= ) {
if (!rev->pending_eof) {
rev->ready = ;
} rev->available = ;
} 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 = ;
} rev->available = ;
} return n;
} #endif if ((size_t) n < size
&& !(ngx_event_flags & NGX_USE_GREEDY_EVENT))
{
rev->ready = ;
} 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 = ; if (n == NGX_ERROR) {
rev->error = ;
} return n;
}

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

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

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