`
xiaoboss
  • 浏览: 650671 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

网络编程中Nagle算法和Delayed ACK的测试

阅读更多

 Nagle算法 的立意是良好的,避免网络中充塞小封包,提高网络的利用率。但是当Nagle算法遇到delayed ACK 悲剧就发生了。Delayed ACK的本意也是为了提高TCP性能,跟应答数据捎带上ACK,同时避免糊涂窗口综合症 ,也可以一个ack确认多个段来节省开销。
    悲剧发生在这种情况,假设一端发送数据并等待另一端应答,协议上分为头部和数据,发送的时候不幸地选择了write-write,然后再read,也就是先发送头部,再发送数据,最后等待应答。发送端的伪代码是这样

write(head);
write(body);
read(response);
 

接收端的处理代码类似这样:

read(request);
process(request);
write(response);

    这里假设head和body都比较小,当默认启用nagle算法,并且是第一次发送的时候,根据nagle算法,第一个段head可以立即发送,因为没有 等待确认的段;接收端收到head,但是包不完整,继续等待body达到并延迟ACK;发送端继续写入body,这时候nagle算法起作用了,因为 head还没有被ACK,所以body要延迟发送。这就造成了发送端和接收端都在等待对方发送数据的现象,发送端等待接收端ACK head以便继续发送body,而接收端在等待发送方发送body并延迟ACK,悲剧的无以言语。这种时候只有等待一端超时并发送数据才能继续往下走。

   正因为nagle算法和delayed ack的影响,再加上这种write-write-read的编程方式造成了很多网贴在讨论为什么自己写的网络程序性能那么差。然后很多人会在帖子里建议 禁用Nagle算法吧,设置TCP_NODELAY为true即可禁用nagle算法。但是这真的是解决问题的唯一办法和最好办法吗?

   其实问题不是出在nagle算法身上的,问题是出在write-write-read这种应用编程上。禁用nagle算法可以暂时解决问题,但是禁用 nagle算法也带来很大坏处,网络中充塞着小封包,网络的利用率上不去,在极端情况下,大量小封包导致网络拥塞甚至崩溃。因此,能不禁止还是不禁止的 好,后面我们会说下什么情况下才需要禁用nagle算法。对大多数应用来说,一般都是连续的请求——应答模型,有请求同时有应答,那么请求包的ACK其实 可以延迟到跟响应一起发送,在这种情况下,其实你只要避免write-write-read形式的调用就可以避免延迟现象,利用writev做聚集写或者 将head和body一起写,然后再read,变成write-read-write-read的形式来调用,就无需禁用nagle算法也可以做到不延 迟。

   writev是系统调用,在Java里是用到GatheringByteChannel .write(ByteBuffer[] srcs, int offset, int length)方法来做聚集写。这里可能还有一点值的提下,很多同学看java nio框架几乎都不用这个writev调用,这是有原因的。主要是因为Java的write本身对ByteBuffer有做临时缓存,而writev没有 做缓存,导致测试来看write反而比writev更高效,因此通常会更推荐用户将head和body放到同一个Buffer里来避免调用writev。

   下面我们将做个实际的代码测试来结束讨论。这个例子很简单,客户端发送一行数据到服务器,服务器简单地将这行数据返回。客户端发送的时候可以选择分两次 发,还是一次发送。分两次发就是write-write-read,一次发就是write-read-write-read,可以看看两种形式下延迟的差 异。注意,在windows上测试下面的代码,客户端和服务器必须分在两台机器上,似乎winsock对loopback连接的处理不一样。

    服务器源码:

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;


public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(8000));
        System.out.println("Server startup at 8000");
        for (;;) {
            Socket socket = serverSocket.accept();
            InputStream in = socket.getInputStream();
            OutputStream out = socket.getOutputStream();

            while (true) {
                try {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    String line = reader.readLine();
                    out.write((line + "\r\n").getBytes());
                }
                catch (Exception e) {
                    break;
                }
            }
        }
    }
}
 


服务端绑定到本地8000端口,并监听连接,连上来的时候就阻塞读取一行数据,并将数据返回给客户端。

客户端代码:

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;


public class Client {

    public static void main(String[] args) throws Exception {
        // 是否分开写head和body
        boolean writeSplit = false;
        String host = "localhost";
        if (args.length >= 1) {
            host = args[0];
        }
        if (args.length >= 2) {
            writeSplit = Boolean.valueOf(args[1]);
        }

        System.out.println("WriteSplit:" + writeSplit);

        Socket socket = new Socket();

        socket.connect(new InetSocketAddress(host, 8000));
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();

        BufferedReader reader = new BufferedReader(new InputStreamReader(in));

        String head = "hello ";
        String body = "world\r\n";
        for (int i = 0; i < 10; i++) {
            long label = System.currentTimeMillis();
            if (writeSplit) {
                out.write(head.getBytes());
                out.write(body.getBytes());
            }
            else {
                out.write((head + body).getBytes());
            }
            String line = reader.readLine();
            System.out.println("RTT:" + (System.currentTimeMillis() - label) + " ,receive:" + line);
        }
        in.close();
        out.close();
        socket.close();
    }

}

 客户端通过一个writeSplit变量来控制是否分开写head和body,如果为true,则先写head再写body,否则将head加上body一次写入。客户端的逻辑也很简单,连上服务器,发送一行,等待应答并打印RTT,循环10次最后关闭连接。

   首先,我们将writeSplit设置为true,也就是分两次写入一行,在我本机测试的结果,我的机器是xp

WriteSplit:true
RTT:8 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:39 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world

    可以看到,每次请求到应答的时间间隔都在40ms,除了第一次。linux的delayed ack是40ms,而不是原来以为的200ms。第一次立即ACK,似乎跟linux的quickack mode有关,这里我不是特别清楚,有比较清楚的同学请指教。

     接下来,我们还是将writeSplit设置为true,但是客户端禁用nagle算法,也就是客户端代码在connect之前加上一行:

 Socket socket = new Socket();
 socket.setTcpNoDelay(true);
 socket.connect(new InetSocketAddress(host, 8000));

     再跑下测试:

WriteSplit:true
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:1 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world

 这时候就正常多了,大部分RTT时间都在1毫秒以下。果然禁用Nagle算法可以解决延迟问题。
   如果我们不禁用nagle算法,而将writeSplit设置为false,也就是将head和body一次写入,再次运行测试(记的将setTcpNoDelay这行删除):

WriteSplit:false
RTT:7 ,receive:hello world
RTT:1 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world

    结果跟禁用nagle算法的效果类似。既然这样,我们还有什么理由一定要禁用nagle算法呢?通过我在xmemcached 的 压测中的测试,启用nagle算法在小数据的存取上甚至有一定的效率优势,memcached协议本身就是个连续的请求应答的模型。上面的测试如果在 windows上跑,会发现RTT最大会在200ms以上,可见winsock的delayed ack超时是200ms。

   最后一个问题,什么情况下才应该禁用nagle算法?当你的应用不是这种连续的请求——应答模型,而是需要实时地单向发送很多小数据的时候或者请求是有间 隔的,则应该禁用nagle算法来提高响应性。一个最明显是例子是telnet应用,你总是希望敲入一行数据后能立即发送给服务器,然后马上看到应答,而 不是说我要连续敲入很多命令或者等待200ms才能看到应答。

   上面是我对nagle算法和delayed ack的理解和测试,有错误的地方请不吝赐教。

分享到:
评论

相关推荐

    tcp-Nagle算法讲解

    上一节流程图中,一个字符发送和字符回显平均时间约为16ms,那么一秒钟少于62个字符,不会出现nagle算法起作用。 如果一秒钟敲击字符过多(超过62字符),或者网络RTT(报文一去一回)过大,那么就有可能启动nagle ...

    Linux关闭Nagle算法,实习Tcp每40ms发一帧

    Linux关闭Nagle算法,实习Tcp每40ms发一帧。

    tcp/ip协议之 Nagle算法 TCP_NODELAY和TCP_CORK.zip

    Nagle算法旨在解决小数据包发送的问题,通过合并多个小数据段来减少网络中的小包数量,从而提高带宽利用率。然而,这种合并策略也可能带来不可预测的延迟和吞吐量下降。 默认情况下,TCP链接会启用Nagle算法。当...

    TcpNoDelay

    但当Nagle算法和delayed ACK相遇时,可能会出现一种被称为“头部-主体”阻塞的情况。 例如,在上述实验模型中,发送端先发送头部数据,然后是主体数据,接着等待接收端的应答。接收端收到头部后,由于主体数据尚未...

    大讲解!一种基于Nagle算法的嵌入式TCP协议.docx

    Nagle算法是为了解决网络上小数据包过多导致的带宽浪费和网络阻塞问题。在传统的TCP协议中,每当应用程序产生新的数据,即使数据量很小,TCP也会立即发送,这可能导致频繁的网络交互,降低了网络带宽的利用率。Nagle...

    ARM的嵌入式以太网通信分析与实时性改进.pdf

    Nagle算法通过合并连续的小数据包来减少网络中的传输次数,而延迟ACK则是等待一段时间来确认多个数据包,以此减少ACK包的发送,进一步优化网络资源。 然而,在嵌入式系统中,尤其是单向数据传输的应用场景,Nagle...

    网络编程之高效编程

    在IT行业中,网络编程是构建分布式系统和互联网应用的基础,而高效的网络编程则是提升系统性能的关键。本主题“网络编程之高效编程”深入探讨了如何优化网络通信、TCP协议以及开发过程中的各种细节,旨在帮助开发者...

    Java网络编程--TCP网络编程(tcp缩略语)

    在实际编程中,我们主要关注应用层、传输层、网络层和数据链路层。 TCP提供了一种可靠的传输机制,通过滑动窗口机制控制流量并实现拥塞控制,同时使用Nagle算法优化小数据包的发送。此外,TCP还引入了重传机制,当...

    Linux网络编程_张斌.rar

    例如,如何正确处理EINTR错误以避免信号中断系统调用,如何使用非阻塞I/O和异步I/O提高程序的并发性能,以及如何利用套接字选项如TCP_NODELAY关闭Nagle算法以减少网络延迟。 书中的代码示例将帮助读者更好地理解...

    unix网络编程卷1:套接字联网api(第3版)

    这本书详细介绍了Unix系统中的网络编程,尤其是套接字(Sockets)API的使用,是IT专业人员深入理解和实践网络通信技术的重要参考资料。 本书的核心内容主要围绕以下几个方面展开: 1. **套接字概念**:套接字是...

    linux网络编程和code

    在这个压缩包“linux网络编程和code”中,很可能是包含了一些关于Linux网络编程的源代码示例,帮助学习者深入理解和实践相关概念。 一、套接字编程 套接字(Socket)是网络通信的基本接口,它是进程间通信的一种...

    网络编程实验报告

    7. **套接字选项和标志**:通过`setsockopt()`函数可以设置套接字选项,如禁用Nagle算法以提高网络效率。 8. **客户端编程**:客户端通常需要使用`connect()`函数建立到服务器的连接,然后发送请求并接收响应。 9....

    C#网络编程,socket 文件上下传

    8. **套接字选项**:Socket类还提供了各种选项,如NoDelay(禁用Nagle算法)和KeepAlive,以优化网络性能和连接保持。 9. **多线程或多任务处理**:在高并发场景下,可以使用线程池或者Task来处理多个客户端请求,...

    windows 网络编程技术 c++

    通过`setsockopt()`和`getsockopt()`函数可以设置和查询套接字的选项,如超时设置、禁用Nagle算法、指定接收和发送缓冲区大小等,以优化网络性能。 8. **网络安全与加密**: 在传输敏感数据时,可以使用SSL/TLS...

    嵌入Linux网络编程实例源代码

    5. **套接字选项与标志**:`setsockopt()`函数允许设置套接字的特定选项,如TCP_NODELAY(禁用Nagle算法)、SO_REUSEADDR(允许快速重启服务)等,这些选项能影响网络通信的性能和行为。 6. **异步编程**:Linux...

    SOCKET 网络编程 计算机网络 作业 客户端 服务器端 client svever

    在实际应用中,可能需要考虑Socket的性能优化,如缓冲区大小的调整、Nagle算法的启用或禁用、TCP_NODELAY选项的设置等。 10. **安全考虑**: 考虑到网络安全,Socket编程可能涉及SSL/TLS加密,确保数据在传输过程...

    UNIX网络编程 卷1 (第三版) 源代码

    《UNIX网络编程 卷1 (第三版) 源...通过深入学习和实践《UNIX网络编程 卷1 (第三版)》的源代码,开发者不仅可以掌握网络编程的基础知识,还能提升解决实际问题的能力,为构建高性能、高可用的网络应用打下坚实基础。

    UNIX网络编程随书源码

    控制消息如TCP的TCP_NODELAY(禁用Nagle算法)和UDP的IP_PKTINFO,可以影响数据传输性能。 4. **多路复用I/O**:UNIX提供了select、poll和epoll等多路复用I/O模型,用于同时监控多个套接字的读写事件,提高程序的...

    tcp ip sockets编程 c语言实现 第2版(中英文pdf各一份)

    8. **调试与测试**:介绍了一些网络程序的调试和测试方法,如使用Wireshark抓包工具分析网络流量,以及如何编写单元测试来验证代码的正确性。 总的来说,《TCP/IP Sockets编程:C语言实现》第二版是一本全面覆盖TCP...

Global site tag (gtag.js) - Google Analytics