Java 原生网络编程-BIO

常见术语

  • Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。在设计模式 中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。
  • 主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层 TCP/IP 协议来建立 TCP 连接。建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UDP 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个 主机上的一个应用程序的通信链路。

短连接:

  • 连接->传输数据->关闭连接
  • 传统 HTTP 是无状态的,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,但任务结束就中断连接。
  • 短连接是指 SOCKET 连接、发送、接收完数据后马上断开连接。

长连接:

  • 连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。
  • 长连接指建立 SOCKET 连接后不管是否使用都保持连接。

什么时候用长连接,短连接?

  • 长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个 TCP 连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就 OK 了,不用建立 TCP 连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成 socket 错误,而且频繁的 socket 创建也是对资源的浪费。
  • 而像 WEB 网站的 http 服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像 WEB 网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源。

通用常识

  • 在通信编程里提供服务的叫服务端,连接服务端使用服务的叫客户端。在开发过程中, 如果类的名字有 Server 或者 ServerSocket 的,表示这个类是给服务端容纳网络服务用的,如果类的名字只有 Socket 的,那么表示这是负责具体的网络读写的。那么对于服务端来说 ServerSocket 就只是个场所,具体和客户端沟通的还是一个一个的 socket,所以在通信编程里,ServerSocket 并不负责具体的网络读写,ServerSocket 就只是负责接收客户端连接后, 新启一个 socket 来和客户端进行沟通。这一点对所有模式的通信编程都是适用的。
  • 在通信编程里,我们关注的其实也就是三个事情:连接(客户端连接服务器,服务器等待和接收连接)、读网络数据、写网络数据,所有模式的通信编程都是围绕着这三件事情进行的。服务端提供 IP 和监听端口,客户端通过连接操作向服务端监听的地址发起连接请求, 通过三次握手连接,如果连接成功建立,双方就可以通过 Socket 进行通信。

InetAddress 类

  • java.net.InetAddress 类是 Java 对 IP 地址(包括 IPv4 和 IPv6) 的高层表示。大多数其他网络类都要用到这个类,包括 Socket、ServerSocket、URL、DatagramSocket,DatagramPacket 等。一般地讲,它包括一个主机名和一个 IP 地址。

常用方法

  • InetAddress 类没有公共构造函数。实际上,InetAddress 有一些静态工厂方法,可以连 接到 DNS 服务器来解析主机名。最常用的是 InetAddress.getByName()。
InetAddress address = InetAddress.getByName("www.baidu.com");

在这里插入图片描述

  • 这个方法并不只是设置 InetAddress 类中的一个私有 Sstring 字段。实际上它会建立与本 地 DNS 服务器的一个连接,来查找名字和数字地址(如果你之前查找过这个主机,这个信息可能会在本地缓存,如果是这样,就不需要再建立网络连接)。如果 DNS 服务器找不到这个地址,这个方法会抛出一个 UnknownHostException 异常,这是 IOException 的一个子类。
  • 还可以按 IP 地址反向查找。例如,如果希望得到地址 180.101.49.12 的主机名,可以向 InetAddress.getByName(()传入一个点分四段地址:
InetAddress address2 = InetAddress.getByName("180.101.49.12");

在这里插入图片描述

  • 如果你查找的地址没有相应的主机名,getHostName()就会返回你提供的点分四段地址。
  • 很多的网站实际上有多个地址。如果出于某种原因你需要得到一个主机的所有地址,可以调用 getAllByName(),它会返回一个数组:
InetAddress[] allByName = InetAddress.getAllByName("www.baidu.com");

在这里插入图片描述

  • 如果你知道一个数字地址,可以由这个地址创建一个 InetAddress 对象,而不必使用 InetAddress.getByAddress()与 DNS 交互。这个方法可以为不存在或者无法解析的主机创建地址:
InetAddress byAddress = InetAddress.getByAddress(bytes);

在这里插入图片描述

  • InetAddress 类有两个 isReachable()方法,可以测试一个特定节点对当前主机是否可达(也就是说,能否建立一个网络连接)。连接可能由于很多原因而阻塞,包括防火墙、代理服务 器、行为失常的路由器和断开的线缆等,或者只是因为试图连接时远程主机没有开机。
address.isReachable(100);

在这里插入图片描述

Networklnterface 类

  • 由于 NetworkInterface 对象表示物理硬件和虚拟地址网络接口,所以不能任意构造。与 InetAddress 类一样,有一些静态工厂方法可以返回与某个网络接口关联的 NetworkInterface 对象。可以通过 IP 地址、名字或枚举来请求一个 NetworkInterface。
 InetAddress address = InetAddress.getByName("127.0.0.1");
        NetworkInterface byInetAddress = NetworkInterface.getByInetAddress(address);

Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
  • 名字的格式与平台有关。在典型的 UNIX 系统上,以太网接口名的形式为 eth0、eth1 等。 本地回送地址的名字可能类似于“lo”。在 Windows 上,名字是类似“CE31”和“ELX100” 的字符串,取自这个特定网络接口的厂商名和硬件模型名。
package org.example.netBase;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;

public class UseNet {
    
    
    public static void main(String[] args) throws UnknownHostException, SocketException {
    
    
        InetAddress address = InetAddress.getByName("127.0.0.1");
        NetworkInterface byInetAddress = NetworkInterface.getByInetAddress(address);
        System.out.println(byInetAddress);
        System.out.println("*****************************");

        Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
        while(networkInterfaces.hasMoreElements()){
    
    
            NetworkInterface networkInterface = networkInterfaces.nextElement();
            System.out.println(networkInterface);
            Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
            while(inetAddresses.hasMoreElements()){
    
    
                System.out.println(networkInterface.getDisplayName()+"-"+inetAddresses.nextElement());
            }
            System.out.println("=======================华丽的分割线========================");
        }

    }
}

在这里插入图片描述

原生 JDK 网络编程 BIO

  • 传统的同步阻塞模型开发中,ServerSocket 负责绑定 IP 地址,启动监听端口;Socket 负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
  • 传统 BIO 通信模型:采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负 责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答模型,同时数据的读取写入也必须阻塞在一个线程内等待其完成。
  • 该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈 1:1 的正比关系,Java 中的线程也是比较宝贵的系统资源,随着请求数的增加,创建的线程数量越多,系统的性能下降的越快,随着访问量的继续增大,系统最终就死掉了。
  • 为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程,实现 1 个或 多个线程处理 N 个客户端的模型(但是底层还是使用的同步阻塞 I/O),通常被称为“伪异 步 I/O 模型“。
  • 如果使用 CachedThreadPool 线程池(不限制线程数量),其实除了能自动帮我们管理线程(复用),看起来也就像是 1:1 的客户端:线程数模型,而使用 FixedThreadPool 我们就有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了 N:M 的伪异步 I/O 模型。
  • 但是,因为限制了线程数量,如果发生读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。
  • 服务端
package org.example.bio;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ServerPool {
    
    

    private static ExecutorService executorService =
            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public static void main(String[] args) throws IOException {
    
    
        //服务端启动必备
        ServerSocket serverSocket = new ServerSocket();
        //绑定监听的端口
        serverSocket.bind(new InetSocketAddress(9001));
        System.out.println("Server is started ...");
        try {
    
    
            while (true) {
    
    
                executorService.execute(new ServerTask(serverSocket.accept()));
            }
        } finally {
    
    
            serverSocket.close();
        }
    }

    public static class ServerTask implements Runnable {
    
    
        Socket socket = null;

        public ServerTask(Socket socket) {
    
    
            this.socket = socket;
        }

        @Override
        public void run() {
    
    
            System.out.println("connect success ...");
            //实例化与客户端通信的输入输出流,先输入再输出
            try(ObjectInputStream inputStream=new ObjectInputStream(socket.getInputStream());
                ObjectOutputStream outputStream=new ObjectOutputStream(socket.getOutputStream())){
    
    
                //接收客户端的输出,也就是服务端的输入
                System.out.println("wait message ...");
                String readUTF = inputStream.readUTF();
                System.out.println("receive :" + readUTF);
                //服务端输出,也就是客户端输入
                outputStream.writeUTF("Hello, " + readUTF);
                outputStream.flush();

            }catch (Exception e){
    
    
                e.printStackTrace();
            }finally {
    
    
                if (socket != null) {
    
    
                    try {
    
    
                        socket.close();
                    } catch (IOException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

  • 客户端
package org.example.bio;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    
    
    public static void main(String[] args) throws IOException {
    
    
        //客户端必备
        Socket socket = new Socket();
        //服务端的通信地址
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9001);
        //连接服务器
        socket.connect(inetSocketAddress);
        //实例化与服务端通信的输入输出流,先输出再输入
        try(ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
        ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())) {
    
    
            //向服务端输出
            System.out.print("请输入需要发送的消息:");
            Scanner scanner = new Scanner(System.in);
            outputStream.writeUTF(scanner.next());
            outputStream.flush();
            //接收服务端的输出
            System.out.println(inputStream.readUTF());
        } finally {
    
    
            if (socket != null) {
    
    
                socket.close();
            }
        }
    }
}

  • 先启动服务端,正在等待连接

在这里插入图片描述

  • 启动客户端,等待客户端输入要发送的消息

在这里插入图片描述

  • 再看服务端,客户端启动后,服务端连接客户端成功,等待客户端发送消息

在这里插入图片描述

  • 客户端输入要发送的消息,回车后接收到服务端返回过来的消息

在这里插入图片描述

  • 再看服务端,接收到了客户端的消息

在这里插入图片描述

  • 通过上面的示例可以看出serverSocket.accept()和inputStream.readUTF()都是阻塞方法,如果服务端没有和客户端连接,或没有接收到客户端的消息,服务端将一直等待。这里使用了线程池,可以同时接收4个请求(通过Runtime.getRuntime().availableProcessors()得到本机的并发数),多出来的请求会进入队列等待前面的请求释放连接,获得cpu资源。

猜你喜欢

转载自blog.csdn.net/qq_40977118/article/details/109315558