Netty|Java I/O 模型之 BIO

??个人主页:水滴V2
支持水滴:点赞、收藏?、留言
大家好,我是水滴~~

文章目录
    • Java BIO的概念
    • Java API解析
    • 服务器端API
      • 创建服务端套接字
      • 绑定本地套接字地址
      • 监听客户端连接
      • 读取客户端数据
      • 向客户端写入数据
    • 客户端API
      • 创建客户端套接字
      • 连接服务器端
      • 读/写数据
    • 示例代码
      • 服务端示例代码
      • 客户端示例代码

Java BIO的概念 Java BIO(Blocking I/O)是一种同步阻塞式I/O模型,从JDK1.0到JDK1.3,Java只能使用BIO实现Socket通信。
在BIO通信模型中,有一个独立的Acceptor线程,来负责监听客户端的连接,它每接收到一个客户端连接,都会为之创建一个新的线程,新线程专门负责处理与之对应客户端的通信。
Netty|Java I/O 模型之 BIO
文章图片

Java API解析 在Java BIO中,服务端ServerSocket负责绑定IP地址和端口,并启动端口监听;而客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
下面,我们通过一段示例代码,来了解服务端和客户端的API。
服务器端API 服务器端是为客户端服务的,若想要客户端知道它的存在,必须向网络中公开出一个套接字地址(IP地址+端口),也就是网络中的一个端点,我们称该端点为服务端套接字(或者叫监听套接字)。
创建服务端套接字
Java BIO中使用ServerSocket类创建一个服务器端套接字。下面示例代码中,创建了一个服务器端套接字实例。此时的套接字还未绑定本地IP和端口,客户端还无法连接。
// 为服务端创建 ServerSocket,并指定端口 ServerSocket serverSocket = new ServerSocket();

绑定本地套接字地址
这里要先说一下InetSocketAddress类,从类名也可以看出,这是一个网络套接字地址(IP地址+端口号)类。该类在实例化时,可以指定具体的IP地址和端口号;也可以只指定端口号,那么其IP地址将是一个通配符地址。
通过bind方法来绑定一个本地套接字地址,并启动了监听。下面示例代码中,只指定了8080端口,未指定IP地址,若服务端有多个IP地址,那么每一个IP都可以被连接。
服务端套接字绑定了本地端口后,实际客户端就已经可以连接了。你可以通过telnet命令连接试一下。
serverSocket.bind(new InetSocketAddress(8080));

监听客户端连接
通过accept方法来监听客户端的连接。该方法是一个阻塞方法,直到有一个客户端连接成功后,该方法才会返回该连接的套接字对象。也就是说,每连接一个客户端,都会生成一个与之对应的套接字对象。
该套接字与客户端的套接字是成对出现的,它们都记录了该连接的四元组信息(源IP地址、源端口、目的IP地址、目的端口)。后面与客户端的I/O操作,都是通过此套接字来完成的。
// 监听,等待客户端连接。该方法会阻塞,直到建立连接。 Socket socket = serverSocket.accept();

读取客户端数据
通过套接字可以获取输入流,调用输入流的read方法来监听客户端的输入。该方法同样是一个阻塞方法,直到有客户端数据输入。
下面代码是从输入流中读取字节数据,并将其缓存到一个字节数组中。最后将字节数据转换为字符串,便是我们要的内容。
// 通过 socket 获取输入流 InputStream inputStream = socket.getInputStream(); // 创建缓冲区数组,用于临时存储发来的数据 byte[] bytes = new byte[1024]; // 从输入流中读取数据,并将它们存储到缓冲区数组中。该方法会阻塞,直到输入数据可用、检查到文件结束或抛出异常 int len = inputStream.read(bytes); if (len != -1) { // 将字节数组转换为文本内容 String content = new String(bytes, 0, len); }

向客户端写入数据
通过套接字获取输出流,通过调用write方法,来向客户端写入数据。写入的数据是一个字节数组,所以我们要将字符串内容转换为字节数组。
// 获取套接字的输入流 OutputStream outputStream = socket.getOutputStream(); // 向输出流写入内容 outputStream.write("收到".getBytes());

客户端API 服务端公开了套接字地址后,客户端便可以连接了。
创建客户端套接字
客户端想要连接服务端,也必须创建一个套接字,我们称之为客户端套接字。下面代码创建了一个未连接的客户端套接字。
// 创建 Socket Socket socket = new Socket();

连接服务器端
通过connect方法来连接服务端,该方法的入参是一个服务端套接字地址(必须指定IP和端口)。该方法是一个阻塞方法,直到连接成功(可以设置超时时间)。
客户端的套接字,同样也有四元组信息,一旦连接成功,会随机分配一个未使用的端口。
// 连接远程端点 socket.connect(new InetSocketAddress("127.0.0.1", 8080));

读/写数据
客户端套接字连接成功,便可以与服务端进行读/写操作了,该操作与服务端相同,就不再赘述了。
示例代码 服务端示例代码
accept方法用来监听客户端连接,为了让所有客户端都能连接,在主线程内使用一个while循环,来依次接收所有连接。
由于read是一个阻塞方法,为了不影响其他客户端的连接,需要为每一个连接创建一个独立的线程,来处理客户端读取操作。
import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; /** * @author 码农StayUp * @date 2021/4/6 0006 */ public class BioServer {private static final int PORT = 8080; public static void main(String[] args) throws IOException {// 为服务端创建 ServerSocket ServerSocket serverSocket = new ServerSocket(); // 绑定本地端口 serverSocket.bind(new InetSocketAddress(PORT)); System.out.printf("[%s] - 服务端启动了,端口为:%s\n", Thread.currentThread().getName(), PORT); // 循环接收每一个客户端连接,当没有连接时会阻塞 while (true) { // 监听,等待客户端连接。该方法会阻塞,直到建立连接。 Socket socket = serverSocket.accept(); System.out.printf("[%s] - 有一个客户端连上来了 - %s\n", Thread.currentThread().getName(), socket.getRemoteSocketAddress()); // 为连接创建一个独立的线程,进行接收数据 new Thread(() -> socketHandler(socket)).start(); } }/** * 处理 socket 连接 * * @param socket */ private static void socketHandler(Socket socket) { try { // 创建缓冲区数组,用于临时存储客户端发来的数据 byte[] bytes = new byte[1024]; // 通过 socket 获取输入流 InputStream inputStream = socket.getInputStream(); // 循环接消息,直到连接关闭 while (true) { // 从输入流中读取数据,并将它们存储到缓冲区数组中。该方法会阻塞,直到输入数据可用、检查到文件结束或抛出异常 int len = inputStream.read(bytes); if (len != -1) { String content = new String(bytes, 0, len); System.out.printf("[%s] - 接收客户端发来的内容:%s\n", Thread.currentThread().getName(), content); } else { System.out.printf("[%s] - 连接关闭...\n", Thread.currentThread().getName()); break; } } } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }

客户端示例代码
【Netty|Java I/O 模型之 BIO】客户端代码相对比较简单,连接成功后进行通信即可。
import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.Scanner; /** * @author 码农StayUp * @date 2021/4/6 0006 */ public class BioClient {private static final String HOST = "127.0.0.1"; private static final int PORT = 8080; public static void main(String[] args) throws IOException {// 创建 Socket Socket socket = new Socket(); // 连接远程端点 socket.connect(new InetSocketAddress(HOST, PORT)); System.out.printf("连接成功,主机:%s,端口:%s\n", HOST, PORT); // 获取输出流 OutputStream outputStream = socket.getOutputStream(); // 获取控制输入内容 Scanner scanner = new Scanner(System.in); while (true) { System.out.print("请输入:"); String content = scanner.nextLine(); if ("quit".equals(content)) { break; } // 将控制台内容写入输出流 outputStream.write(content.getBytes()); } outputStream.close(); scanner.close(); socket.close(); } }

    推荐阅读