这里我们来探讨一下在网络编程过程中,有关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

TCP程序中发送和接收数据的更多相关文章

  1. udp网络程序-发送、接收数据

    1. udp网络程序-发送数据 创建一个基于udp的网络程序流程很简单,具体步骤如下: 创建客户端套接字 发送/接收数据 关闭套接字 代码如下: #coding=utf-8from socket im ...

  2. [Socket网络编程]由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)没有提供地址,发送或接收数据的请求没有被接受。

    原文地址:http://blog.sina.com.cn/s/blog_70bf579801017ylu.html,记录在此方便查看 解决办法: MSDN的说明: Close 方法可关闭远程主机连接, ...

  3. C#程序中从数据库取数据时需注意数据类型之间的对应,int16\int32\int64

    private void btn2_Click(object sender, RoutedEventArgs e)         {             using (SqlConnection ...

  4. 手把手教你Android手机与BLE终端通信--连接,发送和接收数据

    假设你还没有看上一篇 手把手教你Android手机与BLE终端通信--搜索,你就先看看吧,由于这一篇要接着讲搜索到蓝牙后的连接.和连接后的发送和接收数据. 评论里有非常多人问假设一条信息特别长,怎么不 ...

  5. 微信小程序中发送模版消息注意事项

    在微信小程序中发送模版消息 参考微信公众平台Api文档地址:https://mp.weixin.qq.com/debug/wxadoc/dev/api/notice.html#模版消息管理 此参考地址 ...

  6. Netty——高级发送和接收数据handler处理器

    netty发送和接收数据handler处理器 主要是继承 SimpleChannelInboundHandler 和 ChannelInboundHandlerAdapter 一般用netty来发送和 ...

  7. MO拆分计划行程序中写入PRODUCTIONORDERS表数据出现重复导致报错(BUG)20180502

    错误提示:ORA-00001: 违反唯一约束条件 (ABPPMGR.C0248833319_6192)ORA-06512: 在 "STG.FP_MO_SPLIT", line 19 ...

  8. netty发送和接收数据handler处理器

    netty发送和接收数据handler处理器 主要是继承 SimpleChannelInboundHandler 和 ChannelInboundHandlerAdapter 一般用netty来发送和 ...

  9. socket 错误之:OSError: [WinError 10057] 由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)没有提供地址,发送或接收数据的请求没有被接受。

    出错的代码 #server端 import socket import struct sk=socket.socket() sk.bind(('127.0.0.1',8080)) sk.listen( ...

随机推荐

  1. A Simple Game

    A Simple Game Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/65535 K (Java/Others) Total ...

  2. 网络基础四 DNS DHCP 路由 FTP

    第1章 网络基础 1.1 IP地址分类 IP地址的类别-按IP地址数值范围划分 IP地址的类别-按IP地址用途分类 IP地址的类别-按网络通信方式划分 1.2 局域网上网原理过程 DHCP原理过程详情 ...

  3. 详解tomcat的连接数与线程池

    前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章 详解Tomcat配置文件server.xm ...

  4. Javascript 内核Bug

    Javascript 内核Bug: js 执行(9.9+19.8)加法运算 等于 29.700000000000003) <html> <head> <title> ...

  5. JSP 学习笔记

    JSP 全名为Java Server Pages,中文名叫java 服务器页面,它是在传统的 HTML 页面中插入 JAVA 代码片段和 JSP 标签形成的文件. 上一篇 Servlet 中只是讲解了 ...

  6. 《Linux命令行与shell脚本编程大全》 第七章理解Linux文件权限

    Linux沿用了Unix文件权限的方法,允许用户和组根据每个文件和目录的安全性设置来访问文件. 用户权限通过创建用户时分配的用户ID(UID)来跟踪的.每个用户有唯一的ID,但是登录时用的不是UID, ...

  7. jQuery学习笔记之Ajax用法详解

    这篇文章主要介绍了jQuery学习笔记之Ajax用法,结合实例形式较为详细的分析总结了jQuery中ajax的相关使用技巧,包括ajax请求.载入.处理.传递等,需要的朋友可以参考下 本文实例讲述了j ...

  8. 菜鸟谈谈C#中的构造函数和析构函数

    本节说明对象的创建.初始化和销毁过程.本节介绍下列主题: l         类构造函数 l         结构构造函数 l         析构函数 类构造函数 本节将讨论三种类构造函数: 类构造 ...

  9. java变量与内存深入了解

    ========================================================================================= 在我看来,学习jav ...

  10. 【Java入门提高篇】Day1 抽象类

    基础部分内容差不多讲解完了,今天开始进入Java提高篇部分,这部分内容会比之前的内容复杂很多,希望大家做好心理准备,看不懂的部分可以多看两遍,仍不理解的部分那一定是我讲的不够生动,记得留言提醒我. 好 ...