今日内容

  • 网络编程三要素
  • TCP通信
  • 文件上传
  • 模拟B/S
  • NIO

教学目标

  • 能够辨别UDP和TCP协议特点
  • 能够说出TCP协议下两个常用类名称
  • 能够编写TCP协议下字符串数据传输程序
  • 能够理解TCP协议下文件上传案例
  • 能够理解TCP协议下BS案例
  • 能够说出NIO的优点

第一章 网络编程入门

知识点—软件结构

目标

  • 了解软件结构

路径

  • C/S结构
  • B/S结构

讲解

  • C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
  • 特点: 客户端和服务器是分开的,需要下载客户端,对网络要求相对低, 开发和维护成本高,相对稳定

day14【网络编程和NIO】 - 图1

B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。

特点:没有客户端,只有服务器,不需要下载客户端,直接通过浏览器访问, 对网络要求相对高, 开发和维护成本低,服务器压力很大,相对不稳定

day14【网络编程和NIO】 - 图2

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。

小结

  • 网络编程,就是在一定的协议下,实现两台计算机的通信的程序。

知识点—网络编程三要素

目标

  • 理解网络编程三要素

路径

  • 协议
  • IP地址
  • 端口号

讲解

协议

网络通信协议:通信协议是计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
  • TCP协议特点: 面向连接,传输数据安全,传输速度低
  • 例如: 村长发现张三家的牛丢了
  • TCP协议: 村长一定要找到张三,面对面的告诉他他家的牛丢了 打电话: 电话一定要接通,并且是张三接的

    • 连接三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

      • 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
      • 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
      • 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。

day14【网络编程和NIO】 - 图3

  1. 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以**应用十分广泛,例如下载文件、浏览网页等**。
  • UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
  • UDP特点: 面向无连接,传输数据不安全,传输速度快
  • 例如: 村长发现张三家的牛丢了
  • UDP协议: 村长在村里的广播站广播一下张三家的牛丢了,信息丢失,信息发布速度快

IP地址

  • IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。

IP地址分类

  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。

  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。
    为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

常用命令

  • 查看本机IP地址,在控制台输入:
  1. ipconfig
  • 检查网络是否连通,在控制台输入:
  1. ping 空格 IP地址
  2. ping 220.181.57.216
  3. ping www.baidu.com

特殊的IP地址

  • 本机IP地址:127.0.0.1localhost

端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。

  • 端口号:用两个字节表示的整数,它的取值范围是01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。**

利用协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

小结

  • 协议: 计算机在网络中通信需要遵守的规则,常见的有TCP,UDP协议

    • TCP: 面向连接,传输数据安全,传输速度慢
    • UDP: 面向无连接,传输数据不安全,传输速度快
  • IP地址: 用来标示网络中的计算机设备

    • 分类: IPV4 IPV6
    • 本地ip地址: 127.0.0.1 localhost
  • 端口号: 用来标示计算机设备中的应用程序

    • 端口号: 0—65535
    • 自己写的程序指定的端口号要是1024以上
    • 如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

知识点—InetAddress类

目标

  • 能够通过InetAddress类获取ip地址

路径

  • InetAddress类的概述
  • InetAddress类的方法

讲解

InetAddress类的概述

  • 一个该类的对象就代表一个IP地址对象。

InetAddress类的方法

  • static InetAddress getLocalHost() 获得本地主机IP地址对象
  • static InetAddress getByName(String host) 根据IP地址字符串或主机名获得对应的IP地址对象
  • String getHostName();获得主机名
  • String getHostAddress();获得IP地址字符串
  1. public class InetAddressDemo01 {
  2. public static void main(String[] args) throws Exception {
  3. // 获取本地ip地址对象
  4. InetAddress ip1 = InetAddress.getLocalHost();
  5. System.out.println("ip1: "+ip1);// DESKTOP-U8Q5F96/192.168.0.100
  6. // 获取百度ip地址对象
  7. InetAddress ip2 = InetAddress.getByName("www.baidu.com");
  8. System.out.println("ip2:"+ip2);// www.baidu.com/182.61.200.7
  9. // 获得本地的主机名
  10. String hostName = ip1.getHostName();
  11. System.out.println("hostName:"+hostName);// DESKTOP-U8Q5F96
  12. // 获得本地的ip地址
  13. String hostAddress = ip1.getHostAddress();
  14. System.out.println("hostAddress:"+hostAddress);//192.168.0.100
  15. }
  16. }

小结

第二章 TCP通信程序

知识点—TCP协议概述

目标

  • 我们先来了解一下TCP协议使用时需要用到的流程和方法.

路径

  • TCP概述
  • TCP协议相关的类

讲解

TCP概述

  • TCP协议是面向连接的通信协议,即在传输数据前先在发送端和接收器端建立逻辑连接,然后再传输数据。它提供了两台计算机之间可靠无差错的数据传输。TCP通信过程如下图所示:

day14【网络编程和NIO】 - 图4

TCP协议相关的类

  • java.net.Socket : 一个该类的对象就代表一个客户端程序。

    • Socket(String host, int port) 根据ip地址字符串和端口号创建客户端Socket对象
  • 注意事项:只要执行该方法,就会立即连接指定的服务器程序,如果连接不成功,则会抛出异常。
    如果连接成功,则表示三次握手通过。
    • OutputStream getOutputStream(); 获得字节输出流对象
    • InputStream getInputStream();获得字节输入流对象
    • void close();关闭Socket, 会自动关闭相关的流
  • java.net.ServerSocket : 一个该类的对象就代表一个服务器端程序。

    • ServerSocket(int port); 根据指定的端口号开启服务器。
    • Socket accept(); 等待客户端连接并获得与客户端关联的Socket对象 如果没有客户端连接,该方法会一直阻塞
    • void close();关闭ServerSocket,一般不关闭

小结

  • TCP如何建立连接: 在客户端创建Socket对象,指定服务器ip地址和端口号,就会自动连接,如果连接成功,就表示三次握手成功,程序继续执行,如果连接失败,就会报异常
  • TCP如何传输数据: 使用Socket对象获得输出流写出数据,使用Socket对象获得输入流读取数据

实操—TCP通信案例1

需求

  • 客户端向服务器发送字符串数据

路径

  • 客户端实现步骤

    • 创建客户端Socket对象并指定服务器地址和端口号
    • 调用Socket对象的getOutputStream方法获得字节输出流对象
    • 使用字节输出流对象的write方法往服务器端输出数据
    • 关闭Socket对象断开连接。
  • 服务器实现步骤

    • 创建ServerSocket对象并指定端口号(相当于开启了一个服务器)
    • 调用ServerSocket对象的accept方法等待客端户连接并获得对应Socket对象
    • 调用Socket对象的getInputStream方法获得字节输入流对象
    • 调用字节输入流对象的read方法读取客户端发送的数据

实现

  • 客户端代码实现
    1. public class Client {
    2. public static void main(String[] args) throws Exception{
    3. // 创建Socket对象,指定服务器ip和端口号
    4. Socket socket = new Socket("127.0.0.1",6666);
    5. // 通过socket对象获得输出流
    6. OutputStream os = socket.getOutputStream();
    7. // 写出数据
    8. Scanner sc = new Scanner(System.in);
    9. String str = sc.nextLine();
    10. os.write(str.getBytes());
    11. // 关闭流,释放资源
    12. socket.close();
    13. }
    14. }
  • 服务端代码实现
    1. // 服务器
    2. public class Server {
    3. public static void main(String[] args) throws Exception{
    4. // 创建ServerSocket对象,并指定端口号
    5. ServerSocket ss = new ServerSocket(6666);
    6. // 调用accept()方法等待客户端连接,连接成功返回Socket对象
    7. Socket socket = ss.accept();
    8. // 通过Socket对象获得输入流
    9. InputStream is = socket.getInputStream();
    10. // 定义一个byte数组,用来存储读取到的字节数据
    11. byte[] bys = new byte[1024];
    12. int len = is.read(bys);
    13. // 打印数据
    14. System.out.println(new String(bys,0,len));
    15. // 关闭资源
    16. socket.close();
    17. ss.close();// 服务器一般不关闭
    18. }
    19. }

小结

实操—TCP通信案例2

需求

  • 客户端向服务器发送字符串数据,服务器回写字符串数据给客户端(模拟聊天)

路径

  • 客户端实现步骤

    • 创建客户端Socket对象并指定服务器地址和端口号
    • 调用Socket对象的getOutputStream方法获得字节输出流对象
    • 使用字节输出流对象的write方法往服务器端输出数据
    • 调用Socket对象的getInputStream方法获得字节输入流对象
    • 调用字节输入流对象的read方法读取服务器端返回的数据
    • 关闭Socket对象断开连接。
  • 服务器实现步骤

    • 创建ServerSocket对象并指定端口号(相当于开启了一个服务器)
    • 调用ServerSocket对象的accept方法等待客端户连接并获得对应Socket对象
    • 调用Socket对象的getInputStream方法获得字节输入流对象
    • 调用字节输入流对象的read方法读取客户端发送的数据
    • 调用Socket对象的getOutputStream方法获得字节输出流对象
    • 调用字节输出流对象的write方法往客户端输出数据
    • 关闭Socket和ServerSocket对象

实现

  • TCP客户端代码
  1. /*
  2. TCP客户端代码实现步骤
  3. * 创建客户端Socket对象并指定服务器地址和端口号
  4. * 调用Socket对象的getOutputStream方法获得字节输出流对象
  5. * 调用字节输出流对象的write方法往服务器端输出数据
  6. * 调用Socket对象的getInputStream方法获得字节输入流对象
  7. * 调用字节输入流对象的read方法读取服务器端返回的数据
  8. * 关闭Socket对象断开连接。
  9. */
  10. public class Client {
  11. public static void main(String[] args) throws Exception{
  12. // 创建Socket对象,指定服务器ip和端口号
  13. Socket socket = new Socket("127.0.0.1",6666);
  14. while (true) {
  15. // 通过socket对象获得输出流
  16. OutputStream os = socket.getOutputStream();
  17. // 写出数据
  18. Scanner sc = new Scanner(System.in);
  19. System.out.println("请输入向服务器发送的数据:");
  20. String str = sc.nextLine();
  21. os.write(str.getBytes());
  22. // 通过Socket对象获得输入流
  23. InputStream is = socket.getInputStream();
  24. // 定义一个byte数组,用来存储读取到的字节数据
  25. byte[] bys = new byte[1024];
  26. int len = is.read(bys);
  27. // 打印数据
  28. System.out.println(new String(bys,0,len));
  29. }
  30. // 关闭流,释放资源
  31. //socket.close();
  32. }
  33. }
  • 服务端代码实现
  1. /**
  2. TCP服务器端代码实现步骤
  3. * 创建ServerSocket对象并指定端口号(相当于开启了一个服务器)
  4. * 调用ServerSocket对象的accept方法等待客端户连接并获得对应Socket对象
  5. * 调用Socket对象的getInputStream方法获得字节输入流对象
  6. * 调用字节输入流对象的read方法读取客户端发送的数据
  7. * 调用Socket对象的getOutputStream方法获得字节输出流对象
  8. * 调用字节输出流对象的write方法往客户端输出数据
  9. * 关闭Socket和ServerSocket对象
  10. */
  11. public class Server {
  12. public static void main(String[] args) throws Exception{
  13. // 创建ServerSocket对象,并指定端口号
  14. ServerSocket ss = new ServerSocket(6666);
  15. // 调用accept()方法等待客户端连接,连接成功返回Socket对象
  16. Socket socket = ss.accept();
  17. while (true) {
  18. // 通过Socket对象获得输入流
  19. InputStream is = socket.getInputStream();
  20. // 定义一个byte数组,用来存储读取到的字节数据
  21. byte[] bys = new byte[1024];
  22. int len = is.read(bys);
  23. // 打印数据
  24. System.out.println(new String(bys,0,len));
  25. // 通过socket对象获得输出流
  26. OutputStream os = socket.getOutputStream();
  27. // 写出数据
  28. Scanner sc = new Scanner(System.in);
  29. System.out.println("请输入向客户端发送的数据:");
  30. String str = sc.nextLine();
  31. os.write(str.getBytes());
  32. }
  33. // 关闭资源
  34. //socket.close();
  35. //ss.close();// 服务器一般不关闭
  36. }
  37. }

小结

第三章 综合案例

实操—文件上传案例

需求

  • 使用TCP协议, 通过客户端向服务器上传一个文件

分析

  1. 【客户端】输入流,从硬盘读取文件数据到程序中。

  2. 【客户端】输出流,写出文件数据到服务端。

  3. 【服务端】输入流,读取文件数据到服务端程序。

  4. 【服务端】输出流,写出文件数据到服务器硬盘中。

  5. 【服务端】获取输出流,回写数据。

  6. 【客户端】获取输入流,解析回写数据。

day14【网络编程和NIO】 - 图5

实现

拷贝文件

  1. public class Client {
  2. public static void main(String[] args) throws Exception{
  3. // 客户端:
  4. // 1.创建Socket对象,指定服务器的ip地址和端口号
  5. Socket socket = new Socket("127.0.0.1",7777);
  6. // 2.创建字节输入流对象,关联数据源文件路径
  7. FileInputStream fis = new FileInputStream("day14\\aaa\\4.jpg");
  8. // 3.通过Socket对象,获取输出流对象
  9. OutputStream os = socket.getOutputStream();
  10. // 4.定义一个字节数组,用来存储读取到的字节数据
  11. byte[] bys = new byte[8192];
  12. // 4.定义一个int类型的变量,用来存储读取到的字节个数
  13. int len;
  14. // 5.循环读取数据
  15. while ((len = fis.read(bys)) != -1) {
  16. // 6.在循环中,写出数据
  17. os.write(bys,0,len);
  18. }
  19. // 7.关闭流,释放资源
  20. fis.close();
  21. socket.close();
  22. }
  23. }
  24. public class Server {
  25. public static void main(String[] args) throws Exception{
  26. // 服务器:
  27. // 1.创建ServerSocket对象,指定服务器端口号
  28. ServerSocket ss = new ServerSocket(7777);
  29. // 2.调用accept()方法,接收客户端的请求,建立连接,返回Socket对象
  30. Socket socket = ss.accept();
  31. // 3.通过Socekt对象获取字节输入流
  32. InputStream is = socket.getInputStream();
  33. // 4.创建字节输出流对象,关联目的地文件路径
  34. FileOutputStream fos = new FileOutputStream("day14\\bbb\\44.jpg");
  35. // 5.定义一个字节数组,用来存储读取到的字节数据
  36. byte[] bys = new byte[8192];
  37. // 5.定义一个int类型的变量,用来存储读取到的字节个数
  38. int len;
  39. // 6.循环读取数据
  40. while ((len = is.read(bys)) != -1) {
  41. // 7.在循环中,写出数据
  42. fos.write(bys,0,len);
  43. }
  44. // 8.关闭流,释放资源
  45. fos.close();
  46. socket.close();
  47. }
  48. }

文件上传成功后服务器回写字符串数据

  1. // 客户端
  2. public class Client {
  3. public static void main(String[] args) throws Exception{
  4. // 客户端:
  5. // 1.创建Socket对象,指定服务器的ip地址和端口号
  6. Socket socket = new Socket("127.0.0.1",7777);
  7. // 2.创建字节输入流对象,关联数据源文件路径
  8. FileInputStream fis = new FileInputStream("day14\\aaa\\5.jpg");
  9. // 3.通过Socket对象,获取输出流对象
  10. OutputStream os = socket.getOutputStream();
  11. // 4.定义一个字节数组,用来存储读取到的字节数据
  12. byte[] bys = new byte[8192];
  13. // 4.定义一个int类型的变量,用来存储读取到的字节个数
  14. int len;
  15. // 5.循环读取数据
  16. while ((len = fis.read(bys)) != -1) {
  17. // 6.在循环中,写出数据
  18. os.write(bys,0,len);
  19. }
  20. // 告诉服务器,不再写数据了
  21. socket.shutdownOutput();
  22. System.out.println("==========开始接收服务器回写的数据========");
  23. //7.通过Socket对象获取字节输入流
  24. InputStream is = socket.getInputStream();
  25. //8.读取服务器回写的字符串数据
  26. int len2 = is.read(bys);// 卡
  27. System.out.println(new String(bys,0,len2));
  28. // 9.关闭流,释放资源
  29. fis.close();
  30. socket.close();
  31. }
  32. }
  33. // 服务器
  34. public class Server {
  35. public static void main(String[] args) throws Exception{
  36. // 服务器:
  37. // 1.创建ServerSocket对象,指定服务器端口号
  38. ServerSocket ss = new ServerSocket(7777);
  39. // 2.调用accept()方法,接收客户端的请求,建立连接,返回Socket对象
  40. Socket socket = ss.accept();
  41. // 3.通过Socekt对象获取字节输入流
  42. InputStream is = socket.getInputStream();
  43. // 4.创建字节输出流对象,关联目的地文件路径
  44. FileOutputStream fos = new FileOutputStream("day14\\bbb\\55.jpg");
  45. // 5.定义一个字节数组,用来存储读取到的字节数据
  46. byte[] bys = new byte[8192];
  47. // 5.定义一个int类型的变量,用来存储读取到的字节个数
  48. int len;
  49. // 6.循环读取数据
  50. while ((len = is.read(bys)) != -1) {// 卡
  51. // 7.在循环中,写出数据
  52. fos.write(bys,0,len);
  53. }
  54. // 问题: 服务器一直在等待读取客户端写过来的数据,无法回写数据给客户端???
  55. // 原因: 服务器不知道客户端不会再写数据了
  56. // 解决: 客户端要告诉服务器不会再写数据了
  57. System.out.println("==========开始回写数据给客户端========");
  58. //8.通过Socket对象获取输出流
  59. OutputStream os = socket.getOutputStream();
  60. //9.写出字符串数据给客户端("文件上传成功!")
  61. os.write("文件上传成功!".getBytes());
  62. // 10.关闭流,释放资源
  63. fos.close();
  64. socket.close();
  65. }
  66. }

优化文件上传案例

  1. 1.文件名固定----->优化 自动生成唯一的文件名
  2. 2.服务器只能接受一次 ----> 优化 死循环去接收请求,建立连接
  3. 3.例如:如果张三先和服务器建立连接,上传了一个2GB字节大小的文件
  4. 李四后和服务器建立连接,上传了一个2MB字节大小的文件
  5. 李四就必须等张三上传完毕,才能上传文件
  6. 优化---->多线程优化
  7. 张三上传文件,开辟一条线程
  8. 李四上传文件,开辟一条线程
  1. // 服务器
  2. public class Server {
  3. public static void main(String[] args) throws Exception{
  4. // 服务器:
  5. // 1.创建ServerSocket对象,指定服务器端口号
  6. ServerSocket ss = new ServerSocket(7777);
  7. while (true){
  8. // 2.调用accept()方法,接收客户端的请求,建立连接,返回Socket对象
  9. Socket socket = ss.accept();
  10. // 开启线程,执行文件上传的代码
  11. new Thread(new Runnable() {
  12. @Override
  13. public void run() {
  14. FileOutputStream fos = null;
  15. try{
  16. // 3.通过Socekt对象获取字节输入流
  17. InputStream is = socket.getInputStream();
  18. // 4.创建字节输出流对象,关联目的地文件路径
  19. fos = new FileOutputStream("day14\\bbb\\"+System.currentTimeMillis()+".jpg");
  20. // 5.定义一个字节数组,用来存储读取到的字节数据
  21. byte[] bys = new byte[8192];
  22. // 5.定义一个int类型的变量,用来存储读取到的字节个数
  23. int len;
  24. // 6.循环读取数据
  25. while ((len = is.read(bys)) != -1) {// 卡
  26. // 7.在循环中,写出数据
  27. fos.write(bys,0,len);
  28. }
  29. // 问题: 服务器一直在等待读取客户端写过来的数据,无法回写数据给客户端???
  30. // 原因: 服务器不知道客户端不会再写数据了
  31. // 解决: 客户端要告诉服务器不会再写数据了
  32. System.out.println("==========开始回写数据给客户端========");
  33. //8.通过Socket对象获取输出流
  34. OutputStream os = socket.getOutputStream();
  35. //9.写出字符串数据给客户端("文件上传成功!")
  36. os.write("文件上传成功!".getBytes());
  37. }catch (Exception e){
  38. }finally {
  39. // 10.关闭流,释放资源
  40. try {
  41. if (fos != null) {
  42. fos.close();
  43. }
  44. } catch (IOException e) {
  45. }finally {
  46. try {
  47. socket.close();
  48. } catch (IOException e) {
  49. e.printStackTrace();
  50. }
  51. }
  52. }
  53. }
  54. }).start();
  55. }
  56. }
  57. }
  58. // 客户端
  59. public class Client {
  60. public static void main(String[] args) throws Exception{
  61. // 客户端:
  62. // 1.创建Socket对象,指定服务器的ip地址和端口号
  63. Socket socket = new Socket("127.0.0.1",7777);
  64. // 2.创建字节输入流对象,关联数据源文件路径
  65. FileInputStream fis = new FileInputStream("day14\\aaa\\5.jpg");
  66. // 3.通过Socket对象,获取输出流对象
  67. OutputStream os = socket.getOutputStream();
  68. // 4.定义一个字节数组,用来存储读取到的字节数据
  69. byte[] bys = new byte[8192];
  70. // 4.定义一个int类型的变量,用来存储读取到的字节个数
  71. int len;
  72. // 5.循环读取数据
  73. while ((len = fis.read(bys)) != -1) {
  74. // 6.在循环中,写出数据
  75. os.write(bys,0,len);
  76. }
  77. // 告诉服务器,不再写数据了
  78. socket.shutdownOutput();
  79. System.out.println("==========开始接收服务器回写的数据========");
  80. //7.通过Socket对象获取字节输入流
  81. InputStream is = socket.getInputStream();
  82. //8.读取服务器回写的字符串数据
  83. int len2 = is.read(bys);// 卡
  84. System.out.println(new String(bys,0,len2));
  85. // 9.关闭流,释放资源
  86. fis.close();
  87. socket.close();
  88. }
  89. }

小结

实操—模拟B\S服务器 扩展

需求

  • 模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。

分析

  1. 准备页面数据,web文件夹。
  2. 我们模拟服务器端,ServerSocket类监听端口,使用浏览器访问,查看网页效果

实现

浏览器工作原理是遇到图片会开启一个线程进行单独的访问,因此在服务器端加入线程技术。

  1. public class Demo {
  2. public static void main(String[] args) throws Exception {
  3. // 通过读取浏览器端的请求信息,获取浏览器需要访问的页面的路径
  4. // 1.创建ServerSocket对象,指定端口号为9999
  5. ServerSocket ss = new ServerSocket(9999);
  6. while (true) {
  7. // 2.调用accept()方法,接收请求,建立连接,返回Socket对象
  8. Socket socket = ss.accept();
  9. new Thread(new Runnable() {
  10. @Override
  11. public void run() {
  12. try {
  13. // 3.通过返回的Socket对象获取字节输入流,关联连接通道
  14. InputStream is = socket.getInputStream();
  15. // 4.把字节输入流转换为字符输入流
  16. InputStreamReader isr = new InputStreamReader(is);
  17. // 5.创建字符缓冲输入流
  18. BufferedReader br = new BufferedReader(isr);
  19. // 6.使用字符缓冲输入流读取第一行数据
  20. String line = br.readLine();
  21. // 7.使用空格对读取到的第一行数据进行分割
  22. String[] arr = line.split(" ");
  23. // 8.获取分割后数组中索引为1的元素,对其进行截取
  24. String path = arr[1].substring(1);
  25. System.out.println("浏览器需要访问的页面路径是:" + path);
  26. // 服务器把浏览器需要访问的页面响应给浏览器
  27. // 9.创建一个字节输入流,关联数据源文件路径
  28. FileInputStream fis = new FileInputStream(path);
  29. // 10.通过Socket对象获得输出流,关联连接通道
  30. OutputStream os = socket.getOutputStream();
  31. // 11.定义一个变量,用来存储读取到的字节数据
  32. byte[] bys = new byte[8192];
  33. int len;
  34. // 响应页面的时候需要同时把以下响应过去给浏览器
  35. os.write("HTTP/1.1 200 OK\r\n".getBytes());
  36. os.write("Content-Type:text/html\r\n".getBytes());
  37. os.write("\r\n".getBytes());
  38. // 12.循环读取
  39. while ((len = fis.read(bys)) != -1) {
  40. // 13.在循环中,写出数据给浏览器
  41. os.write(bys, 0, len);
  42. }
  43. // 关闭Socket对象,释放资源
  44. fis.close();
  45. socket.close();
  46. } catch (IOException e) {
  47. }
  48. }
  49. }).start();
  50. }
  51. }
  52. /**
  53. * 1.读取到浏览器端的请求信息
  54. *
  55. * @return
  56. * @throws IOException
  57. */
  58. private static Socket method01() throws IOException {
  59. // 1.读取到浏览器端的请求信息
  60. // 1.1 创建ServerSocket对象,指定端口号为9999
  61. ServerSocket ss = new ServerSocket(9999);
  62. // 1.2 调用accept()方法,接收请求,建立连接,返回Socket对象
  63. Socket socket = ss.accept();
  64. // 1.3 通过返回的Socket对象获取输入流,关联连接通道
  65. InputStream is = socket.getInputStream();
  66. // 1.4 使用输入流去读取数据
  67. byte[] bys = new byte[8192];
  68. int len = is.read(bys);
  69. // 1.5 打印读取到的数据
  70. System.out.println(new String(bys, 0, len));
  71. /*
  72. GET /day12/web/index.html HTTP/1.1
  73. Host: localhost:9999
  74. Connection: keep-alive
  75. Cache-Control: max-age=0
  76. Upgrade-Insecure-Requests: 1
  77. User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
  78. Sec-Fetch-User: ?1
  79. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,**;q=0.8,application/signed-exchange;v=b3
  80. Sec-Fetch-Site: none
  81. Sec-Fetch-Mode: navigate
  82. Accept-Encoding: gzip, deflate, br
  83. Accept-Language: zh-CN,zh;q=0.9
  84. Cookie: Idea-7071e3d8=cc177568-5581-4562-aeac-26fcb6ca7e56
  85. */
  86. return socket;
  87. }
  88. }

访问效果:

day14【网络编程和NIO】 - 图6

图解:

day14【网络编程和NIO】 - 图7

小结

第四章 NIO

知识点—NIO概述

目的

  • 了解NIO的概述

路径

  • 同步和异步
  • 阻塞和非阻塞

讲解

在我们学习Java的NIO流之前,我们都要了解几个关键词

  • 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系

    • 同步: 调用方法之后,必须要得到一个返回值 例如: 买火车票,一定要买到票,才能继续下一步
    • 异步: 调用方法之后,不需要有返回值,但是会有回调函数,回调函数指的是满足条件之后会自动执行的方法 例如: 买火车票, 不一定要买到票,我可以交代售票员,当有票的话,你就帮我出张票
  • 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理

    • 阻塞:如果没有达到方法的目的,就会一直停在那里(等待) , 例如: ServerSocket的accept()方法
    • 非阻塞: 不管方法有没有达到目的,都直接往下执行(不等待)
  • IO: 同步阻塞
  • NIO: 同步非阻塞
  • AIO: 异步非阻塞

在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据,面向流的I/O速度非常慢,而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理数据,每一个操作在一步中产生或者消费一个数据,按块处理要比按字节处理数据快的多。

在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2\AIO,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程

首先,我们要先了解一下NIO的三个主要组成部分:Buffer(缓冲区)、Channel(通道)、Selector(选择器)

小结

  • Buffer(缓冲区)、Channel(通道)、Selector(选择器)是NIO的三个部分
  • NIO是在访问个数特别大的时候才使用的 , 比如流行的软件或者流行的游戏中会有高并发和大量连接.

第三章 Buffer类(缓冲区)

知识点—Buffer的概述和分类

目标

  • 了解Buffer概述

路径

  • Buffer的概述
  • Buffer的分类

讲解

概述:Buffer是一个对象,它是对某种基本类型的数组进行了封装。

作用: 在NIO中,就是通过 Buffer 来读写数据的。所有的数据都是用Buffer来处理的,它是NIO读写数据的中转池, 通常使用字节数组。

Buffer主要有如下几种:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

小结

知识点—创建ByteBuffer

目标

  • 掌握创建ByteBuffer对象

路径

  • 创建ByteBuffer对象的三种方式

讲解

  • ByteBuffer类内部封装了一个byte[]数组,并可以通过一些方法对这个数组进行操作。

  • 创建ByteBuffer对象

    • 方式一:在堆中创建缓冲区:allocate(int capacity)
    • 方式二: 在系统内存创建缓冲区:allocatDirect(int capacity)
    • 方式三:通过数组创建缓冲区:wrap(byte[] arr)
  • 案例演示:

    • 方式一:在堆中创建缓冲区:allocate(int capacity)
      1. public static void main(String[] args) {
      2. //创建堆缓冲区
      3. ByteBuffer byteBuffer = ByteBuffer.allocate(10);
      4. }


day14【网络编程和NIO】 - 图8

  • 方式二: 在系统内存创建缓冲区:allocatDirect(int capacity)
    1. public static void main(String[] args) {
    2. //创建直接缓冲区
    3. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
    4. }
  1. -

在堆中创建缓冲区称为:间接缓冲区

  1. -

在系统内存创建缓冲区称为:直接缓冲区

  1. -

间接缓冲区的创建和销毁效率要高于直接缓冲区

  1. -

间接缓冲区的工作效率要低于直接缓冲区

  • 方式三:通过数组创建缓冲区:wrap(byte[] arr)
    1. public static void main(String[] args) {
    2. byte[] byteArray = new byte[10];
    3. ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
    4. }
  • 此种方式创建的缓冲区为:间接缓冲区

小结

知识点—添加数据-put

目标

  • 掌握添加数据-put方法的使用

路径

  • 添加数据-put方法的使用

讲解

  • public ByteBuffer put(byte b):向当前可用位置添加数据。

  • public ByteBuffer put(byte[] byteArray):向当前可用位置添加一个byte[]数组

  • public ByteBuffer put(byte[] byteArray,int offset,int len):添加一个byte[]数组的一部分

    1. public static void main(String[] args) {
    2. yteBuffer b1 = ByteBuffer.allocate(10);
    3. // 添加数据
    4. b1.put((byte)11);
    5. b1.put((byte)12);
    6. b1.put((byte)13);
    7. // ByteBuffer转换为普通字节数组
    8. byte[] bytes = b1.array();
    9. System.out.println(Arrays.toString(bytes));
    10. // 打印结果: [11, 12, 13, 0, 0, 0, 0, 0, 0, 0]
    11. }
  1. public class Test_添加数据 {
  2. public static void main(String[] args) {
  3. ByteBuffer b1 = ByteBuffer.allocate(10);
  4. // 添加数据
  5. b1.put((byte)11);
  6. b1.put((byte)12);
  7. b1.put((byte)13);
  8. // ByteBuffer转换为普通字节数组
  9. byte[] bytes = b1.array();
  10. System.out.println(Arrays.toString(bytes));
  11. //打印结果: [11, 12, 13, 0, 0, 0, 0, 0, 0, 0]
  12. System.out.println("=======================");
  13. byte[] b2 = {14,15,16};
  14. // 添加数据
  15. b1.put(b2);
  16. // ByteBuffer转换为普通字节数组
  17. byte[] b = b1.array();
  18. System.out.println(Arrays.toString(b));
  19. //打印结果: [11, 12, 13, 14, 15, 16, 0, 0, 0, 0]
  20. }
  21. }
  1. public class Test_添加数据 {
  2. public static void main(String[] args) {
  3. ByteBuffer b1 = ByteBuffer.allocate(10);
  4. // 添加数据
  5. b1.put((byte)11);
  6. b1.put((byte)12);
  7. b1.put((byte)13);
  8. // ByteBuffer转换为普通字节数组
  9. byte[] bytes = b1.array();
  10. System.out.println(Arrays.toString(bytes));
  11. // 打印结果: [11, 12, 13, 0, 0, 0, 0, 0, 0, 0]
  12. System.out.println("=======================");
  13. byte[] b2 = {14,15,16};
  14. // 添加数据
  15. b1.put(b2,0,1);
  16. // ByteBuffer转换为普通字节数组
  17. byte[] b = b1.array();
  18. System.out.println(Arrays.toString(b));
  19. // 打印结果: [11, 12, 13, 14, 0, 0, 0, 0, 0, 0]
  20. }
  21. }

小结

知识点—容量-capacity

目标

  • 掌握容量-capacity方法的使用

路径

  • 容量-capacity方法的使用

讲解

  • Buffer的容量(capacity)是指:Buffer所能够包含的元素的最大数量。定义了Buffer后,容量是不可变的。

  • 示例代码:

    1. public static void main(String[] args) {
    2. ByteBuffer b1 = ByteBuffer.allocate(10);
    3. System.out.println("容量:" + b1.capacity());//10。之后不可改变
    4. byte[] byteArray = {97, 98, 99, 100};
    5. ByteBuffer b2 = ByteBuffer.wrap(byteArray);
    6. System.out.println("容量:" + b2.capacity());//4。之后不可改变
    7. }
  • 结果:
    1. 容量:10
    2. 容量:4

小结

知识点—限制-limit

目标

  • 掌握限制-limit方法的使用

路径

  • 限制-limit方法的使用

讲解

  • 限制limit是指:第一个不应该读取或写入元素的index索引。缓冲区的限制(limit)不能为负,并且不能大于容量。

  • 有两个相关方法:

    • public int limit():获取此缓冲区的限制。
    • public Buffer limit(int newLimit):设置此缓冲区的限制。
  • 示例代码:

    1. public class Test_添加数据 {
    2. public static void main(String[] args) {
    3. ByteBuffer b1 = ByteBuffer.allocate(10);
    4. // 添加数据
    5. b1.put((byte)10);
    6. // 获取限制
    7. int limit1 = b1.limit();
    8. System.out.println("limit1:"+limit1);// 10
    9. // 设置限制
    10. b1.limit(3);
    11. // 添加元素
    12. b1.put((byte)20);
    13. b1.put((byte)30);
    14. // b1.put((byte)40);// 报异常,因为限制位置索引为3,所以再存14就会报异常:BufferOverflowException
    15. }
    16. }


图示:
day14【网络编程和NIO】 - 图9

小结

知识点—位置-position

目标

  • 掌握位置-position方法的使用

路径

  • 位置-position方法的使用

讲解

  • 位置position是指:当前可写入的索引。位置不能小于0,并且不能大于”限制”。

  • 有两个相关方法:

    • public int position():获取当前可写入位置索引。
    • public Buffer position(int p):更改当前可写入位置索引。
  • 示例代码:

    1. public class Test_添加数据 {
    2. public static void main(String[] args) {
    3. ByteBuffer b1 = ByteBuffer.allocate(10);
    4. // 添加数据
    5. b1.put((byte)11);
    6. // 获取当前位置索引
    7. int position = b1.position();
    8. System.out.println("position:"+position);// 1
    9. // 设置当前位置索引
    10. b1.position(5);
    11. b1.put((byte)22);
    12. b1.put((byte)33);
    13. System.out.println("position:"+b1.position());// 7
    14. System.out.println(Arrays.toString(b1.array()));
    15. // 打印结果:[11, 0, 0, 0, 0, 22, 33, 0, 0, 0]
    16. }
    17. }

小结

知识点—标记-mark

目标

  • 掌握标记-mark方法的使用

路径

  • 标记-mark方法的使用

讲解

  • 标记mark是指:当调用缓冲区的reset()方法时,会将缓冲区的position位置重置为该索引。

  • 相关方法:

    • public Buffer mark():设置此缓冲区的标记为当前的position位置。
    • public Buffer reset() : 将此缓冲区的位置重置为以前标记的位置。
  • 示例代码:

    1. public static void main(String[] args) {
    2. ByteBuffer b1 = ByteBuffer.allocate(10);
    3. // 添加数据
    4. b1.put((byte)11);
    5. // 获取当前位置索引
    6. int position = b1.position();
    7. System.out.println("position:"+position);// 1
    8. // 标记当前位置索引
    9. b1.mark();
    10. // 添加元素
    11. b1.put((byte)22);
    12. b1.put((byte)33);
    13. // 获取当前位置索引
    14. System.out.println("position:"+b1.position());// 3
    15. System.out.println(Arrays.toString(b1.array()));
    16. // 打印结果:[11, 22, 33, 0, 0, 0, 0, 0, 0, 0]
    17. // 重置当前位置索引
    18. b1.reset();
    19. // 获取当前位置索引
    20. System.out.println("position:"+b1.position());// 1
    21. // 添加元素
    22. b1.put((byte)44);
    23. System.out.println(Arrays.toString(b1.array()));
    24. // 打印结果:[11, 44, 33, 0, 0, 0, 0, 0, 0, 0]
    25. }

小结

-略

知识点—其它方法

目标

  • 了解其它方法方法的使用

路径

  • 其它方法的使用

讲解

  1. - public int remaining():获取positionlimit之间的元素数。
  2. - public boolean isReadOnly():获取当前缓冲区是否只读。
  3. - public boolean isDirect():获取当前缓冲区是否为直接缓冲区。
  4. - public Buffer rewind():重绕此缓冲区。
  5. - position位置设置为:0
  6. - 限制limit不变。
  7. - 丢弃标记。
  8. - public Buffer clear():还原缓冲区的状态。
  9. - position设置为:0
  10. - 将限制limit设置为容量capacity
  11. - 丢弃标记mark
  12. - public Buffer flip():缩小limit的范围。
  13. - limit设置为当前position位置;
  14. - 将当前position位置设置为0
  15. - 丢弃标记。
  1. public static void main(String[] args) {
  2. //创建对象
  3. ByteBuffer buffer = ByteBuffer.allocate(10);
  4. //添加元素
  5. buffer.put((byte)11);
  6. buffer.put((byte)22);
  7. buffer.put((byte)33);
  8. //限制
  9. buffer.limit(6);
  10. //容量是10 位置是3 限制是6
  11. System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
  12. //还原
  13. buffer.clear();
  14. //容量是10 位置是0 限制是10
  15. System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
  16. }
  17. public static void main(String[] args) {
  18. //创建对象
  19. ByteBuffer buffer = ByteBuffer.allocate(10);
  20. //添加元素
  21. buffer.put((byte)11);
  22. buffer.put((byte)22);
  23. buffer.put((byte)33);
  24. //限制
  25. buffer.limit(6);
  26. //容量是10 位置是3 限制是6
  27. System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
  28. //切换
  29. buffer.flip();
  30. //容量是10 位置是0 限制是3
  31. System.out.println("容量是" + buffer.capacity() + " 位置是" + buffer.position() + " 限制是" + buffer.limit());
  32. }

小结

第四章 Channel(通道)

知识点—Channel概述

目标

  • 理解Channel 的概述和分类

路径

  • Channel 的概述
  • Channel 的分类

讲解

Channel 的概述

Channel(通道):Channel是一个对象,可以通过它读取和写入数据, 可以把它看做是IO中的流,不同的是:Channel是双向的, Channel对象既可以调用读取的方法, 也可以调用写出的方法 。

输入流: 读

输出流: 写

Channel: 读,写

Channel 的分类

在Java NIO中的Channel主要有如下几种类型:

  • FileChannel:从文件读取数据的 输入流和输出流
  • DatagramChannel:读写UDP网络协议数据 DatagramPackge
  • SocketChannel:读写TCP网络协议数据 Socket
  • ServerSocketChannel:可以监听TCP连接 ServerSocket

小结

知识点—FileChannel类的基本使用

目标

  • 使用FileChannel类完成文件的复制

路径

  • 获取FileChannel类的对象
  • 使用FileChannel类完成文件的复制

讲解

获取FileChannel类的对象

  • java.nio.channels.FileChannel (抽象类):用于读、写文件的通道。

  • FileChannel是抽象类,我们可以通过FileInputStream和FileOutputStream的getChannel()方法方便的获取一个它的子类对象。

    1. FileInputStream fi=new FileInputStream(new File(src));
    2. FileOutputStream fo=new FileOutputStream(new File(dst));
    3. //获得传输通道channel
    4. FileChannel inChannel=fi.getChannel();
    5. FileChannel outChannel=fo.getChannel();

使用FileChannel类完成文件的复制

  • 我们将通过CopyFile这个示例让大家体会NIO的操作过程。CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。
  1. public static void main(String[] args) throws Exception{
  2. FileInputStream fis = new FileInputStream("day19\\aaa\\a.txt");
  3. FileOutputStream fos = new FileOutputStream("day19\\aaa\\aCopy1.txt");
  4. // 获得FileChannel管道对象
  5. FileChannel c1 = fis.getChannel();
  6. FileChannel c2 = fos.getChannel();
  7. // 创建ByteBuffer数组
  8. ByteBuffer b = ByteBuffer.allocate(1000);
  9. // 循环读取数据
  10. while ((c1.read(b)) != -1){// 读取的字节会填充postion到limit位置之间
  11. // 重置 postion为0,limit为postion的位置
  12. b.flip();
  13. // 写出数据
  14. c2.write(b);// 会把postion到limit之间的数据写出
  15. // 还原
  16. b.clear();// positon为:0 limit为: capacity 用于下次读取
  17. }
  18. // 释放资源
  19. c2.close();
  20. c1.close();
  21. fos.close();
  22. fis.close();
  23. /*byte[] bys = new byte[8192];
  24. int len;
  25. while ((len = fis.read(bys)) != -1){
  26. fos.write(bys,0,len);
  27. }
  28. fos.close();
  29. fis.close();*/
  30. }

小结

知识点—FileChannel结合MappedByteBuffer实现高效读写

目标

  • MappedByteBuffer类的使用

路径

  • MappedByteBuffer类的概述
  • FileChannel结合MapppedByteBuffer复制2G以下文件
  • FileChannel结合MapppedByteBuffer复制2G以上文件

讲解

MappedByteBuffer类的概述

  • 上例直接使用FileChannel结合ByteBuffer实现的管道读写,但并不能提高文件的读写效率。

  • ByteBuffer有个抽象子类:MappedByteBuffer,它可以将文件直接映射至内存,把硬盘中的读写变成内存中的读写, 所以可以提高大文件的读写效率。

  • 可以调用FileChannel的map()方法获取一个MappedByteBuffer,map()方法的原型:
    MappedByteBuffer map(MapMode mode, long position, long size);
    说明:将节点中从position开始的size个字节映射到返回的MappedByteBuffer中。

复制2GB以下的文件

  • 复制d:\b.rar文件,此文件大概600多兆,复制完毕用时不到2秒。此例不能复制大于2G的文件,因为map的第三个参数被限制在Integer.MAX_VALUE(字节) = 2G。
  1. public static void main(String[] args) throws Exception{
  2. //java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
  3. //"r"表示:只读--输入流,只读就可以。
  4. RandomAccessFile r1 = new RandomAccessFile("day19\\aaa\\a.txt","r");
  5. //"rw"表示:读、写--输出流,需要读、写。
  6. RandomAccessFile r2 = new RandomAccessFile("day19\\aaa\\aCopy2.txt","rw");
  7. // 获得FileChannel管道对象
  8. FileChannel c1 = r1.getChannel();
  9. FileChannel c2 = r2.getChannel();
  10. // 获取文件的大小
  11. long size = c1.size();
  12. // 直接把硬盘中的文件映射到内存中
  13. MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
  14. MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);
  15. // 循环读取数据
  16. for (long i = 0; i < size; i++) {
  17. // 读取字节
  18. byte b = b1.get();
  19. // 保存到第二个数组中
  20. b2.put(b);
  21. }
  22. // 释放资源
  23. c2.close();
  24. c1.close();
  25. r2.close();
  26. r1.close();
  27. }
  • 代码说明:

  • map()方法的第一个参数mode:映射的三种模式,在这三种模式下得到的将是三种不同的MappedByteBuffer:三种模式都是Channel的内部类MapMode中定义的静态常量,这里以FileChannel举例:
    1). FileChannel.MapMode.READ_ONLY:得到的镜像只能读不能写(只能使用get之类的读取Buffer中的内容);
    2). FileChannel.MapMode.READ_WRITE:得到的镜像可读可写(既然可写了必然可读),对其写会直接更改到存储节点;
    3). FileChannel.MapMode.PRIVATE:得到一个私有的镜像,其实就是一个(position, size)区域的副本罢了,也是可读可写,只不过写不会影响到存储节点,就是一个普通的ByteBuffer了!!

  • 为什么使用RandomAccessFile?
    1). 使用InputStream获得的Channel可以映射,使用map时只能指定为READ_ONLY模式,不能指定为READ_WRITE和PRIVATE,否则会抛出运行时异常!
    2). 使用OutputStream得到的Channel不可以映射!并且OutputStream的Channel也只能write不能read!
    3). 只有RandomAccessFile获取的Channel才能开启任意的这三种模式!

复制2GB以上的文件

  • 下例使用循环,将文件分块,可以高效的复制大于2G的文件
  1. public static void main(String[] args) throws Exception{
  2. //java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
  3. //"r"表示:只读--输入流,只读就可以。
  4. RandomAccessFile r1 = new RandomAccessFile("H:\\课堂资料.zip","r");
  5. //"rw"表示:读、写--输出流,需要读、写。
  6. RandomAccessFile r2 = new RandomAccessFile("H:\\课堂资料2.zip","rw");
  7. // 获得FileChannel管道对象
  8. FileChannel c1 = r1.getChannel();
  9. FileChannel c2 = r2.getChannel();
  10. // 获取文件的大小
  11. long size = c1.size();
  12. // 每次期望复制500M
  13. int everySize = 1024*1024*500;
  14. // 总共需要复制多少次
  15. long count = size % everySize == 0 ? size/everySize : size/everySize+1;
  16. // 开始复制
  17. for (long i = 0; i < count; i++) {
  18. // 每次开始复制的位置
  19. long start = everySize*i;
  20. // 每次复制的实际大小
  21. long trueSize = size - start > everySize ? everySize : size - start;
  22. // 直接把硬盘中的文件映射到内存中
  23. MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, start, trueSize);
  24. MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, start, trueSize);
  25. // 循环读取数据
  26. for (long j = 0; j < trueSize; j++) {
  27. // 读取字节
  28. byte b = b1.get();
  29. // 保存到第二个数组中
  30. b2.put(b);
  31. }
  32. }
  33. // 释放资源
  34. c2.close();
  35. c1.close();
  36. r2.close();
  37. r1.close();
  38. }

小结

知识点—ServerSocketChannel和SocketChannel创建连接

目标

  • ServerSocketChannel和SocketChannel创建连接

路径

  • SocketChannel创建连接
  • ServerSocketChanne创建连接

讲解

SocketChannel创建连接

  • 客户端:SocketChannel类用于连接的客户端,它相当于:Socket。
    1). 先调用SocketChannel的open()方法打开通道:
    1. SocketChannel socket = SocketChannel.open()


2). 调用SocketChannel的实例方法connect(SocketAddress add)连接服务器:

  1. socket.connect(new InetSocketAddress("localhost", 8888));


示例:客户端连接服务器:

  1. public class Client {
  2. public static void main(String[] args) throws Exception {
  3. SocketChannel socket = SocketChannel.open();
  4. socket.connect(new InetSocketAddress("localhost", 8888));
  5. System.out.println("后续代码......");
  6. }
  7. }

ServerSocketChanne创建连接

  • 服务器端:ServerSocketChannel类用于连接的服务器端,它相当于:ServerSocket。

  • 调用ServerSocketChannel的静态方法open()就可以获得ServerSocketChannel对象, 但并没有指定端口号, 必须通过其套接字的bind方法将其绑定到特定地址,才能接受连接。

    1. ServerSocketChannel serverChannel = ServerSocketChannel.open()
  • 调用ServerSocketChannel的实例方法bind(SocketAddress add):绑定本机监听端口,准备接受连接。
    注:java.net.SocketAddress(抽象类):代表一个Socket地址。
    我们可以使用它的子类:java.net.InetSocketAddress(类)
    构造方法:InetSocketAddress(int port):指定本机监听端口。
    1. serverChannel.bind(new InetSocketAddress(8888));
  • 调用ServerSocketChannel的实例方法accept():等待连接。
    1. SocketChannel accept = serverChannel.accept();
    2. System.out.println("后续代码...");


示例:服务器端等待连接(默认-阻塞模式)

  1. public class Server {
  2. public static void main(String[] args) throws Exception{
  3. ServerSocketChannel serverChannel = ServerSocketChannel.open();
  4. serverChannel.bind(new InetSocketAddress(8888));
  5. System.out.println("【服务器】等待客户端连接...");
  6. SocketChannel accept = serverChannel.accept();
  7. System.out.println("后续代码......");
  8. }
  9. }


运行后结果:

  1. 【服务器】等待客户端连接...
  • 我们可以通过ServerSocketChannel的configureBlocking(boolean b)方法设置accept()是否阻塞

    1. public class Server {
    2. public static void main(String[] args) throws Exception {
    3. ServerSocketChannel ssc = ServerSocketChannel.open();
    4. ssc.bind(new InetSocketAddress(8888));
    5. System.out.println("【服务器】等待客户端连接...");
    6. //设置非阻塞连接
    7. ssc.configureBlocking(false);// 写成false叫非阻塞, 写成true叫阻塞
    8. while(true) {
    9. //获取客户端连接
    10. SocketChannel sc = ssc.accept();
    11. if(sc != null){
    12. //不等于null说明连接上了客户端
    13. System.out.println("连接上了。。");
    14. //读取数据操作
    15. break;
    16. }else{
    17. //没连接上客户端
    18. System.out.println("打会儿游戏~");
    19. Thread.sleep(2000);
    20. }
    21. }
    22. }
    23. }


运行后结果:

  1. 【服务器】等待客户端连接...
  2. 打会儿游戏~
  3. 有客户端来了就输出: 连接上了。。

小结

知识点—NIO网络编程收发信息

目标

  • 使用ServerSocketChannel代替之前的ServerSocket,来完成网络编程的收发数据。

路径

  • 书写服务器代码
  • 书写客户端代码

讲解

书写服务器代码

  1. public class Server {
  2. public static void main(String[] args) throws IOException{
  3. //创建对象
  4. //ServerSocket ss = new ServerSocket(8888);
  5. //创建
  6. ServerSocketChannel ssc = ServerSocketChannel.open();
  7. //服务器绑定端口
  8. ssc.bind(new InetSocketAddress(8888));
  9. //连接上客户端
  10. SocketChannel sc = ssc.accept();
  11. //服务器端接受数据
  12. //创建数组
  13. ByteBuffer buffer = ByteBuffer.allocate(1024);
  14. //接收数据
  15. int len = sc.read(buffer);
  16. //打印结构
  17. System.out.println(new String(buffer.array(),0,len));
  18. //关闭资源
  19. sc.close();
  20. }
  21. }

书写客户端代码

  1. public class Client {
  2. public static void main(String[] args) {
  3. //创建对象
  4. //Socket s = new Socket("127.0.0.1",8888);
  5. //创建对象
  6. SocketChannel sc = SocketChannel.open();
  7. //连接服务器
  8. sc.connect(new InetSocketAddress("127.0.0.1",8888));
  9. //客户端发数据
  10. //创建数组
  11. ByteBuffer buffer = ByteBuffer.allocate(1024);
  12. //数组中添加数据
  13. buffer.put("你好啊~".getBytes());
  14. //切换
  15. buffer.flip();
  16. //发出数据
  17. sc.write(buffer);
  18. //关流
  19. sc.close();
  20. }
  21. }

小结

第五章 Selector(选择器)

知识点—多路复用的概念

目标

  • 了解多路复用的概念

路径

  • 服务器端的非多路复用效果
  • 服务器端的多路复用效果

讲解

选择器Selector是NIO中的重要技术之一。它与SelectableChannel联合使用实现了非阻塞的多路复用。使用它可以节省CPU资源,提高程序的运行效率。

“多路”是指:服务器端同时监听多个“端口”的情况。每个端口都要监听多个客户端的连接。

  • 服务器端的非多路复用效
    day14【网络编程和NIO】 - 图10
    如果不使用“多路复用”,服务器端需要开很多线程处理每个端口的请求。如果在高并发环境下,造成系统性能下降。

  • 服务器端的多路复用效果
    day14【网络编程和NIO】 - 图11
    使用了多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发、高频段业务环境下有非常重要的优势

小结

  • 多路复用的意思就是一个Selector可以监听多个服务器端口。

知识点—选择器Selector的获取和注册

目标

  • 理解Selector的作用以及基本使用

路径

  • Selector选择器的概述和作用
  • Selector选择器的获取
  • 注册Channel到Selector

讲解

Selector选择器的概述和作用

概述: Selector被称为:选择器,也被称为:多路复用器,可以把多个Channel注册到一个Selector选择器上, 那么就可以实现利用一个线程来处理这多个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了, 减少系统负担, 提高效率。因为线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。

作用: 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率 .

Selector选择器的获取

  1. Selector selector = Selector.open();

注册Channel到Selector

通过调用 channel.register(Selector sel, int ops)方法来实现注册:

  1. channel.configureBlocking(false);// 设置非阻塞
  2. SelectionKey key =channel.register(selector,SelectionKey.OP_READ);

register()方法的第二个参数:是一个int值,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,而且可以使用SelectionKey的四个常量表示:

  1. 连接就绪—常量:SelectionKey.OP_CONNECT

  2. 接收就绪—常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在注册时只能使用此项)

  3. 读就绪—常量:SelectionKey.OP_READ

  4. 写就绪—常量:SelectionKey.OP_WRITE
    注意:对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。

  • 案例演示; 监听一个通道

    1. public class Test1 {
    2. public static void main(String[] args) throws Exception{
    3. /*
    4. - Selector选择器的概述和作用
    5. 概述: Selector被称为:选择器,也被称为:多路复用器,可以把多个Channel注册到一个Selector选择器上,
    6. 那么就可以实现利用一个线程来处理这多个Channel上发生的事件,并且能够根据事件情况决定Channel读写。
    7. 作用: 一个Selector可以监听多个Channel发生的事件, 减少系统负担 , 提高程序执行效率 .
    8. - Selector选择器的获取
    9. 通过Selector.open()来获取Selector选择器对象
    10. - 注册Channel到Selector
    11. 通过Channel的register(Selector sel, int ops)方法把Channel注册到指定的选择器上
    12. 参数1: 表示选择器
    13. 参数2: 选择器要监听Channel的什么事件
    14. 注意:
    15. 1.对于ServerSocketChannel在注册时,只能使用OP_ACCEPT,否则抛出异常。
    16. 2.ServerSocketChannel要设置成非阻塞
    17. */
    18. // 获取ServerSocketChannel服务器通道对象
    19. ServerSocketChannel ssc1 = ServerSocketChannel.open();
    20. // 绑定端口号
    21. ssc1.bind(new InetSocketAddress(7777));
    22. // 设置非阻塞
    23. ssc1.configureBlocking(false);
    24. // 获取Selector选择器对象
    25. Selector selector = Selector.open();
    26. // 把服务器通道的accept()交给选择器来处理
    27. // 注册Channel到Selector选择器上
    28. ssc1.register(selector, SelectionKey.OP_ACCEPT);
    29. }
    30. }
  • 示例:服务器创建3个通道,同时监听3个端口,并将3个通道注册到一个选择器中
  1. public class Test2 {
  2. public static void main(String[] args) throws Exception{
  3. /*
  4. 把多个Channel注册到一个选择器上
  5. */
  6. // 获取ServerSocketChannel服务器通道对象
  7. ServerSocketChannel ssc1 = ServerSocketChannel.open();
  8. // 绑定端口号
  9. ssc1.bind(new InetSocketAddress(7777));
  10. // 获取ServerSocketChannel服务器通道对象
  11. ServerSocketChannel ssc2 = ServerSocketChannel.open();
  12. // 绑定端口号
  13. ssc2.bind(new InetSocketAddress(8888));
  14. // 获取ServerSocketChannel服务器通道对象
  15. ServerSocketChannel ssc3 = ServerSocketChannel.open();
  16. // 绑定端口号
  17. ssc3.bind(new InetSocketAddress(9999));
  18. // 设置非阻塞
  19. ssc1.configureBlocking(false);
  20. ssc2.configureBlocking(false);
  21. ssc3.configureBlocking(false);
  22. // 获取Selector选择器对象
  23. Selector selector = Selector.open();
  24. // 把服务器通道的accept()交给选择器来处理
  25. // 注册Channel到Selector选择器上
  26. ssc1.register(selector, SelectionKey.OP_ACCEPT);
  27. ssc2.register(selector,SelectionKey.OP_ACCEPT);
  28. ssc3.register(selector,SelectionKey.OP_ACCEPT);
  29. }
  30. }

接下来,就可以通过选择器selector操作三个通道了。

小结

知识点—Selector的常用方法

目标

  • Selector的常用方法

路径

  • Selector的select()方法
  • Selector的selectedKeys()方法
  • Selector的keys()方法

讲解

Selector的select()方法:

  • 作用: 服务器等待客户端连接的方法

  • 阻塞问题:

    • 在连接到第一个客户端之前,会一直阻塞
    • 当连接到客户端后,如果客户端没有被处理,该方法会计入不阻塞状态
    • 当连接到客户端后,如果客户端有被处理,该方法又会进入阻塞状态 ```java public class Server1 { public static void main(String[] args) throws Exception { /*

      1. - Selectorselect()方法
      2. 作用:服务器等待客户端连接的方法
      3. 阻塞:
      4. 1.在没有客户端连接之前该方法会一直阻塞
      5. 2.当连接到客户端后没有被处理,该方法就会进入不阻塞状态
      6. 3.当连接到客户端后有被处理,该方法就会进入阻塞状态
      7. */

      // 获取ServerSocketChannel服务器通道对象 ServerSocketChannel ssc1 = ServerSocketChannel.open(); // 绑定端口号 ssc1.bind(new InetSocketAddress(7777));

      // 获取ServerSocketChannel服务器通道对象 ServerSocketChannel ssc2 = ServerSocketChannel.open(); // 绑定端口号 ssc2.bind(new InetSocketAddress(8888));

  1. // 获取ServerSocketChannel服务器通道对象
  2. ServerSocketChannel ssc3 = ServerSocketChannel.open();
  3. // 绑定端口号
  4. ssc3.bind(new InetSocketAddress(9999));
  5. // 设置非阻塞
  6. ssc1.configureBlocking(false);
  7. ssc2.configureBlocking(false);
  8. ssc3.configureBlocking(false);
  9. // 获取Selector选择器对象
  10. Selector selector = Selector.open();
  11. // 把服务器通道的accept()交给选择器来处理
  12. // 注册Channel到Selector选择器上
  13. ssc1.register(selector, SelectionKey.OP_ACCEPT);
  14. ssc2.register(selector, SelectionKey.OP_ACCEPT);
  15. ssc3.register(selector, SelectionKey.OP_ACCEPT);
  16. // 死循环一直接受客户端的连接请求
  17. while (true) {
  18. System.out.println(1);
  19. // 服务器等待客户端的连接
  20. selector.select();// 阻塞
  21. System.out.println(2);
  22. // 处理客户端请求的代码--->暂时看不懂,先放着
  23. Set<SelectionKey> keySet = selector.selectedKeys();// 存储所有被连接的服务器Channel对象
  24. for (SelectionKey key : keySet) {
  25. ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
  26. SocketChannel sc = ssc.accept();
  27. System.out.println("...开始处理,接受数据,代码省略...");
  28. //...
  29. }
  30. }
  31. }

}

  1. <a name="8e56ef60"></a>
  2. #### Selector的selectedKeys()方法
  3. -
  4. 获取已连接的所有通道集合
  5. ```java
  6. public class Server2 {
  7. public static void main(String[] args) throws Exception {
  8. /*
  9. - Selector的selectedKeys()方法
  10. 作用: 获取所有被连接的服务器Channel对象的Set集合
  11. 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
  12. 如何获取被连接的服务器Channel对象:
  13. 遍历所有被连接的服务器Channel对象的Set集合
  14. 获取该集合中的SelectionKey对象
  15. 根据SelectionKey对象调用channel方法,获得服务器Channel对象
  16. - Selector的keys()方法
  17. */
  18. // 获取ServerSocketChannel服务器通道对象
  19. ServerSocketChannel ssc1 = ServerSocketChannel.open();
  20. // 绑定端口号
  21. ssc1.bind(new InetSocketAddress(7777));
  22. // 获取ServerSocketChannel服务器通道对象
  23. ServerSocketChannel ssc2 = ServerSocketChannel.open();
  24. // 绑定端口号
  25. ssc2.bind(new InetSocketAddress(8888));
  26. // 获取ServerSocketChannel服务器通道对象
  27. ServerSocketChannel ssc3 = ServerSocketChannel.open();
  28. // 绑定端口号
  29. ssc3.bind(new InetSocketAddress(9999));
  30. // 设置非阻塞
  31. ssc1.configureBlocking(false);
  32. ssc2.configureBlocking(false);
  33. ssc3.configureBlocking(false);
  34. // 获取Selector选择器对象
  35. Selector selector = Selector.open();
  36. // 把服务器通道的accept()交给选择器来处理
  37. // 注册Channel到Selector选择器上
  38. ssc1.register(selector, SelectionKey.OP_ACCEPT);
  39. ssc2.register(selector, SelectionKey.OP_ACCEPT);
  40. ssc3.register(selector, SelectionKey.OP_ACCEPT);
  41. // 获取所有被连接的服务器Channel对象的Set集合
  42. // 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
  43. Set<SelectionKey> keySet = selector.selectedKeys();
  44. System.out.println("被连接的服务器对象有多少个:"+keySet.size());// 0
  45. // 死循环一直接受客户端的连接请求
  46. while (true) {
  47. System.out.println(1);
  48. // 服务器等待客户端的连接
  49. selector.select();// 阻塞
  50. System.out.println(2);
  51. System.out.println("被连接的服务器对象个数:"+keySet.size());// 有多少个客户端连接服务器成功,就打印几
  52. // 处理客户端请求的代码--->暂时看不懂,先放着
  53. // 获取所有被连接的服务器Channel对象的集合
  54. /*Set<SelectionKey> keySet = selector.selectedKeys();
  55. // 遍历所有被连接的服务器Channel对象,拿到每一个SelectionKey
  56. for (SelectionKey key : keySet) {
  57. // 根据SelectionKey获取服务器Channel对象
  58. ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
  59. // 获得客户端Channel对象
  60. SocketChannel sc = ssc.accept();
  61. // 处理
  62. System.out.println("...开始处理,接受数据,代码省略...");
  63. //...
  64. }*/
  65. }
  66. }
  67. }

Selector的keys()方法

  • 获取已注册的所有通道集合 ```java public class Server3 { public static void main(String[] args) throws Exception {

    1. /*
    2. - Selector的keys()方法
    3. 获取所有被注册的服务器Channel对象的Set集合
    4. 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
    5. */
    6. // 获取ServerSocketChannel服务器通道对象
    7. ServerSocketChannel ssc1 = ServerSocketChannel.open();
    8. // 绑定端口号
    9. ssc1.bind(new InetSocketAddress(7777));
    10. // 获取ServerSocketChannel服务器通道对象
    11. ServerSocketChannel ssc2 = ServerSocketChannel.open();
    12. // 绑定端口号
    13. ssc2.bind(new InetSocketAddress(8888));
  1. // 获取ServerSocketChannel服务器通道对象
  2. ServerSocketChannel ssc3 = ServerSocketChannel.open();
  3. // 绑定端口号
  4. ssc3.bind(new InetSocketAddress(9999));
  5. // 设置非阻塞
  6. ssc1.configureBlocking(false);
  7. ssc2.configureBlocking(false);
  8. ssc3.configureBlocking(false);
  9. // 获取Selector选择器对象
  10. Selector selector = Selector.open();
  11. // 把服务器通道的accept()交给选择器来处理
  12. // 注册Channel到Selector选择器上
  13. ssc1.register(selector, SelectionKey.OP_ACCEPT);
  14. ssc2.register(selector, SelectionKey.OP_ACCEPT);
  15. ssc3.register(selector, SelectionKey.OP_ACCEPT);
  16. // 获取所有被连接的服务器Channel对象的Set集合
  17. // 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
  18. Set<SelectionKey> keySet = selector.selectedKeys();
  19. System.out.println("被连接的服务器对象有多少个:"+keySet.size());// 0
  20. // 获取所有被注册的服务器Channel对象的Set集合
  21. // 该Set集合中的元素类型是SelectionKey,该SelectionKey类其实就是对Channel的一个封装
  22. Set<SelectionKey> keys = selector.keys();
  23. System.out.println("被注册的服务器对象有多少个:"+keys.size()); // 3
  24. // 死循环一直接受客户端的连接请求
  25. while (true) {
  26. System.out.println(1);
  27. // 服务器等待客户端的连接
  28. selector.select();// 阻塞
  29. System.out.println(2);
  30. System.out.println("被连接的服务器对象个数:"+keySet.size());// 有多少个客户端连接服务器成功,就打印几
  31. System.out.println("被注册的服务器对象个数:"+keys.size());// 选择器上注册了多少个服务器Channel,就打印几
  32. }
  33. }

}

  1. <a name="5db9fd7c-24"></a>
  2. ### 小结
  3. <a name="bc78b522"></a>
  4. ## 实操--Selector多路复用
  5. <a name="e6cefb85-4"></a>
  6. ### 需求
  7. - 使用Selector进行多路复用,监听3个服务器端口
  8. <a name="72fa7c88-2"></a>
  9. ### 分析
  10. - 创建3个服务器通道,设置成非阻塞
  11. - 获取Selector选择器
  12. - 把Selector注册到三个服务器通道上
  13. - 循环去等待客户端连接
  14. - 遍历所有被连接的服务器通道集合
  15. - 处理客户端请求
  16. <a name="38164c8b-4"></a>
  17. ### 实现
  18. -
  19. 案例:
  20. ```java
  21. public class Server1 {
  22. public static void main(String[] args) throws Exception {
  23. /*
  24. 需求: 使用Selector进行多路复用,监听3个服务器端口
  25. 分析:
  26. 1.创建3个服务器Channel对象,并绑定端口号
  27. 2.把3个服务器Channel对象设置成非阻塞
  28. 3.获得Selector选择器
  29. 4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
  30. 5.死循环去等待客户端的连接
  31. 6.获取所有被连接的服务器Channel对象的Set集合
  32. 7.循环遍历所有被连接的服务器Channel对象
  33. 8.处理客户端的请求
  34. */
  35. // 1.创建3个服务器Channel对象,并绑定端口号
  36. ServerSocketChannel ssc1 = ServerSocketChannel.open();
  37. ssc1.bind(new InetSocketAddress(7777));
  38. ServerSocketChannel ssc2 = ServerSocketChannel.open();
  39. ssc2.bind(new InetSocketAddress(8888));
  40. ServerSocketChannel ssc3 = ServerSocketChannel.open();
  41. ssc3.bind(new InetSocketAddress(9999));
  42. // 2.把3个服务器Channel对象设置成非阻塞
  43. ssc1.configureBlocking(false);
  44. ssc2.configureBlocking(false);
  45. ssc3.configureBlocking(false);
  46. // 3.获得Selector选择器
  47. Selector selector = Selector.open();
  48. // 4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
  49. ssc1.register(selector, SelectionKey.OP_ACCEPT);
  50. ssc2.register(selector, SelectionKey.OP_ACCEPT);
  51. ssc3.register(selector, SelectionKey.OP_ACCEPT);
  52. // 5.死循环去等待客户端的连接
  53. while (true) {
  54. // 服务器等待客户端连接
  55. System.out.println(1);
  56. selector.select();
  57. // 6.获取所有被连接的服务器Channel对象的Set集合
  58. Set<SelectionKey> keySet = selector.selectedKeys(); // 2
  59. // 7.循环遍历所有被连接的服务器Channel对象,获取每一个被连接的服务器Channel对象
  60. for (SelectionKey key : keySet) {// 遍历出7777端口 8888端口
  61. // 8.由于SelectionKey是对Channel的封装,所以我们得根据key获取被连接的服务器Channel对象
  62. ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
  63. // 9.处理客户端的请求
  64. // 9.1 获取连接的客户端对象
  65. SocketChannel sc = ssc.accept();
  66. // 9.2 创建ByteBuffer缓冲数组
  67. ByteBuffer b = ByteBuffer.allocate(1024);
  68. // 9.3 读取数据
  69. int len = sc.read(b);// 把读取到的字节数据存储到b缓冲数组中,返回读取到的字节个数
  70. // 9.4 打印输出
  71. System.out.println(new String(b.array(), 0, len));
  72. // 10. 释放资源
  73. sc.close();
  74. }
  75. }
  76. /*
  77. - 问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,
  78. 导致在遍历集合时,遍历到已经没用的对象,出现了异常
  79. - 解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历
  80. */
  81. }
  82. }
  • 问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,导致在遍历集合时,遍历到已经没用的对象,出现了异常

  • 解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历

  • 代码如下:

    1. public class Server2 {
    2. public static void main(String[] args) throws Exception {
    3. /*
    4. 需求: 使用Selector进行多路复用,监听3个服务器端口
    5. 分析:
    6. 1.创建3个服务器Channel对象,并绑定端口号
    7. 2.把3个服务器Channel对象设置成非阻塞
    8. 3.获得Selector选择器
    9. 4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
    10. 5.死循环去等待客户端的连接
    11. 6.获取所有被连接的服务器Channel对象的Set集合
    12. 7.循环遍历所有被连接的服务器Channel对象
    13. 8.处理客户端的请求
    14. - 问题: Selector把所有被连接的服务器对象放在了一个Set集合中,但是使用完后并没有删除,
    15. 导致在遍历集合时,遍历到已经没用的对象,出现了异常
    16. - 解决办法: 使用完了,应该从集合中删除,由于遍历的同时不能删除,所以使用迭代器进行遍历
    17. */
    18. // 1.创建3个服务器Channel对象,并绑定端口号
    19. ServerSocketChannel ssc1 = ServerSocketChannel.open();
    20. ssc1.bind(new InetSocketAddress(7777));
    21. ServerSocketChannel ssc2 = ServerSocketChannel.open();
    22. ssc2.bind(new InetSocketAddress(8888));
    23. ServerSocketChannel ssc3 = ServerSocketChannel.open();
    24. ssc3.bind(new InetSocketAddress(9999));
    25. // 2.把3个服务器Channel对象设置成非阻塞
    26. ssc1.configureBlocking(false);
    27. ssc2.configureBlocking(false);
    28. ssc3.configureBlocking(false);
    29. // 3.获得Selector选择器
    30. Selector selector = Selector.open();
    31. // 4.把3个个服务器Channel对象对象注册到同一个Selector选择器上,指定监听事件
    32. ssc1.register(selector, SelectionKey.OP_ACCEPT);
    33. ssc2.register(selector, SelectionKey.OP_ACCEPT);
    34. ssc3.register(selector, SelectionKey.OP_ACCEPT);
    35. // 5.死循环去等待客户端的连接
    36. while (true) {
    37. // 服务器等待客户端连接
    38. System.out.println(1);
    39. selector.select();
    40. // 6.获取所有被连接的服务器Channel对象的Set集合
    41. Set<SelectionKey> keySet = selector.selectedKeys();
    42. // 7.循环遍历所有被连接的服务器Channel对象,获取每一个被连接的服务器Channel对象
    43. Iterator<SelectionKey> it = keySet.iterator();
    44. // 迭代器的快捷键: itit
    45. while (it.hasNext()){
    46. // 遍历出来的SelectionKey
    47. SelectionKey key = it.next();
    48. // 8.由于SelectionKey是对Channel的封装,所以我们得根据key获取被连接的服务器Channel对象
    49. ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
    50. // 9.处理客户端的请求
    51. // 9.1 获取连接的客户端对象
    52. SocketChannel sc = ssc.accept();
    53. // 9.2 创建ByteBuffer缓冲数组
    54. ByteBuffer b = ByteBuffer.allocate(1024);
    55. // 9.3 读取数据
    56. int len = sc.read(b);// 把读取到的字节数据存储到b缓冲数组中,返回读取到的字节个数
    57. // 9.4 打印输出
    58. System.out.println(new String(b.array(), 0, len));
    59. // 10. 释放资源
    60. sc.close();
    61. // 用完了就得删除
    62. it.remove();
    63. }
    64. }
    65. }
    66. }

小结

第六章 NIO2-AIO(异步、非阻塞)

知识点—AIO概述

目标

  • 了解AIO的概述

路径

  • 同步,异步,阻塞,非阻塞概念回顾
  • AIO相关类和方法介绍

讲解

同步,异步,阻塞,非阻塞概念回顾

  1. - 同步:调用方法之后,必须要得到一个返回值。
  2. - 异步:调用方法之后,没有返回值,但是会有回调函数。回调函数指的是满足条件之后会自动执行的方法
  3. - 阻塞:如果没有达到方法的目的,就一直停在这里【等待】。
  4. - 非阻塞:不管有没有达到目的,都直接【往下执行】。

day14【网络编程和NIO】 - 图12

AIO相关类和方法介绍

AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。

但是对AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2——>AIO,主要在Java.nio.channels包下增加了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户连接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。

在AIO编程中,发出一个事件(accept read write.connect等)之后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。

void completed(V result, A attachment); 成功

void failed(Throwable exc, A attachment);

小结

实操—AIO 同步连接同步读(没有意义,不要求写)

需求

  • AIO同步写法,读取客户端写过来的数据

分析

  • 获取AsynchronousServerSocketChannel对象,绑定端口
  • 同步接收客户端请求
  • 读取数据

实现

  1. public static void main(String[] args) throws Exception {
  2. //创建对象
  3. AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
  4. //绑定端口
  5. assc.bind(new InetSocketAddress(8888));
  6. //获取连接
  7. //Future里面放的就是方法的结果
  8. //********同步********
  9. System.out.println("准备连接客户端");
  10. Future<AsynchronousSocketChannel> future = assc.accept();
  11. //Future方法需要调用get()方法获取真正的返回值
  12. AsynchronousSocketChannel sc = future.get();
  13. System.out.println("连接上了客户端");
  14. //读取客户端发来的数据
  15. ByteBuffer buffer = ByteBuffer.allocate(1024);
  16. //读取
  17. //以前返回的是读取到的个数,真正的个数就在Future里面放着
  18. //********同步********
  19. System.out.println("准备读取数据");
  20. Future<Integer> future2 = sc.read(buffer);
  21. //获取真正的返回值
  22. Integer len = future2.get();
  23. System.out.println("读取到了数据");
  24. //打印
  25. System.out.println(new String(buffer.array(),0,len));
  26. }

小结

实操—AIO 异步非阻塞连接

需求

  • AIO异步非阻塞的连接方法

分析

  • 获取AsynchronousServerSocketChannel对象,绑定端口
  • 异步接收客户端请求

    • void accept(A attachment, CompletionHandler handler)
    • 第一个参数: 附件,没啥用,传入null即可
    • 第二个参数: CompletionHandler抽象类 ,AIO中的事件处理类

      • void completed(V result, A attachment);异步连接成功,就会自动调用这个方法
      • void failed(Throwable exc, A attachment);异步连接失败,就会自动调用这个方法

实现

  • 服务器端:
  1. public static void main(String[] args) throws IOException {
  2. //服务器异步的连接方法
  3. //创建对象
  4. AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
  5. //绑定端口
  6. assc.bind(new InetSocketAddress(8000));
  7. //【异步非阻塞】方式!!!!!连接客户端
  8. System.out.println("11111111111");
  9. assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
  10. @Override
  11. //回调函数,当成功连接了客户端之后,会自动回来调用这个方法
  12. public void completed(AsynchronousSocketChannel result, Object attachment) {
  13. //completed是完成,如果连接成功会自动调用这个方法
  14. System.out.println("completed");
  15. }
  16. @Override
  17. public void failed(Throwable exc, Object attachment) {
  18. //failed是失败,如果连接失败会自动调用这个方法
  19. }
  20. });
  21. System.out.println("22222222222");
  22. //写一个死循环让程序别结束(因为程序结束了无法演示效果)
  23. while(true){
  24. }
  25. }

小结

实操—AIO 异步非阻塞连接和异步读

需求

  • 实现异步连接,异步读

分析

  • 获取AsynchronousServerSocketChannel对象,绑定端口
  • 异步接收客户端请求
  • 在CompletionHandler的completed方法中异步读数据

实现

  • 服务器端代码:
  1. //异步非阻塞连接和读取
  2. public static void main(String[] args) throws IOException {
  3. //创建对象
  4. AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
  5. //绑定端口
  6. assc.bind(new InetSocketAddress(8000));
  7. //异步非阻塞连接!!!!
  8. //第一个参数是一个附件
  9. System.out.println(1);
  10. assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
  11. @Override
  12. public void completed(AsynchronousSocketChannel s, Object attachment) {
  13. //如果连接客户端成功,应该获取客户端发来的数据
  14. //completed()的第一个参数表示的是Socket对象.
  15. System.out.println(5);
  16. //创建数组
  17. ByteBuffer buffer = ByteBuffer.allocate(1024);
  18. //异步非阻塞读!!!!!
  19. System.out.println(3);
  20. s.read(buffer, null, new CompletionHandler<Integer, Object>() {
  21. @Override
  22. public void completed(Integer len, Object attachment) {
  23. //读取成功会自动调用这个方法
  24. //completed()方法的第一个参数是read()读取到的实际个数
  25. //打印数据
  26. System.out.println(6);
  27. System.out.println(new String(buffer.array(),0,len));
  28. }
  29. @Override
  30. public void failed(Throwable exc, Object attachment) {
  31. }
  32. });
  33. System.out.println(4);
  34. }
  35. @Override
  36. public void failed(Throwable exc, Object attachment) {
  37. }
  38. });
  39. System.out.println(2);
  40. //让程序别结束写一个死循环
  41. while(true){
  42. }
  43. }

小结

实操—AIO 异步非阻塞客户端请求连接(没有意思)

需求

  • 完成异步非阻塞客户端请求连接

分析

  • 创建客户端对象
  • 异步请求连接服务器

实现

  1. public class Demo客户端AIO {
  2. public static void main(String[] args) throws IOException {
  3. //客户端也有AIO对象
  4. AsynchronousSocketChannel asc = AsynchronousSocketChannel.open();
  5. System.out.println(1);
  6. //指定要连接的服务器的ip和端口
  7. asc.connect(new InetSocketAddress("123.23.44.3",8000), null, new CompletionHandler<Void, Object>() {
  8. @Override
  9. public void completed(Void result, Object attachment) {
  10. //成功就执行这个方法
  11. System.out.println(3);
  12. }
  13. @Override
  14. public void failed(Throwable exc, Object attachment) {
  15. //失败就执行这个方法
  16. System.out.println(4);
  17. }
  18. });
  19. System.out.println(2);
  20. //写一个循环让main方法别结束
  21. while(true){
  22. }
  23. }
  24. }
  25. 如果连接成功:
  26. 1
  27. 2
  28. 3
  29. 如果连接失败:
  30. 1
  31. 2
  32. 4

小结

总结

  1. - 能够辨别UDPTCP协议特点
  2. UDP: 面向无连接,传输数据不安全,速度快
  3. TCP: 面向连接,传输数据安全,速度慢
  4. - 能够说出TCP协议下两个常用类名称
  5. Socket\ServerSocket
  6. 常用方法
  7. - 能够编写TCP协议下字符串数据传输程序
  8. - 客户端实现步骤
  9. - 创建客户端Socket对象并指定服务器地址和端口号
  10. - 调用Socket对象的getOutputStream方法获得字节输出流对象
  11. - 使用字节输出流对象的write方法往服务器端输出数据
  12. - 调用Socket对象的getInputStream方法获得字节输入流对象
  13. - 调用字节输入流对象的read方法读取服务器端返回的数据
  14. - 关闭Socket对象断开连接。
  15. - 服务器实现步骤
  16. - 创建ServerSocket对象并指定端口号(相当于开启了一个服务器)
  17. - 调用ServerSocket对象的accept方法等待客端户连接并获得对应Socket对象
  18. - 调用Socket对象的getInputStream方法获得字节输入流对象
  19. - 调用字节输入流对象的read方法读取客户端发送的数据
  20. - 调用Socket对象的getOutputStream方法获得字节输出流对象
  21. - 调用字节输出流对象的write方法往客户端输出数据
  22. - 关闭SocketServerSocket对象
  23. - 能够理解TCP协议下文件上传案例
  24. 1. 【客户端】输入流,从硬盘读取文件数据到程序中。
  25. 2. 【客户端】输出流,写出文件数据到服务端。
  26. 3. 【服务端】输入流,读取文件数据到服务端程序。
  27. 4. 【服务端】输出流,写出文件数据到服务器硬盘中。
  28. 5. 【服务端】获取输出流,回写数据。
  29. 6. 【客户端】获取输入流,解析回写数据。
  30. 注意: 客户端上传文件的数据后,一定要调用shutDownOutput()方法,告诉服务器不会再写数据了
  31. - 能够理解TCP协议下BS案例 了解
  32. 1. 准备页面数据,web文件夹。
  33. 2. 我们模拟服务器端,ServerSocket类监听端口,使用浏览器访问,查看网页效果
  34. - 能够说出NIO的优点
  35. IO: 同步阻塞
  36. NIO: 同步非阻塞
  37. NIO2\AIO: 异步非阻塞