3_分布式架构的基石-tcp通信协议

输入一个网址后的过程

  1. DNS域名解析系统对输入的网址解析.
    • 域名解析系统本质是个数据服务器, 里边存储了域名和IP的的对应关系.
    • 通过解析过程, 最后会得到一个IP地址, 通过该IP地址, 就能访问服务器了.
    • 解析过程
      • 浏览器先检查自身缓存有没有这个域名对应的ip, 有则解析结束.
      • 若浏览器缓存中没有, 会检查操作系统缓存中有没有对应的已解析过的结果.
      • 若还没名中域名, 会请求本地域名服务器(LDNS)解析.
      • 若LDNS仍未命中, 就直接跳到根域名服务器请求解析.
      • 根域名服务器会返回给LDNS一个所查询域的主域名服务器(顶级域名服务器, 如.com, .org)地址.
      • 此时LDNS会再发送请求给上一步返回的主域名服务器(gTLD).
      • 接受请求的gTLD查找并返回这个域名对应的(网站注册的域名服务器name server)的地址.
      • name server根据映射关系找到目标IP, 返回给LDNS.
      • LDNS缓存这个域名和对应的IP.
      • LDNS把解析的结果返回给用户, 用户缓存到本地系统缓存中, 结束!
  2. 建立TCP连接
    • 客户端通过三次握手与服务器建立TCP安全连接, 为之后的HTTP请求响应做准备.
  3. 客户端发送给HTTP请求, 服务端响应请求(同时传回页面).
  4. 浏览器接收到服务器资源后, 解析渲染成web页面.
  5. 连接结束, 通过四次分手断开连接.

了解TCP/IP协议

  1. 什么是协议: 协议相当于两个需要通过网络通信的程序达成的一种约定, 它规定了报文的交换方式和包含的意义.
    • 比如(HTTP)为了解决在服务器之间传递超文本对象的问题, 这些超文本对象在服务器中创建和存储, 并由Web浏览器进行可视化, 完成用户对远程内容的感知和体验.
  2. 什么是IP协议
    1. IP协议提供了一组数据报文服务, 每组分组报文都是由网络独立处理和分发, 每个IP报文必须包含一个目的地址的字段.
    2. 就像寄送快递包裹一样, 为了实现这个功能, 就像我们寄送快递都需要写明收件人信息, 但是和我们寄送快递一样, 也可能会出现包裹丢失问题, 所以IP协议只是一个“尽力而为”的协议.
    3. 在网络传输过程中, 可能会发生报文丢失, 报文顺序打乱, 重复发送的情况. IP协议层之上的传输层, 提供了两种可以选择的协议, TCP, UPD. 这两种协议都是建立在IP层所提供的服务基础上, 根据应用程序的不同需求选择不同方式的传输.
  3. TCP/IP
    1. TCP协议能够检测和恢复IP层提供的主机到主机的通信中可能发生的报文丢失, 重复及其他错误. 
    2. TCP提供了一 个可信赖的字节流通道, 这样应用程序就不需要考虑这些问题.
    3. 同时, TCP协议是一种面向连接的协议, 在使用TCP进行通信之前, 两个应用程序之间需要建立一个TCP连接, 而这个连接又涉及到两台电脑需要完成握手消息的交换.
  4. UDP/IP
    1. UDP协议不会对IP层产生的错误进行修复, 而是简单的扩展了IP协议“尽力而为”的数据报文服务, 使它能够在应用程序之间工作, 而不是在主机之间工作, 因此使用UDP协议必须要考虑到报文丢失, 顺序混乱的问题.

使用协议进行通信

  1. 什么是socket: 它是计算机之间进行通信的一种约定或一种方式. 通过socket这种约定, 一台计算机可以接收其他计算机的数据, 也可以向其他计算机发送数据.
    • 不同类型的socket和底层协议有关.
  2. stream socket: 实现tcp连接
    //Server
        public static void main(String[] args) {
    
            ServerSocket serverSocket = null;
            BufferedReader bufferedReader = null;
    
            try {
                serverSocket = new ServerSocket(8080);
                Socket socket = serverSocket.accept(); //等待客户端连接
                //获得输入流
                bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                System.out.println(bufferedReader.readLine());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (serverSocket != null) {
                    try {
                        serverSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    //Client
        public static void main(String[] args) {
            Socket socket = null;
            try {
                socket = new Socket("127.0.0.1", 8080);
                PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                out.println("Hello");
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
  3. datagram socket: 实现udp连接
    //Server
        public static void main(String[] args) {
            //创建服务, 并且接收一个数据包
            DatagramSocket datagramSocket =  null;
            byte[] receiveData = new byte[1024];
            try {
                datagramSocket = new DatagramSocket(8080);
                DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
                datagramSocket.receive(receivePacket);
                System.out.println(new String(receiveData, 0, receivePacket.getLength()));
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(datagramSocket != null) {
                    datagramSocket.close();
                }
            }
    
        }
    
    //Client
        public static void main(String[] args){
            InetAddress address = null;
            byte[] sendData = "Hello".getBytes();
            try {
                address = InetAddress.getByName("localhost");
                DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, 8080);
                DatagramSocket datagramSocket = new DatagramSocket();
                datagramSocket.send(sendPacket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

通信的性能问题

  • 传统的通信(BIO)
    1. 我们发现TCP响应服务器一次只能处理一个客户端请求, 当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时, 虽然在连接建立后可以向服务端发送数据, 但是在服务端处理完之前的请求之前, 却不会对新的客户端做出响应, 这种类型的服务器称为"迭代服务器".
    2. 迭代服务器是按照顺序处理客户端请求, 也就是服务端必须要处理完前一个请求才能对下一个客户端的请求进行响应.
    3. 但是在实际应用中, 我们不能接受这样的处理方式, 所以我们需要一种方法可以独立处理每一个连接, 并且不会相互干扰. 而Java的多线程技术刚好满足这个需求, 这个机制可以让服务器能方便处理多个客户端的请求.
  • 如何提高通信性能
    1. 对于TCP通信来说, 每个TCP Socket的内核中都有一个发送缓冲区和一个接收缓冲区, TCP的全双工的工作模式及TCP的滑动窗口就是依赖于这两个独立的Buffer和该Buffer的填充状态.
    2. 接收缓冲区把数据缓存到内核, 若应用进程一直没有调用Socket的read()进行读取, 那么该数据会一直被缓存在接收缓冲区内. 不管进程是否读取Socket, 对端发来的数据都会经过内核接收并缓存到Socket的内核接收缓冲区.
    3. read所要做的工作, 就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里.
    4. 进程调用Socket的send()发送数据的时候, 一般情况下是将数据从应用层用户的Buffer里复制到Socket的内核发送缓冲区, 然后send就会在上层返回. 换句话说, send返回时, 数据不一定会被发送到对端.
    5. Socket的接收缓冲区被TCP用来缓存网络上收到的数据, 一直保存到应用进程读走为止. 如果应用进程一直没有读取, 那么Buffer满了以后, 出现的情况是: 通知对端TCP协议中的窗口关闭, 保证TCP接收缓冲区不会移除, 保证了TCP是可靠传输的. 如果对方无视窗口大小发出了超过窗口大小的数据, 那么接收方会把这些数据丢弃.
  • 如何使用非阻塞提高性能?
    1. 我们前面提到的方式依然是阻塞IO, 而非阻塞要解决的就是I/O线程与Socket解耦的问题, 因此, 它引入了事件机制来达到解耦的目的.
    2. 我们可以认为NIO底层中存在一个I/O调度线程, 它不断的扫描每个Socket的缓冲区, 当发现写入缓冲区为空的时候, 它会产生一个Socket可写事件, 此时程序就可以把数据写入到Socket中, 如果一次写不完, 就等待下一次的可写事件通知. 反之, 当发现缓冲区里有数据的时候, 它会产生一个Socket可读事件, 程序收到这个通知事件就可以从Socket读取数据了.

关于NIO

  1. 实际上基于上面讲的传统的BIO模型, 一个请求一个线程的方式, 如果涉及到上千个客户端访问时, 会产生很多问题, 如扩展性, 系统资源开销等. 所以我们需要一种方法来轮询一组客户端, 来查找哪个连接需要提供服务, 这就是NIO
  2. 缓冲区
    • 在NIO中, 所有数据都是用缓冲区处理, 在读取数据的时候, 它是直接读到缓冲区中, 在写入数据的时候, 也是写到缓冲区. 任何时候访问NIO中的数据, 都是通过缓冲区进行的操作.
  3. 通道
    • Channel通道, 就像一个自来水管一样, 可以通过它读取和写入数据, Channel是全双工的, 所以数据是双向流动.
  4. 多路复用
    1. 多路复用器Selector, 是NIO的基础, 多路复用器提供选择已经就绪的任务的能力, 简单来说, Selector会不断轮询注册上的Channel, 如果某个Channel上面有新的TCP连接接入, 读, 写事件, 这个Channel就处于就绪状态, 会被Selector轮询出来, 然后通过SelectionKey可以获取就绪的Channel进行I/O操作;
    2. 一个多路复用器可以同时轮询多个Channel, 通过这个机制可以接入成千上万的客户端.
  5. NIO的优缺点
    • 优点: 无论多少个客户端请求, 都只需要在一个线程轮询注册在多路复用器上的Channel.
    • 缺点: 同步, 复杂.

组播协议Multicast

  1. 对于某些信息, 多个接受者都可能感兴趣的时候, 那么我们应该怎么解决呢?
    • 我们可以向每个接受者单播一个数据副本, 但是如果这样的话, 效率会低. 而且同样的数据发送多次, 浪费带宽.
  2. 解决方案: 我们把复制数据包的工作交给网络来做, 而不是由发送者负责.
    1. 有两种发送类型: 广播/多播
    2. 广播: 网络中的所有主机都会接收到一份数据副本.
    3. 多播: 消息只发送给一个多播地址, 网络只是将数据分发给哪些想要接收发送到该多播地址的数据的主机.
    4. 总的来说, 要实现该功能, UDP最合适.
  3. 广播
    • 广播是主机向子网内所有主机发送消息, 子网内所有主机都能收到来自某台主机的广播信息, 属于点对所有点通信. 广播意味着网络向子网每一个主机都投递一份数据包, 不论这些主机是否乐意接收该数据包.
  4. 多播
    • 多播是主机向一组主机发送信息, 存在某个组内的所有主机都可以接收到消息, 属于点对多点通信.
posted @ 2020-05-20 16:15  yellowstreak  阅读(245)  评论(0编辑  收藏  举报