精华内容
下载资源
问答
  • 在分布式架构中,网络通信底层基础,没有网络,也就没有所谓的...可能大家已经忘记了网络通信的重要性,本篇文章会详细分析网络通信底层原理!! 1.1 理解通信的本质 如图1-1所示,当我们通过浏览器访问一个网址时

    在分布式架构中,网络通信是底层基础,没有网络,也就没有所谓的分布式架构。只有通过网络才能使得一大片机器互相协作,共同完成一件事情。

    同样,在大规模的系统架构中,应用吞吐量上不去、网络存在通信延迟、我们首先考虑的都是网络问题,因此网络的重要性不言而喻。

    作为现代化应用型程序员,要开发一个网络通信的应用,是非常简单的。不仅仅有成熟的api,还有非常方便的通信框架。

    可能大家已经忘记了网络通信的重要性,本篇文章会详细分析网络通信的底层原理!!

    1.1 理解通信的本质

    如图1-1所示,当我们通过浏览器访问一个网址时,一段时间后该网址会渲染出访问的内容,这个过程是怎么实现的呢?

    image-20210821151126835

    图1-1

    我想站在今天,在做的同学都知道,它是基于http协议来实现数据通信的,这里有两个字很重要,就是“协议”。

    两个计算机之间要实现数据通信,必须遵循同一种协议,否则,就像一个中国人和一个外国人交流时,一个讲英语另一个讲解中文,肯定是无法正常交流。在计算机中,协议非常常见。

    1.1.1 协议的组成

    我们写的Java代码,计算机能够理解并且执行,原因是人和计算机之间遵循了同一种语言,那就是Java,如图1-2所示,.java文件最终编译成.class文件这个过程,也同样涉及到协议。

    image-20210821151817974

    图1-2 java编译过程

    所以,在计算机中,协议是指大家需要共同遵循的规则,只有实现统一规则之后,才能实现不同节点之间的数据通信,从而让计算机的应用更加强大。

    组成一个协议,需要具备三个要素:

    • 语法,就是这一段内容要符合一定的规则和格式。例如,括号要成对,结束要使用分号等。
    • 语义,就是这一段内容要代表某种意义。例如数字减去数字是有意义的,数字减去文本一般来说就没有意义。
    • 时序,就是先干啥,后干啥。例如,可以先加上某个数值,然后再减去某个数值。

    1.1.2 http协议

    理解了协议的作用,那协议是长什么样的呢?

    那么再来看图1-3的场景,人们通过浏览器访问网站,用到了http协议。

    image-20210821151126835

    图1-3 http协议

    http协议包含包含几个部分:

    • http请求组成
      • 状态行
      • 请求头
      • 消息主体
    • http响应组成
      • 状态行
      • 响应头
      • 响应正文

    Http响应报文如图1-4所示,那么这个协议的三要素分别是:

    • 语法: http协议的消息体由状态、头部、内容组成。
    • 语义: 比如状态,200表示成功,404表示请求路径不存在等,通信双方必须遵循该语义。
    • 时序: 组成消息体的三部分的排列顺序,必须要有request,才会产生response。

    而浏览器按照http协议做好了相关的处理后,才能让大家通过网址访问网络上的各种信息。

    image-20210821155309605

    图1-4

    1.1.3 常用的网络协议

    DNS协议、Http协议、SSH协议、TCP协议、FTP协议等,这些都是大家比较常用的协议类型。无论哪种协议,本质上仍然是由协议的三要素组成,只是应用场景不同。

    DNS、HTTP、HTTPS 所在的层我们称为应用层。经过应用层封装后,浏览器会将应用层的包交给下一层去完成,通过 socket 编程来实现。下一层是传输层。传输层有两种协议,一种是无连接的协议 UDP,一种是面向连接的协议 TCP。对于通信可靠性要求的场景来说,往往使用 TCP 协议。所谓的面向连接就是,TCP 会保证这个包能够到达目的地。如果不能到达,就会重新发送,直至到达。

    1.3 TCP/IP通信原理分析

    一次网络通信到底是怎么完成的呢?

    涉及到网络通信,那我们一定会提到一个网络模型的概念,如图1-5所示。表示TCP/IP的四层概念模型和OSI七层网络模型,它是一种概念模型,由国际标准化组织提出来的,试图让全世界范围内的计算机能基于该网络标准实现互联。

    image-20210821170017901

    图1-5

    网络模型为什么要分层呢?其实从我们现在的业务分层架构中就不难发现,任何系统一旦变得复杂,就都会采用分层设计。它的主要好处是

    • 实现高内聚低耦合
    • 每一层有自己单一的职责
    • 提高可复用性和降低维护成本

    1.2.1 http通信过程的发送数据包

    由于我们的课程并不是专门来讲网络,所以只是提及一下网络分层模型,为了让大家更简单的理解网络分层模型的工作原理,我们仍然以一次网络通信的数据包传输为例进行分析,如图1-6所示。

    image-20210821175033443

    图1-6

    图1-6的工作流程描述如下:

    • 假设我们要登录某一个网站,此时基于Http协议会构建一个http协议报文,这个报文中按照http协议的规范组装,其中包括要传输的用户名和密码。这个是属于应用层协议。

    • 经过应用层封装后,浏览器会把应用层的包交给TCP/IP四层模型中的下一层,也就是传输层来完成,传输层有两种协议:

      • TCP协议,可靠的通信协议,该协议会确保数据包能达到目的地
      • UDP协议,不可靠通信协议,可能会存在数据丢失

      在http通信中使用了TCP协议,TCP协议会有两个端口,一个是浏览器监听的端口,一个是目标服务器进程的端口。操作系统会根据端口来判断这个数据包应该分发给那个进程。

    • 传输层封装完成后,该数据包会技术交给网络层来处理,网络层协议是IP协议,IP协议中会包含源IP地址(也就是客户端及其的IP)和目标服务器的IP地址。

    • 操作系统知道了目标IP地址后,就开始根据这个IP来寻找目标机器,而目标服务器一定是部署在不同的地方,这种跨网络节点的访问,需要经过网关(所谓网关就是一个网络到另外一个网络的关口)。

      所以数据包首先需要先通过自己当前所在网络的网关出去,然后访问到目标服务器,但是在数据包传输到目标服务器之前,需要再组装MAC头信息。

      Mac头包含本地的Mac地址和目标服务器的Mac地址,这个MAC地址怎么获得的呢?

      • 获取本机MAC地址的方法是,操作系统会发送一个广播消息询问网关地址(192.168.1.1)是谁?收到该广播消息的网关会回应一个MAC地址。这个广播消息是基于ARP协议实现的(这个协议简单来说就是已知目标机器的ip,需要获得目标机器的mac地址。(发送一个广播消息,这个ip是谁的,请来认领。认领ip的机器会发送一个mac地址的响应))。

        为了避免每次都用 ARP 请求,机器本地也会进行 ARP 缓存。当然机器会不断地上线下线,IP 也可能会变,所以 ARP 的 MAC 地址缓存过一段时间就会过期。

      • 获取远程机器的MAC地址的方法也同样是基于ARP协议实现的。

    完成MAC地址组装后,一个完整的数据包就构成了。这个时候会把这个数据包给到网卡,网卡再把这个数据包发出去,由于这个数据包中包含MAC地址,因此它能够到达网关进行传输。网关收到包之后,会根据路由信息,判断下一步应该怎么走。网关往往是一个路由器,到某个 IP 地址应该怎么走,这个叫作路由表。

    1.2.2 http通信过程中的接收数据包

    当数据包发送到网关后,会根据网关的路由信息判断该数据包要传输到那个网段上。数据从客户端发送到目标服务器,可能会经过多个网关,所以数据包根据网关路由进入到下一个网关后,继续根据下一个网关的MAC地址寻找下下一个网关,直到到达目标网络服务器上。

    这个时候服务器收到包之后,最后一个网关知道这个网络包就是要去当前局域网的,于是拿着目标IP通过ARP协议大喊一声这是谁? 目标服务器就会给网关回复一个MAC地址。 然后网络包在最后那个网关修改目标的MAC地址,通过这个MAC地址,网络包找到了目标服务器。

    当目标服务器和MAC地址对上后,开始取出MAC头信息,接着把数据包发送给操作系统的网络层。网络层会取出IP头信息,IP头里面会写上一层封装的是TCP协议,于是交给传输层来处理,实现过程如图1-7所示。

    在这一层中,对于收到的每个数据包都会有一个回复,表示服务器端已经收到了该数据包。如果过一段时间客户端没有收到该确认包,发送端的 TCP 层会重新发送这个包,还是上面的过程,直到最终收到回复。

    这个重试是TCP协议层来实现的,不需要我们应用来主动发起。

    image-20210822152538600

    图1-7

    为什么有了MAC层还要走IP层呢?

    之前我们提到,mac地址是唯一的,那理论上,在任何两个设备之间,我应该都可以通过mac地址发送数据,为什么还需要ip地址?

    mac地址就好像个人的身份证号,人的身份证号和人户口所在的城市,出生的日期有关,但是和人所在的位置没有关系,人是会移动的,知道一个人的身份证号,并不能找到它这个人,mac地址类似,它是和设备的生产者,批次,日期之类的关联起来,知道一个设备的mac,并不能在网络中将数据发送给它,除非它和发送方的在同一个网络内。

    所以要实现机器之间的通信,我们还需要有ip地址的概念,ip地址表达的是当前机器在网络中的位置,类似于城市名+道路号+门牌号的概念。通过ip层的寻址,我们能知道按何种路径在全世界任意两台Internet上的的机器间传输数据。

    1.4 详解TCP可靠性通信特性

    我们知道,TCP协议是属于可靠性通信协议,它能够确保数据包不被丢失。首先我们先了解一下TCP的三次握手和四次挥手。

    1.4.1 TCP的三次握手

    两个节点需要进行数据通信,首先得先建立连接。而在建立连接时,TCP采用了三次握手来实现连接建立。如图1-8所示。

    image-20210822161148923

    图1-8

    第一次握手(SYN=1, seq=x)

    客户端发送一个 TCP的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,**保存在包头的序列号(Sequence Number)**字段里。发送完毕后,客户端进入 SYN_SEND 状态。

    第二次握手(SYN=1, ACK=1, seq=y, ACK num=x+1):

    服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

    第三次握手(ACK=1,ACK num=y+1)

    客户端再次发送确认包(ACK),SYN标志位为0,ACK标志位为1,并且把服务器发来 ACK的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN发完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP握手结束。

    1.4.2 TCP为什么是三次握手?

    TCP是全双工,如果没有第三次的握手,服务端不能确认客户端是否ready,不知道什么时候可以往客户端发数据包。三次的握手刚好两边都互相确认对方已经ready。

    我们假设网络的不可靠性,

    A发起一个连接,当发起一个请求没有得到反馈的时候,会有很多可能性,比如请求包丢失,或者超时,或者B没有响应

    由于A不能确认结果,于是再发,当有一个请求包到了B之后,A并不知道这个数据包已经到了B,所以可能还会重试。

    所以B收到请求之后,知道了A的存在并且要和我建立连接,这个时候B会发送ack给到A,告诉A我收到了请求包。

    对于B来说,这个应答包也是一个网络通信,我怎么知道能不能到达A呢?所以这个时候B不能很主观的认为连接已经建立好了,还需要等到A再次发送应答包来确认。

    1.4.3 TCP的四次挥手

    如图1-9所示,TCP的连接断开,会通过所谓的四次挥手完成。

    四次挥手表示TCP断开连接的时候,需要客户端和服务端总共发送4个包以确认连接的断开;客户端或服务器均可主动发起挥手动作(因为TCP是一个全双工协议),在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

    image-20210822162756038

    图1-9

    上述交互过程如下:

    • 断开的时候,我们可以看到,当A客户端说说“我要断开连接”,就进入 FIN_WAIT_1 的状态。

    • B 服务端收到“我要断开连接”的消息后,发送"知道了"给到A客户端,就进入 CLOSE_WAIT 的状态。

    • A 收到“B 说知道了”,就进入 FIN_WAIT_2 的状态,如果这个时候 B 服务器挂掉了,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。

    • 如果 B 服务器正常,则发送了“B 要关闭连接”的请求到达 A 时,A 发送“知道 B 也要关闭连接”的 ACK 后,从 FIN_WAIT_2 状态结束。

    • 按说这个时候 A 可以退出了,但是最后的这个 ACK 万一 B 收不到呢?则 B 会重新发一个“B 要关闭连接”,这个时候 A 已经跑路了的话,B 就再也收不到 ACK 了,因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到 ACK 的话,“B 说不玩了”会重发的,A 会重新发一个 ACK 并且足够时间到达 B。

    这个等待实现是2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃(此时A直接进入CLOSE状态)。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

    第一次挥手(FIN=1,seq=x)

    假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入 FIN_WAIT_1 状态。

    第二次挥手(ACK=1,ACKnum=x+1)

    服务器端确认客户端的 FIN包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

    第三次挥手(FIN=1,seq=w)

    服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN置为1。发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

    第四次挥手(ACK=1,ACKnum=w+1)

    客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK包。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

    【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

    答:三次握手是因为因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET(因为可能还有消息没处理完),所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

    【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

    答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

    1.4.4 TCP协议的报文传输

    连接建立好之后,就开始进行数据包的传输了。那TCP作为一个可靠的通信协议,如何保证消息传输的可靠性呢?

    TCP采用了消息确认的方式来保证数据报文传输的安全性,也就是说客户端发送了数据包到服务端后,服务端会返回一个确认消息给到客户端,如果客户端没有收到确认包,则会重新再发送。

    为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)

    如图1-10所示,为了记录所有发送的包和接收的包,TCP协议在发送端和接收端分别拿会有发送缓冲区和接收缓冲区,TCP的全双工的工作模式及TCP的滑动窗口就是依赖于这两个独立的Buffer和该Buffer的填充状态。

    接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到Socket的内核接收缓冲区。

    read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里。进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer里复制到Socket的内核发送缓冲区,然后send就会在上层返回。换句话说,send返回时,数据不一定会被发送到对端。

    image-20210822173920975

    图1-10

    发送端/接收端的缓冲区中是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

    • 第一部分:发送了并且已经确认的。
    • 第二部分:发送了并且尚未确认的。需要等待确认后,才能移除。
    • 第三部分:没有发送,但是已经等待发送的。
    • 第四部分:没有发送,并且暂时还不会发送的。

    这里的第三部分和第四部分之所以做一个区分,其实是因为TCP采用做了流量控制,这里采用了滑动窗口的方式来实现流量整形,避免出现数据拥堵的情况。

    image-20210822173129991

    图1-11

    为了更好的理解数据包的通信过程,我们通过下面这个网址来演示一下

    https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html

    1.4.5 滑动窗口协议

    上述地址中动画演示的部分,其实就是数据包发送和确认机制,同时还涉及到互动窗口协议。

    滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题;发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口

    发送窗口

    就是发送端允许连续发送的幀的序号表。

    发送端可以不等待应答而连续发送的最大幀数称为发送窗口的尺寸。

    接收窗口

    接收方允许接收的幀的序号表,凡落在 接收窗口内的幀,接收方都必须处理,落在接收窗口外的幀被丢弃。

    接收方每次允许接收的幀数称为接收窗口的尺寸。

    1.5 理解阻塞通信的本质

    理解了TCP通信的原理后,在Java中我们会采用Socket套接字来实现网络通信,下面这段代码演示了Socket通信的案例。

    public class ServerSocketExample {
    
        public static void main(String[] args) throws IOException {
            final int DEFAULT_PORT = 8080;
            ServerSocket serverSocket = null;
            serverSocket = new ServerSocket(DEFAULT_PORT);
            System.out.println("启动服务,监听端口:" + DEFAULT_PORT);
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("客户端:" + socket.getPort() + "已连接");
                new Thread(new Runnable() {
                    Socket socket;
                    public Runnable setSocket(Socket s){
                        this.socket=s;
                        return this;
                    }
                    @Override
                    public void run() {
                        try {
                            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                            String clientStr = null; //读取一行信息
                            clientStr = bufferedReader.readLine();
                            System.out.println("客户端发了一段消息:" + clientStr);
                            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                            bufferedWriter.write("我已经收到你的消息了");
                            bufferedWriter.flush(); //清空缓冲区触发消息发送
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }.setSocket(socket)).start();
    
            }
        }
    }
    

    在我们讲Redis的专题中详细讲到过,上述通信是BIO模型,也就是阻塞通信模型,阻塞主要体现的点是

    • accept,阻塞等待客户端连接
    • io阻塞,阻塞等待客户端的数据传输。

    相信大家和我一样有一些以后,这个阻塞和唤醒到底是怎么回事,下面我们简单来了解一下。

    1.5.1 阻塞操作的本质

    阻塞是指进程在等待某个事件发生之前的等待状态,它是属于操作系统层面的调度,我们通过下面操作来追踪Java程序中有多少程序,每一个线程对内核产生了哪些操作。

    strace,Linux操作系统中的指令

    1. 把ServerSocketExample.java,去掉package导入头,拷贝到linux服务器的 /data/app目录下。

    2. 使用javac ServerSocketExample.java进行编译,得到.class文件

    3. 使用下面这个命令来追踪(打开一个新窗口)

      按照strace官网的描述, strace是一个可用于诊断、调试和教学的Linux用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。

      strace -ff -o out java ServerSocketExample
      
      • -f 跟踪目标进程,以及目标进程创建的所有子进程
      • -o 把strace的输出单独写到指定的文件
    4. 上述指令执行完成后,会在/data/app目录下得到很多out.*的文件,每个文件代表一个线程。因为Java本身是多线程的。

      [root@localhost app]# ll
      total 748
      -rw-r--r--. 1 root root  14808 Aug 23 12:51 out.33320 //最小的表示主线程
      -rw-r--r--. 1 root root 186893 Aug 23 12:51 out.33321
      -rw-r--r--. 1 root root    961 Aug 23 12:51 out.33322
      -rw-r--r--. 1 root root    917 Aug 23 12:51 out.33323
      -rw-r--r--. 1 root root    833 Aug 23 12:51 out.33324
      -rw-r--r--. 1 root root    819 Aug 23 12:51 out.33325
      -rw-r--r--. 1 root root  23627 Aug 23 12:53 out.33326
      -rw-r--r--. 1 root root   1326 Aug 23 12:51 out.33327
      -rw-r--r--. 1 root root   1144 Aug 23 12:51 out.33328
      -rw-r--r--. 1 root root   1270 Aug 23 12:51 out.33329
      -rw-r--r--. 1 root root   8136 Aug 23 12:53 out.33330
      -rw-r--r--. 1 root root   8158 Aug 23 12:53 out.33331
      -rw-r--r--. 1 root root   6966 Aug 23 12:53 out.33332
      -rw-r--r--. 1 root root   1040 Aug 23 12:51 out.33333
      -rw-r--r--. 1 root root 445489 Aug 23 12:53 out.33334
      
    5. 打开out.33321这个文件(主线程后面的一个文件),shift+g到该文件的尾部,可以看到如下内容。

      下面这些方法,都是属于系统调用,也就是调用操作系统提供的内核指令触发相关的操作。

      # 创建socket fd 
      socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5 
      ....
      # 绑定8888端口
      bind(5, {sa_family=AF_INET6, sin6_port=htons(8888), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
      # 创建一个socket并监听申请的连接, 5表示sockfd,50表示等待队列的最大长度
      listen(5, 50)                           = 0
      mprotect(0x7f21d00df000, 4096, PROT_READ|PROT_WRITE) = 0
      write(1, "\345\220\257\345\212\250\346\234\215\345\212\241\357\274\214\347\233\221\345\220\254\347\253\257\345\217\243\357\274\23288"..., 34) = 34
      write(1, "\n", 1)                       = 1
      lseek(3, 58916778, SEEK_SET)            = 58916778
      read(3, "PK\3\4\n\0\0\10\0\0U\23\213O\336\274\205\24X8\0\0X8\0\0\25\0\0\0", 30) = 30
      lseek(3, 58916829, SEEK_SET)            = 58916829
      read(3, "\312\376\272\276\0\0\0004\1\367\n\0\6\1\37\t\0\237\1 \t\0\237\1!\t\0\237\1\"\t\0"..., 14424) = 14424
      # poll, 把当前的文件指针挂到等待队列,文件指针指的是fd=5,简单来说就是让当前进程阻塞,直到有事件触发唤醒
      * events: 表示请求事件,POLLIN(普通或优先级带数据可读)、POLLERR,发生错误。
      poll([{fd=5, events=POLLIN|POLLERR}], 1, -1
      

    从这个代码中可以看到,Socket的accept方法最终是调用系统的poll函数来实现线程阻塞的。

    通过在linux服务器输入 man 2 poll

    man: 帮助手册

    2: 表示系统调用相关的函数

    DESCRIPTION
           poll()  performs  a  similar  task  to  select(2): it waits for one of a set of file
           descriptors to become ready to perform I/O.
    

    poll类似于select函数,它可以等待一组文件描述符中的IO就绪事件

    1. 通过下面命令访问socket server。

      telnet 192.168.221.128 8888
      

      这个时候通过tail -f out.33321这个文件,发现被阻塞的poll()方法,被POLLIN事件唤醒了,表示监听到了一次连接。

      poll([{fd=5, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=5, revents=POLLIN}])
      accept(5, {sa_family=AF_INET6, sin6_port=htons(53778), inet_pton(AF_INET6, "::ffff:192.168.221.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
      

    1.5.2 阻塞被唤醒的过程

    如图1-12所示,网络数据包通过网线传输到目标服务器的网卡,再通过2所示的硬件电路传输,最终把数据写入到内存中的某个地址上,接着网卡通过中断信号通知CPU有数据到达,操作系统就知道当前有新的数据包传递过来,于是CPU开始执行中断程序,中断程序的主要逻辑是

    • 先把网卡接收到的数据写入到对应的Socket接收缓冲区中
    • 再唤醒被阻塞在poll()方法上的线程

    img

    图1-12

    1.5.3 阻塞的整体原理分析

    操作系统为了支持多任务处理,所以实现了进程调度功能,运行中的进程表示获得了CPU的使用权,当进程(线程)因为某些操作导致阻塞时,就会释放CPU使用权,使得操作系统能够多任务的执行。

    当多个进程是运行状态等待CPU调度时,这些进程会保存到一个可运行队列中,如图1-13所示。

    image-20210823143314215

    图1-13
    当进程A执行创建Socket语句时,在Linux操作系统中会创建一个由文件系统管理的Socket对象,这个Socket对象包含发送缓冲区、接收缓冲区、等待队列等,其中等待队列是非常重要的结构,它指向所有需要等待当前Socket事件的进程,如图1-14所示。

    当进程A调用poll()方法阻塞时,操作系统会把当前进程A从工作队列移动到Socket的等待队列中(将进程A的指针指向等待队列,后续需要进行唤醒),此时A被阻塞,CPU继续执行下一个进程。

    image-20210823145055784

    图1-14

    当Socket收到数据时,等待该Socket FD的进程会收到被唤醒,如图1-15所示,计算机通过网卡接收到客户端传过来的数据,网卡会把这个数据写入到内存,然后再通过中断信号通知CPU有数据到达,于是CPU开始执行中断程序。

    当发生了中断,就意味着需要操作系统的介入,开展管理工作。由于操作系统的管理工作(**如进程切换、分配IO设备)需要使用特权指令,因此CPU要从用户态转换为核心态。**中断就可以使CPU从用户态转换为核心态,使操作系统获得计算机的控制权。因此,有了中断,才能实现多道程序并发执行。

    此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面(步骤 ④),再唤醒进程 A(步骤 ⑤),重新将进程 A 放入工作队列中。

    image-20210823150147501

    图1-15

    1.5 Linux中的select/poll模型本质

    前面在1.4节中讲的其实是Recv()方法,它只能监视单个Socket。而在实际应用中,这种单Socket监听很明显会影响到客户端连接数,所以我们需要寻找一种能够同时监听多个Socket的方法,而select/poll就是在这个背景下产生的,其中poll方法在前面的案例中就讲过,默认情况下使用poll模型。

    先来了解一下select模型,由于在前面的分析中我们知道Recv()只能实现对单个socket的监听,当客户端连接数较多的时候,会导致吞吐量非常低,所以我们想,能不能实现同时监听多个socket,只要任何一个socket连接存在IO就绪事件,就触发进程的唤醒。

    如图1-16所示,假设程序同时监听socket1和socket2这两个socket连接,那么当应用程序调用select方法后,操作系统会把进程A分别指向这连个个socket的等待队列中。当任何一个Socket收到数据后,中断程序会唤醒对应的进程。

    当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。

    image-20210823153708311

    图1-16

    select模式有二个问题,

    • 就是每次调用select都需要将进程加入到所有监视器socket的等待队列,每次唤醒都需要从等待队列中移除,这里涉及到两次遍历,有一定的性能开销。
    • 进程被唤醒后,并不知道哪些socket收到了数据,所以还需要遍历一次所有的socket,得到就绪的socket列表

    由于这两个问题产生的性能影响,所以select默认规定只能监视1024个socket,虽然可以通过修改监视的文件描述符数量,但是这样会降低效率。而poll模式和select基本是一样,最大的区别是poll没有最大文件描述符限制。

    1.6 Linux中的epoll模型

    有没有更加高效的方法,能够减少遍历也能达到同时监听多个fd的目的呢?epoll模型就可以解决这个问题。

    epoll 其实是event poll的组合,它和select最大的区别在于,epoll会把哪个socket发生了什么样的IO事件通知给应用程序,所以epoll实际上就是事件驱动,具体原理如图1-17所示。

    在epoll中提供了三个方法分别是epoll_create、epoll_ctl、epoll_wait。具体执行流程如下

    • 首先调用epoll_create方法,在内核创建一个eventpoll对象,这个对象会维护一个epitem集合,它是一个红黑树结构。这个集合简单理解成fd集合。
    • 接着调用epoll_ctl函数将当前fd封装成epitem加入到eventpoll对象中,并给这个epitem加入一个回调函数注册到内核。当这个fd收到网络IO事件时,会把该fd对应的epitem加入到eventpoll中的就绪列表rdlist(双向链表)中。同时再唤醒被阻塞的进程A。
    • 进程A继续调用epoll_wait方法,直接读取epoll中就绪队列rdlist中的epitem,如果rdlist队列为空,则阻塞等待或者等待超时。

    从epoll的原理中可以得知,由于rdlist的存在,使得进程A被唤醒后知道哪些Socket(fd)发生了IO事件,从而在不需要遍历的情况下获取所有就绪的socket连接。

    image-20210823212943850

    图1-17

    版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构
    如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!

    展开全文
  • 但是底层实质上还是两台计算机的连接,通过字节流交换数据,通过协议来规定传输控制和数据解码,而我们的web应用程序就是基于HTTP协议的网络应用程序,由于涉及到的网络解决,所以技术人员将有关网络解决部分独立...

    5fe5e9776d16b01773f7c5b086993d84.png

    前言

    前面一篇文章,我从整个应用程序的整体以及跟运行环境的关系简单聊了一下我们现在常用的Spring框架的设计基础和准则,其中主要是控制反转和依赖注入,以及容器化编程等概念。

    这里我不想去复述这些概念的定义,由于那些东西网上随意都能百度到,我想通过我的形容将这些概念串联起来,让大家更好的去立即它们知道为什么要这样去做,我们每天开发使用的框架究竟是个什么东西,它的设计思想以及规范的由来。做到知其然还知其所以然,能够让我们在开发过程中更好的去使用它们,面对问题知道它大概的处理方向。

    本文我想继续沿着前面的思路来谈谈基于Web的应用程序需要使用Spring框架的容器化管理开发相关的了解。

    Web应用程序与Servlet规范

    当然说起应用程序开发来,我们都熟习,现在应用程序有很多种分类,最初的控制台程序,服务组件程序,到桌面应用程序,到基于HTTP访问协议的web应程序等。

    其实它们的本质就是基于某种输入/输出过程解决的程序。比方我们最常见但是实际应用中很少的控制台应用程序,它就是基于标准的I/O实现类的应用程序,接收命令行作为输入流,控制台作为标准输出形式的应用程序。它的运行只要要有一个进程壳来构建输入和输出流就可。

    而对于我们今天要详细谈的Web应用程序,其实它是起源于一种运行在操作系统上在组件程序,只不过它们的数据输入输出是基于网络数据流的。

    网络基础

    从基础的网络知识我们知道,网络上传输数据需要通过一个7层模型,也就是从最初的网络硬件笼统定义到最高级的应用程序这个层级穿透而来。

    要将两台物理的机器连接起来,我们需要对两个机器进行标识命名,这些靠的是IP和端口,而网络链路上传输的数据都是字节数据流,要知道这些数据流是没有什么具体格式的,但是到了网络层时,我们必需要知道它来自哪里要发给谁,所以我们需要对其进行肯定的格式限制,这种笼统是通过电报格式定义来完成的。

    比方我们需要定义发送的长度,标记为,能否有顺序等,这些字节流就被包裹成一个个数据报文,而后我们必需定义每个发送端和接收端之间的商定,就是告诉对方我发送的是什么,你该如何接收它,比方多长是一个完整数据包,数据包的先后顺序等,这些都是在我们知道了两个通信店的IP地址以及如何连接也就是我们说的传输控制协议TCP的基础上我们定义了更高级的应用协议,比方HTTP,FILE,MAIL等协议,当然最常见的协议就是HTTP协议了。

    HTTP协议

    它是在基础的报文格式定义基础上的一个更高级的笼统,它能告诉通信双方我们通信的数据如何解析。就拿超文本传输协议来说吧,它规定了头信息和内容信息,它还规定了解决这些信息的方法以及结果反馈代码,这就是我们常说的GET,POST,DELETE,OPTIONS等,返回代码比方从100,到500系列,当然这些已经进入到了应用协议部分。

    我们所有的网络应用程序都是基于点对点的通信的应用。也就是说要创立这样的程序我们必需首先标识出能够连接的两个点,首先是主机名或者者IP地址,而后就是我们要连接的具体应用,它一般会表现在哪个端口上或者者端口下面的哪个路径上。有了对等点的定义形容,我们即可以定义其控制传输的笼统,套接字概念。

    编解码问题

    其实质上就是创立一个输入输出流的通道,网络数据流都是通过Socket这个概念来定义和形容的。而我们编程,特别是Java的编程,我们只要要在我们应用程序所管理的空间中定义一个可以连接网络Socket的通道,同时在内存中划出一块缓冲区,让通道能够有可操作的空间,而后利用不同缓冲区之间数据流动的过程对数据进行相应形式的变化,比方最基础的是如何将网络传输的字节流,转变为我们高级语言定义高级数据类型的过程,这个过程通常被称为解码,同样当我们需要将应用程序能够了解的各种数据类型转变为可用通过网络传输给其它地方的字节流时,这个过程被称为编码。

    由于硬件能够搞懂的目前就两种状态,这两种状态用数字表示就是二进制的0和1,所以我们使用的众多高级编程语言里,哪些复杂的数据类型都需要转变成二进制的字节形式才可以被CPU了解,被网络硬件传输。因而我们的编程不可避免的就需要去完成这种编码和解码解决。当然随着高级语言的不断进化,各类常用的解决都已经变身为各种语言标准的类库或者者功能包了,我们只要要拿来用就可。除非你想编写自己的通信协议或者者定义特殊的数据格式,否则对于编解码来说一般不会涉及到。

    理解了所有应用程序都是对数据流的解决这个基础后,我们再来看Web应用程序,它是一种基于网络的服务和独立访问结构的应用程序,也就是我们常说的Server-Client模式。

    关于Servlet

    这里之所以定义出服务端和用户端其实主要还是一个功能上的区分,但是底层实质上还是两台计算机的连接,通过字节流交换数据,通过协议来规定传输控制和数据解码,而我们的web应用程序就是基于HTTP协议的网络应用程序,由于涉及到的网络解决,所以技术人员将有关网络解决部分独立出来规定了很多规范,比方端点形容规范,数据传输格式规范,如何利用所在计算机操作系统环境的设置的规范等,这些反应到Java编程里就是我们都熟习的Servlet规范。

    这个规范首先告诉我们基于Web的应用程序的基础网络部分需要在每台联网计算机上有一个角色来负责,我们称这个角色为容器,或者者说是web服务器。

    它就是要实现对计算机网络的标识,连接,规定解析数据格式等工作,当而后来我们将其发展成综合性的服务器,一边要解决HTTP协议,一边还可以通过少量接口更操作系统进行交互调用操作系统的功能组件来解决。比方网卡,文件的输入输出控制器等等。

    我们可以简单的形容一下一个Servlet容器的实现功能,首先它需要对运行自己的主机信息有一个笼统,能够让运行的程序理解它以及使用它可以使用的资源。

    而后,它需要将基于网络的字节流进行高级语言数据类型的转换,比方将基于字节流解析成遵循HTTP协议的数据格式,HttpRequest,HttpResponse以及HttpServletRequest,HttpServletResponse等。

    同时将对于宿主服务器环境参数笼统后引入到该容器中用于跟我们的应用程序交互。所以只需实现了Servlet的规范,即可以作为操作系统和我们应用程序之间的媒介。

    Servlet容器

    市场上有许多成熟Servlet容器产品比方Tomcat,Jetty,Weblogic,Glassfish等。这里面有很多轻量级的,只负责将输入的网络数据流转换为我们应用程序能够了解和解决的数据形式,而这个过程都是通过创立输入和输出数据流的过程来完成的。有少量商业应用的实现附加的内容比较多,比方对系统环境资源的笼统继承,比方数据库连接资源,文件输入输出组件等。

    我们开发的应用程序根据我们设计开发准则,我们首先将应用分解能功能组件,将每个功能组件设计成一个可以在容器中独立运行的组件,该组件就是HttpServlet请求解决组件。

    我们会根据请求的目标地址来标记各种功能,而后将这些唯一的目标地址和HTTP方法来标识运行的目标组件,而这个组件可以通过容器来计算机环境进行交互。

    所以Servlet的顶层笼统就是一个service方法,该方法的输入参数就是由容器进行封装过的请求体和回复体以及环境变量对象等。

    当然我们会根据HTTP协议来具体的细化其支持的HTTP方法,所以我们可以来通过doGet,doPost等方法来完成具体的解决。

    有了我们这样一个基础规范的功能实现,我们就有了一个可以包容和管理具体功能应用组件的容器,这个容器就是我们所说的web服务器。

    假如你清楚了Servlet规范的本质就是对网络数据流的封装和编解码解决,你可以自己动手从基础的二进制数据流的封装和编解码转换开始设计自己的web应用服务器。

    也就是去实现点对点的通信解决,这里说一下,目前的微服务架构的基础就是对web服务器基础网络实现的重新分解设计。

    Servlet 3.0 引入了反应流概念,就是通过接收方控制来管理大批量数据流的输入输出。

    Spring框架和Web应用设计

    理解了上面有关Web应用程序的结构后,我们再来看看Spring框架在web应用程序开发中扮演的角色是什么。

    我们知道Java企业级开发中有Java EE框架,其实就是基于Servlet容器来的,它只是将企业级应用开发的所有基础功能都组件化了,比方容器化依赖注入,JPA等,当然必需有匹配的Web应用服务器来支持其运行。

    同样的Spring框架的核心部分就是组件容器,它的功能是通过更加有效更加轻量级的去组织和管理应用程序各功能组件。

    其巧妙之处在于将整个组件设计成了一个Servlet组件实现,这就是Spring框架里最为核心的DispatchServlet,跟所有Servlet定义规范一样,我们需要用一个请求的目标路径来标识这个Servlet,而后让Servlet容器在启动时将它加载,并绑定到目标路径上,以此在一个对根目录请求的解决器中启动一个应用程序组件管理容器,并将其解决器handler实现成一个前台控制模式,负责对其根目录后的URL部分进行识别和匹配,以此来实现对Spring容器中负责解决后续URL资源的解决器的路由。

    简单说来,就是当外部访问请求通过网络到达Web服务器时,会将其根据Servlet规范和HTTP协议将其解码成HttpServlet的请求和回复数据结构类型,而后解析其访问的目标资源URL,来匹配我们在Spring容器中注册的用于解决它的组件和方法名称,从而完成对该Servlet请求的解决。

    因为现在我们开发应用程序时除了连续的文件上传下载解决外,大多都是将二进制转换为JSON数据格式或者者XML格式,如此我们只要要在Spring容器中注册相应的解决组件就可。

    总结

    说到这里想说的东西还没说完,但是文章长度已经超出了预期,所以就此打住吧,只能在接下来另辟文章继续讲了。

    本篇文章简单的讲了一下从Web应用程序的特点,以及能够辅助Web应用程序运行的基础容器服务规范,进而到了Spring框架的设计准则和结构实现设计。

    这里希望能够带大家从Web应用程序有别于其余类型的应用程序的特点开始,到支持Web应用程序运行的Servlet规范实现,在到Spring框架应用在Web应用程序时扮演的角色等内容过了一遍。接下来我会继续沿着这个思路,讲一下MVC模式,以及反应流解决模式等内容。

    展开全文
  • java socket原理

    2021-02-28 08:54:59
    TCP / IP模型中有5层结构:应用层、传输层、网络层、数据链路层以及物理层。其中IP协议是位于网络层的,TCP协议是位于传输层的,通过IP协议可以使两台计算机使用同一种语言,从而允许Internet上连接不同...在Java中...

    TCP / IP模型中有5层结构:应用层、传输层、网络层、数据链路层以及物理层。其中IP协议是位于网络层的,TCP协议是位于传输层的,通过IP协议可以使两台计算机使用同一种语言,从而允许Internet上连接不同类型的计算机和不同的操作系统的网络,IP协议只能保证计算机能够接收和发送分组数据,当计算机要和远程的计算机建立连接时,TCP协议会让它们建立连接:用于发送和接收数据的虚拟电路。

    在Java中,我们使用Socket、ServerSocket类创建一个套接字连接,从套接字得到的结果就是一个InputStream以及OutputStream对象,以便将连接作为一个IO流对象对待。通过IO流可以从流中读取数据或者写数据到流中。

    套接字是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。Java有两个基于数据流的套接字类:ServerSocket,服务器用它监听进入的连接,Socket,客户端用它初始一次连接。监听套接字只能接收新的连接请求,不能接收实际的数据包,即ServerSocket不能接收实际的数据包。

    套接字是基于TCP / IP实现的,它是用来提供一个访问TCP的服务接口,即Socket是TCP的应用编程接口API,通过它应用层就可以访问TCP提供的服务。

    Socket简介

    Socket字面意思就是套接字,包含了源IP地址、源端口、目的IP地址和目的端口。

    Socket是应用层与TCP / IP协议族通信的中间软件抽象层,它是一组接口,在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP / IP协议族隐藏在Socket接口后面。

    Socket底层数据结构

    对于套接字结构,是指底层实现(包括JVM和TCP / IP,但通常是后者)的数据结构集合,包含了特定Socket所关联的信息。套接字结构包含:

    该套接字所关联的本地和远程互联网地址和端口

    一个FIFO队列用于存放接收到的等待分配的数据(RecvQ),以及一个用于存放等待传输数据的队列(SendQ)

    打开和关闭TCP握手相关的额外协议状态信息

    41afcffb8122

    image.png

    由于TCP提供了一种可信赖的字节流服务,任何写入Socket和OutputStream的数据副本都没有保留,直到连接的另一端将这些数据成功接收,向输出流写数据并不意味着数据实际上已经被发送—,它们只是被复制到本地缓冲区,就算在Socket的OutputStream上进行flush操作,也不能保证数据能够立即发送到信道。此外,字节流服务的自身属性决定了其无法保留输入流中消息的边界消息

    Socket数据传输的底层实现

    一旦创建了一个套接字实例,操作系统就会为其分配缓冲区以存放接收和要发送的数据。

    Java可以设置读写缓冲区的大小:

    setReceiveBufferSize(int size)

    setSendBufferSize(int size

    向输出流写数据并不意味着数据实际上已经被发送,它们只是被复制到了发送缓冲队列SendQ,就是在Socket的OutputStream上调用flush方法,也不能保证数据能够立即发送到网络。真正的数据发送是由操作系统的TCP协议栈模块从缓冲区中取数据发送到网络来完成的。当有数据从网络来到时,TCP协议栈模块接收数据并放入缓冲区队列RecvQ,输入流InputStream通过read方法从RecvQ中取出数据。

    Socket超时

    Scoke连接建立超时

    Socket连接建立是基于TCP的连接建立过程。TCP的连接需要通过3次握手报文来完成,开始建立TCP连接时需要发送同步SYN报文,然后等待确认报文SYN + ACK,最后再发送确认报文ACK。TCP连接的关闭通过4次挥手来完成,主动关闭TCP连接的一方发送FIN报文,等待对方的确认报文,被动关闭的一方也发送FIN报文,然后等待确认报文。

    正在等待TCP连接请求的一端有一个固定长度的连接队列,该队列中的连接已经被TCP接受(即三次握手已经完成),但还没有被应用层所接受。TCP接受一个连接是将其放入这个连接队列,而应用层接受连接是将其从该队列中移出。应用层可以通过设置backlog变量来指明该连接队列的最大长度,即已被TCP接受而等待应用层接受的最大连接数。

    当一个连接请求SYN到达时,TCP确定是否接受这个连接。如果队列中还有空间,TCP模块将对SYN进行确认并完成连接的建立,但应用层只有在三次握手中的第三个报文收到后才会知道这个新连接,如果队列没有空间,TCP将不理会收到的SYN。如果应用层不能及时接受已被TCP接受的连接,这些连接可能会占满整个连接队列,新的连接请求可能不会响应而会超时。如果一个连接请求SYN发送后,一段时间后没有收到确认SYN + ACK,TCP会重传这个连接请求SYN两次,每次重传的时间间隔加倍,在规定的时间内仍然没有收到SYN + ACK,TCP将放弃这个连接请求,连接请求就超时了。

    Java Socket连接建立超时和TCP是相同的,如果TCP建立连接时三次握手超时,那么导致Socket连接建立也就超时了,可以设置Socket连接建立的超时时间:

    connect(SocketAddress endpoint,int timeout)

    如果在timeout内,连接没有建立成功,TimeoutExpection异常被抛出,如果timeout的值小于三次握手的时间,那么Socket连接永远不会建立。

    不同应用层有不同的连接建立过程,Socket的连接建立和TCP一样,仅仅需要三次握手就完成连接,但有些应用程序需要交互很多信息后才能成功建立连接,比如Telnet协议,在TCP三次握手完成后,需要进行选项协商之后,Telnet连接才建立完成。

    2、Socket读超时

    如果输入缓冲队列RecvQ中没有数据,read操作会一直阻塞而挂起线程,直到有新的数据到来或者有异常产生,调用setSoTimeout(int timeout)可以设置超时时间,如果到了超时时间仍没有数据,read会抛出一个SocketTimeoutExpection,程序需要捕获这个异常,但是当前的Socket连接仍然是有效的。

    如果对方进程奔溃、对方机器突然重启、网络断开,本端的read会一直阻塞下去,这是设置超时时间是非常重要的,否则调用read的线程会一直挂起。

    TCP模块把接受到的数据放入RecvQ中,直到应用层调用输入流的read方法来读取。如果RecvQ队列被填满了,这时TCP会根据滑动窗口机制通知对方不要继续发送数据,本端停止接收从对端发送来的数据,直到接受者应用程序调用输入流的read方法后腾出空间。

    3、Socket写超时

    Socket的写超时是基于TCP的超时重传。超时重传是TCP保证数据可靠性传输的一个重要机制,其原理是在发送一个数据报文后就开启一个计时器,在一定时间内如果没有得到发送报文的确认ACK,那么就重新发送报文。如果重新发送多次之后,仍没有确认报文,就发送一个复位报文RST,然后关闭TCP连接。首次数据报文发送与复位报文传输之间的时间差大约为9分钟,也就是说如果9分钟内没有得到确认报文,就关闭连接,但是这个值是根据不同的TCP协议栈实现而不同。

    如果发送端调用write持续地写出数据,直到SendQ队列被填满。如果在SendQ队列已满时调用write方法,则write将被阻塞,直到SendQ有新的空闲空间为止,也就是说直到一些字节传输到了接受者套接字的RecvQ中,如果此时RecvQ队列也已经被填满,所有操作都将被停止,直到接收端调用read方法将一些字节传输到应用程序。

    Socket写超时是基于TCP协议栈的超时重传机制,一般不需要设置write的超时时间,也没有提供这种方法。

    关闭服务端连接

    在客户端和服务端的数据交互完成后,一般需要关闭网络连接。对于服务端来说,需要关闭Socket和ServerSocket。

    在关闭Socket后,客户端并不会马上感知自已的Socket已经关闭,也就是说,在服务端的Socket关闭后,客户端的Socket的isClosed和isConnected方法仍然会分别得到false和true。但对已关闭的Socket的输入输出流进行操作会抛出一个SocketException异常。下面的代码演示了在服务端关闭Socket后,客户端是所何反应的。

    package server;

    import java.net.*;

    class Client

    {

    public static void main(String[] args) throws Exception

    {

    Socket socket = new Socket("127.0.0.1", 1234);

    Thread.sleep(1000);

    // socket.getOutputStream().write(1);

    System.out.println("read() = " + socket.getInputStream().read());

    System.out.println("isConnected() = " + socket.isConnected());

    System.out.println("isClosed() = " + socket.isClosed());

    }

    }

    public class CloseSocket

    {

    public static void main(String[] args) throws Exception

    {

    ServerSocket serverSocket = new ServerSocket(1234);

    while (true)

    {

    Socket socket = serverSocket.accept();

    socket.close();

    }

    }

    }

    read() = -1

    isConnected() = true

    isClosed() = false

    从上面的运行结果可以看出例程Client并未抛出SocketException异常。而在012行的read方法返回了-1。如果将socket.close去掉,客户端的read方法将处于阻塞状态。这是因为Java在发现无法从服务端的Socket得到数据后,就通过read方法返回了-1。如果将011行的注释去掉,Client就会抛出一个SocketException异常。在write时候并不会抛异常, write再去read就会抛异常,因为服务端进程不会向客户端发送ACK 报文,而是发送了一个RST 报文请求将处于异常状态的连接复位。客户端接收到了这个抛异常,

    RST (Reset)

    TCP连接的断开有两种方式:

    连接正常关闭时双方会发送FIN,经历4次挥手过程;

    通过RST包异常退出,此时会丢弃缓冲区内的数据,也不会对RST响应ACK。

    java中,调用Socket#close()可以关闭Socket,该方法类似Unix网络编程中的close方法,将Socket的 读写 都关闭,已经排队等待发送的数据会被尝试发送,最后(默认)发送FIN。考虑一个典型的网络事务,A向B发送数据,A发送完毕后close(),FIN发送出去;B一直read直到返回了-1,也通过close()发送FIN,4次挥手,连接关闭,一切都很和谐。

    那什么时候会用RST而非FIN关闭连接呢?

    Socket#setSoLinger(true,0),则close时会发送RST

    如果主动关闭方缓冲区还有数据没有被应用层消费掉,close会发送RST并忽略这些数据

    A向B发送数据,B已经通过close()方法关闭了Socket,虽然TCP规定半关闭状态下B仍然可以接收数据,但close动作关闭了该socket上的任何数据操作,如果此时A继续write,B将返回RST,A的该次write无法立即通知应用层(因为write仅把数据写入发送缓冲区),只会把状态保存在tcp协议栈内,下次read时才会抛出SocketException。

    2. 对已关闭socket读写会产生的异常

    2.1 主动关闭方

    close()后,无论是发送FIN/RST关闭的,之后再读写均会抛java.net.SocketException:socket is closed.

    2.2 被动关闭方

    被FIN关闭

    写(即向”已被对方关闭的Socket”写)

    如上所说,第一次write得到RST响应但不抛异常,第二次write抛异常,ubuntu下是broken pipe (断开的管道),win7下是Software caused connection abort: socket write error

    读 – 始终返回 -1

    被RST关闭

    读写都会抛出异常:connection reset (by peer)

    重点在于:

    connection reset:另一端用RST主动关闭连接

    broken pipe / Software caused connection abort: socket write error : 对方已调用Socket#close()关闭连接,己方还在写数据

    展开全文
  • 本文介绍了JAVA中实现原生的 socket 通信机制原理,分享给大家,具体如下:当前环境jdk == 1.8知识点socket 的连接处理IO 输入、输出流的处理请求数据格式处理请求模型优化场景今天,和大家聊一下 JAVA 中的 socket ...

    本文介绍了JAVA中实现原生的 socket 通信机制原理,分享给大家,具体如下:

    当前环境

    jdk == 1.8

    知识点

    socket 的连接处理

    IO 输入、输出流的处理

    请求数据格式处理

    请求模型优化

    场景

    今天,和大家聊一下 JAVA 中的 socket 通信问题。这里采用最简单的一请求一响应模型为例,假设我们现在需要向 baidu 站点进行通信。我们用 JAVA 原生的 socket 该如何实现。

    建立 socket 连接

    首先,我们需要建立 socket 连接(核心代码)

    import java.net.InetSocketAddress;

    import java.net.Socket;

    import java.net.SocketAddress;

    // 初始化 socket

    Socket socket = new Socket();

    // 初始化远程连接地址

    SocketAddress remote = new InetSocketAddress(host, port);

    // 建立连接

    socket.connect(remote);

    处理 socket 输入输出流

    成功建立 socket 连接后,我们就能获得它的输入输出流,通信的本质是对输入输出流的处理。通过输入流,读取网络连接上传来的数据,通过输出流,将本地的数据传出给远端。

    socket 连接实际与处理文件流有点类似,都是在进行 IO 操作。

    获取输入、输出流代码如下:

    // 输入流

    InputStream in = socket.getInputStream();

    // 输出流

    OutputStream out = socket.getOutputStream();

    关于 IO 流的处理,我们一般会用相应的包装类来处理 IO 流,如果直接处理的话,我们需要对 byte[] 进行操作,而这是相对比较繁琐的。如果采用包装类,我们可以直接以string、int等类型进行处理,简化了 IO 字节操作。

    下面以 BufferedReader与 PrintWriter作为输入输出的包装类进行处理。

    // 获取 socket 输入流

    private BufferedReader getReader(Socket socket) throws IOException {

    InputStream in = socket.getInputStream();

    return new BufferedReader(new InputStreamReader(in));

    }

    // 获取 socket 输出流

    private PrintWriter getWriter(Socket socket) throws IOException {

    OutputStream out = socket.getOutputStream();

    return new PrintWriter(new OutputStreamWriter(out));

    }

    数据请求与响应

    有了 socket 连接、IO 输入输出流,下面就该向发送请求数据,以及获取请求的响应结果。

    因为有了 IO 包装类的支持,我们可以直接以字符串的格式进行传输,由包装类帮我们将数据装换成相应的字节流。

    因为我们与 baidu 站点进行的是 HTTP 访问,所有我们不需要额外定义输出格式。采用标准的 HTTP 传输格式,就能进行请求响应了(某些特定的 RPC 框架,可能会有自定义的通信格式)。

    请求的数据内容处理如下:

    public class HttpUtil {

    public static String compositeRequest(String host){

    return "GET / HTTP/1.1\r\n" +

    "Host: " + host + "\r\n" +

    "User-Agent: curl/7.43.0\r\n" +

    "Accept: */*\r\n\r\n";

    }

    }

    发送请求数据代码如下:

    // 发起请求

    PrintWriter writer = getWriter(socket);

    writer.write(HttpUtil.compositeRequest(host));

    writer.flush();

    接收响应数据代码如下:

    // 读取响应

    String msg;

    BufferedReader reader = getReader(socket);

    while ((msg = reader.readLine()) != null){

    System.out.println(msg);

    }

    至此,讲完了原生 socket 下的创建连接、发送请求与接收响应的所有核心代码。

    完整代码如下:

    import java.io.*;

    import java.net.InetSocketAddress;

    import java.net.Socket;

    import java.net.SocketAddress;

    import com.test.network.util.HttpUtil;

    public class SocketHttpClient {

    public void start(String host, int port) {

    // 初始化 socket

    Socket socket = new Socket();

    try {

    // 设置 socket 连接

    SocketAddress remote = new InetSocketAddress(host, port);

    socket.setSoTimeout(5000);

    socket.connect(remote);

    // 发起请求

    PrintWriter writer = getWriter(socket);

    System.out.println(HttpUtil.compositeRequest(host));

    writer.write(HttpUtil.compositeRequest(host));

    writer.flush();

    // 读取响应

    String msg;

    BufferedReader reader = getReader(socket);

    while ((msg = reader.readLine()) != null){

    System.out.println(msg);

    }

    } catch (IOException e) {

    e.printStackTrace();

    } finally {

    try {

    socket.close();

    } catch (IOException e) {

    e.printStackTrace();

    }

    }

    }

    private BufferedReader getReader(Socket socket) throws IOException {

    InputStream in = socket.getInputStream();

    return new BufferedReader(new InputStreamReader(in));

    }

    private PrintWriter getWriter(Socket socket) throws IOException {

    OutputStream out = socket.getOutputStream();

    return new PrintWriter(new OutputStreamWriter(out));

    }

    }

    下面,我们通过实例化一个客户端,来展示 socket 通信的结果。

    public class Application {

    public static void main(String[] args) {

    new SocketHttpClient().start("www.baidu.com", 80);

    }

    }

    结果输出:

    a406cfd408fe6ced5d3161e1504f46d8.png

    请求模型优化

    这种方式,虽然实现功能没什么问题。但是我们细看,发现在 IO 写入与读取过程,是发生了 IO 阻塞的情况。即:

    // 会发生 IO 阻塞

    writer.write(HttpUtil.compositeRequest(host));

    reader.readLine();

    所以如果要同时请求10个不同的站点,如下:

    public class SingleThreadApplication {

    public static void main(String[] args) {

    // HttpConstant.HOSTS 为 站点集合

    for (String host: HttpConstant.HOSTS) {

    new SocketHttpClient().start(host, HttpConstant.PORT);

    }

    }

    }

    它一定是第一个请求响应结束后,才会发起下一个站点处理。

    这在服务端更明显,虽然这里的代码是客户端连接,但是具体的操作和服务端是差不多的。请求只能一个个串行处理,这在响应时间上肯定不能达标。

    多线程处理

    有人觉得这根本不是问题,JAVA 是多线程的编程语言。对于这种情况,采用多线程的模型再合适不过。

    public class MultiThreadApplication {

    public static void main(String[] args) {

    for (final String host: HttpConstant.HOSTS) {

    Thread t = new Thread(new Runnable() {

    public void run() {

    new SocketHttpClient().start(host, HttpConstant.PORT);

    }

    });

    t.start();

    }

    }

    }

    这种方式起初看起来挺有用的,但并发量一大,应用会起很多的线程。都知道,在服务器上,每一个线程实际都会占据一个文件句柄。而服务器上的句柄数是有限的,而且大量的线程,造成的线程间切换的消耗也会相当的大。所以这种方式在并发量大的场景下,一定是承载不住的。

    多线程 + 线程池 处理

    既然线程太多不行,那我们控制一下线程创建的数目不就行了。只启动固定的线程数来进行 socket 处理,既利用了多线程的处理,又控制了系统的资源消耗。

    public class ThreadPoolApplication {

    public static void main(String[] args) {

    ExecutorService executorService = Executors.newFixedThreadPool(8);

    for (final String host: HttpConstant.HOSTS) {

    Thread t = new Thread(new Runnable() {

    public void run() {

    new SocketHttpClient().start(host, HttpConstant.PORT);

    }

    });

    executorService.submit(t);

    new SocketHttpClient().start(host, HttpConstant.PORT);

    }

    }

    }

    关于启动的线程数,一般 CPU 密集型会设置在 N+1(N为CPU核数),IO 密集型设置在 2N + 1。

    这种方式,看起来是最优的了。那有没有更好的呢,如果一个线程能同时处理多个 socket 连接,并且在每个 socket 输入输出数据没有准备好的情况下,不进行阻塞,那是不是更优呢。这种技术叫做“IO多路复用”。在 JAVA 的 nio 包中,提供了相应的实现。

    后续

    JAVA 中是如何实现 IO多路复用

    Netty 下的实现异步请求的

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

    展开全文
  • Java socket通信在不断的进行相关代码的开发,下面我们就看看如何才能更好的使用有关技术为我们的编程工作带来一定的帮助。Java socket通信Java语言中是一个使用很广泛的工具,下面我们就来仔细的学习下有关的方法...
  • 在分布式架构中,网络通信底层基础,没有网络,也就没有所谓的分布式架构。只有通过网络才能使得一大片机器互相协作,共同完成一件事情。 需要框架源码的朋友可以看我个人简介联系我。推荐分布式架构源码 同样...
  • java分布式之rmi实例教程网络通信原理 Java分布式之RMI实例教程 网络通信原理 前言 最近的联通项目,下一阶段可能会涉及到和各省间的 RMI 接口,所以总结一下 08 年中国 移动自动拨测系统用到的 RMI 技术,以备...
  • 学过网络的同学可以把它理解为基于传输TCP/IP协议的进一步封装,封装到以至于我们从表面上使用就像对文件流一样的打开、读写和关闭等操作。此外,它是面向应用程序的,应用程序可以通过它发送或接收数据而不用过多的...
  • Java基础】Java 网络通信基础 技术详解
  • java底层通信--Socket

    2020-12-22 19:30:04
    以前一直不太重视java 基础的整理,感觉在实际开发中好像java 基础用处不大,感觉不理解一些底层的东西对开发工作影响也不大。不过,后来我发现,很多东西都是相互联系的,如果底层的东西你不理解,后面的很多与之有...
  • 对于使用过java远程调试的老手来说,有没有想过它的底层是怎么实现的呢?今天这篇文章就来揭秘(程序员应该了解自己每天使用的工具,磨炼自己的技艺) 1. Java远程调试基本操作 Java进程默认不支持远程调试,如果...
  • Java I/O是Java基础之一,在面试中也比较常见,在这里我们尝试通过这篇文章阐述Java I/O的基础概念,帮助大家更好的理解...后来查了看了好多,才明白Java I/O的原理是以Linux网络I/O模型为基础的,理解了Linux网络I...
  • 本文介绍操作系统I/O工作原理Java I/O设计,基本使用,开源项目中实现高性能I/O常见方法和实现,彻底搞懂高性能I/O之道基础概念在介绍I/O原理之前,先重温几个基础概念:(1) 操作系统与内核操作系统:管理计算机...
  • 网络 (Network)、互联网 (internet)、因特网 (Internet) ISP(互联网服务提供商) 网络分类(局域网、城域网、广域网) 常见接口(FastEthernet、GigabitEthernet、Serial) 上网方式(电话线入户、光纤入...
  • 既然涉及到网络通信,就不得不说一下多线程,同步异步相关的知识了.Netty的网络模型是多线程的Reactor模式,所有I/O请求都是异步调用,我们今天就来探讨一下一些基础概念和Java NIO的底层机制.为了节约你的时间,...
  • 首先我们来看一下当访问一个域名时它的过程查找 DNS首先,浏览器... 网络层(IP数据包)-> 数据链路层(协议单元)如果发送时 ARP 缓存中没有相关数据,则发送 ARP 广播,等待 ARP 回应ARP 回应后,将 IP 地址与下一...
  • 在分布式架构中,网络通信底层基础,没有网络,也就没有所谓的...可能大家已经忘记了网络通信的重要性,本篇文章会详细分析网络通信底层原理!! 1.1 理解通信的本质 如图1-1所示,当我们通过浏览器访问一个网址时
  • 一、前言:TCP原理简介首先,保证文章完整性,TCP的理论原理还是需要简介一下,略显枯燥๑乛◡乛๑。TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP...
  • 众多大厂在招聘的时候,不仅会要求面试者能简单地使用Redis,还要能深入地理解底层实现原理,并且具备解决常见问题的能力。可以说,熟练掌握Redis已经成为了技术人的一个必备技能。 但是,在学习和使用Redis的过程中...
  • 新的producer和所有的服务器网络通信都是异步地,在ack=-1模式下需要等待所有的replica副本完成复制时,可以大幅减少等待时间。 kafka消息消费模式 Kafka选取了传统的pull拉取模式; 推送模式 push 由broker主动推送...
  • Socket通信原理

    2021-05-23 03:33:12
    原标题:Socket通信原理一、Socket通信简介Android与服务器的通信方式主要有两种:Http通信Socket通信两者的最大差异在于:Http连接使用的是“请求-响应方式”,即在请求时建立连接通道,当客户端向服务器发送请求后...
  • 0引言通信原理是电子信息和通信工程类专业重要的专业基础课,理论和实践性都很强,不但需要掌握和理解基本的概念,还需要通过课程实验来强化所学理论。为配合通信原理的教学活动,各高校在该课程实验设置和投入方面花费...
  • 上一篇文章 《漫谈socket-io的基本原理》 用了现实非常浅显的例子,尽可能地阐释非阻塞、阻塞、多线程、多路复用poll和 epoll 背后演进的整体思考脉络,将有助于读者从宏观的角度把握住socket-io的本质。 本文将聚焦...
  • Java网络编程Socket原理

    2021-01-11 21:34:33
    Java网络编程之底层Linux的Socket网络套接字 用户态和内核态 用户态和内核态主要是基于操作系统Linux来说的。 内核态:内核态其实可以直接理解为内核,主要是控制CPU或者磁盘、网卡等硬件设备这些资源。 用户态:...
  • 本文将介绍 Java Web 服务器是如何运行的, Web 服务器也称为超文本传输协议( HyperText Transfer Protocol, HTTP)服务器, 因为它使用 Http 与其客户端(通常是 Web 浏览器)进行通信, 基于 Java 的 Web 服务器会使用两...
  • 如下图: Binder IPC 原理 四、Binder 通信模型 介绍完 Binder IPC 的底层通信原理,接下来我们看看实现层面是如何设计的。 一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方分别为客户端进程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 54,703
精华内容 21,881
关键字:

java底层网络通信原理

java 订阅