zoukankan      html  css  js  c++  java
  • 网络 IO 工作机制

    ref: 深入分析 java web 技术内幕 2.3
    两台计算机之间进行数据的传输需要经过很多步骤。首先有相互沟通的意向,然后还要有沟通的通道:通过电话还是面对面交流(物理链路)。最后,两个人说话的步调一致,明白什么时候自己说话,什么时候对方说话(通信协议)。本文简单介绍通信协议以及如何完成数据的传输。

    TCP 状态转化

    首先看看如何建立和关闭 TCP 连接,建立连接&传输过程中状态的变化见下图:
    TCP状态转换

    说明:
    1、CLOSED:起始点,在超时或者连接关闭时进入此状态;
    2、LISTEN:Server 端在等待连接时的状态,Server端为此要调用 Socket、bind、listen 等函数,就能进入此状态。这被称为被动打开(等待客户端来连接);
    3、SYN-SENT:客户端发起连接,发送 SYN 给服务端。如果服务端不能连接,则直接进入 CLOSED 状态;
    4、SYC-RCVD:与 3 对应,服务器端接口客户端的 SYN 请求,服务端由 LISTEN 状态进入 SYN-RCVD 状态。同时服务器端要回应一个 ACK,发送一个 SYN 给客户端;
    另一种情况是,客户端在发起 SYN 的同时接收到服务器端的 SYN 请求,客户端会由 SYN-SEND 状态进入 SYN-RCVD 状态
    5、ESTABLISHED:服务端和客户端在完成 3 次握手之后进入状态,说明已经可以传输数据了。
    6、FIN-WAIT-1:主动关闭的一方,由状态5进入。具体动作是发送了 FIN 给对方;
    7、FIN-WAIT-2:主动关闭的一方,在接受到对方的 FIN ACK 之后,进入此状态。由此不能再接受对方的数据,但是能够向对方发送数据。
    8、CLOSE-WAIT:接收到 FIN 以后,被动关闭的一方进入此状态。具体动作是在接收到 FIN 之后,同时发送 ACK。
    9、LAST_ACK:被动关闭的一方,发起关闭请求,有8状态进入此状态。具体动作是发送 FIN 给对方,同时在接收到 ACK 时进入CLOSED 状态;
    10、CLOSING:两边同时发送关闭请求时,会由 FIN-WAIT-1 进入此状态。具体动作是接收到 FIN 请求,同时响应一个 ACK;
    11、TIME-WAIT:进入这个状态比较复杂,也是我们最常见的一个连接状态,有3个状态可以转化为此状态:

    • 由 FIN-WAIT-2 转换到 TIME-WAIT ,具体情况:在双方不同时发起 FIN 的情况下,主动关闭的一方在完成自身发起的关闭请求之后,接收到被关闭一方的 FIN 之后进入此状态。
    • 由CLOSING 进入,具体情况:在双方同时发起关闭,都做了 发送 FIN 请求,同时接受到了对 FIN 的ACK 之后,这是就由CLOSING 状态进入此状态。
    • 由 FIN-WAIT-1 状态进入,具体情况:同时接收到 FIN(对方) 和 ACK(本身发起的回应),它与 CLOSING 转换的区别在于本身发起的 FIN 的回应 ACK 先与对方对方的 FIN 请求到达,而由 CLOSING 转换则是 对方的 FIN 先于 ACK 到达。

    简单理解:

    • 无论是建立连接还是关闭,需要两边都发送请求,并且得到响应之后,连接才完成;
    • 只不过,连接建立的时候,响应侧的响应和请求是同一次发出,而关闭是,响应和请求是分开发出。

    除此之外,网络连接的状态也很重要,在排查问题的时候,网络连接处于什么状态,可以帮助我们判断问题到底在什么地方。

    影响网络传输的因素

    • 网络带宽:速度
    • 距离:时间
    • TCP 拥塞控制:我们知道 TCP 传输是一个“停-等-停-等”的协议,传输方和接受方需要步调一致,要达到步调一致就要通过拥塞控制来调节。
      TCP 在传输的时候会设立一个“窗口”(BDP,bandwidth delay product),这个窗口的大小是有 带宽 和 RTT(Round-Trip time,数据在两端来回的时间,也就是响应时间)决定。具体公式是 带宽(b/s)* RTT(s)。通过这个值可以得出理论上最优的 TCP 缓冲区大小。linux 2.4 已经可以自动的调整发送端的缓冲区的大小,而到 linux 2.6.7 时,接收端也可以自动调整了。

    JAVA Socket 的工作机制

    Socket 是套接字的意思,它没有一个具体的实体,描述计算机之间完成相互通信的一种抽象功能。可以把Socket理解成一种交通工具,用于应用程序之间数据的传输。大部分情况下我们使用的都是基于 TCP/IP 的流套接字,它是一种稳定的通信协议。

    网络中的主机A 的应用要能和网络中的主机B 上的应用程序进行通信,必须通过 Socket 建立连接,而建立 Socket 连接必须由底层 TCP/IP 来建立TCP连接。建立 TCP 连接需要底层 IP 来寻址网络中的主机。通过IP,我们可以找到目标主机,但是目标主机上可能有多个应用,如何和指定的应用进行通信呢?这就需要通过 TCP/UDP 的地址,也就是端口号来指定。这样我们就可以通过一个 Socket 示例来唯一代表一个主机上的应用的通信链路了。

    建立通信链路

    当客户端要和服务端通信时,客户端首先要创建一个 Socket 实例,操作系统将为这个 实例 分配一个没有被使用的本地端口号,并创建一个包含本地地址、远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中,直到连接关闭。在创建 Socket 实例的构造函数正确返回之前,将要进行 TCP 的3次握手协议,TCP 握手协议完成后,Socket 实例对象将会创建完成,否则将抛出 IOException 错误。

    与之对应的服务端将会创建一个 ServerSocket 实例,创建 ServerSocket 比较简单,只要指定的端口没有被占用,一般实例都会创建成功。同时操作系统也会为 ServerSocket 实例创建一个底层的数据结构,在这个数据结构中包含指定监听的端口号和包含监听地址的通配符,通常情况下都是“*”,表示监听所有的地址。之后调用 accept() 方法之后,将会进入阻塞状态,等待客户端的请求。

    当一个新的请求来时,将为这个链接创建一个新 的套接字数据结构,该套接字数据信息包含的地址和端口信息正是请求源地址和端口。这个新创建的数据结构将会被关联到 ServerSocket 实例的未完成的连接数据结构列表中。需要注意的是,这时服务端与之对应的 Socket 实例并没有完成创建,而要等到与客户端的3次握手完成后,这个服务端的 Socket 实例才会返回,并将这个 Socket 实例对应的数据结构从未完成列表移动到已完成列表中。所以与 ServerSocket 所关联的列表中每个数据结构都代表与一个客户端建立的 TCP 连接。

    数据传输

    传输数据是我们建立连接的目的,下面简要介绍如何通过 Socket 传输数据。
    **
    当连接建立成功后,服务端和客户端都拥有了一个 Socket 实例,每个 Socket 实例都会有一个 InputStream 和 OutputStream,并通过这两个对象来交换数据。同时,我们知道网络IO 都是通过 字节流传输的,当创建 Socket 对象时,操作系统将会为 InputStream 和 OutputStream 分别分配一定大小的缓冲区,数据的写入和读取都是通过这个缓冲区完成的。写入端将数据写到OutputStream 对应的 SendQ 队列中,当队列填满时,数据将会被转移到另一端的 InputStream 的 RecvQ 队列中,如果这时 RecvQ 已经满了,那么 OutputStream 的 write 方法将会被阻塞,直到 RecvQ 有足够的空间容纳 SendQ 的数据。**
    特别值得注意的是,这个缓冲区的大小以及写入端的速度和读取端的速度非常影响这个连接的数据传输效率,由于可能发生阻塞,所以网络IO和磁盘 IO 不同的是数据写入和读取还要有一个协调的过程,如果在两边同时传送数据可能会产生死锁。那么该如何避免这种情况,需要 NIO 来解决。

  • 相关阅读:
    POJ3421(质因数分解)
    POJ2236(并查集入门)
    POJ3111(最大化平均值)
    POJ2976(最大化平均值)
    POJ3104(二分搜索)
    POJ3258(最大化最小值)
    POJ2456(最大化最小值)
    POJ1063Cable master(二分搜索)
    Numpy模块(一)
    Django简介
  • 原文地址:https://www.cnblogs.com/paxing/p/10645010.html
Copyright © 2011-2022 走看看