1. socket知识与IO模型

1.1 Socket

1.1.1 概述

套接字,两台主机间的连接端点,TCP/IP协议是传输层协议,解决数据传输的问题,http是应用层协议,解决数据的包装问题。socket是支持TCP/IP的网络通信的基本操作单元。是网络通信中端点的抽象表示,包含通信必须的五种信息:

  1. 连接使用的协议
  2. 本地主机的IP
  3. 本地进程的端口
  4. 远程主机的IP
  5. 远程进程的端口

    1.1.2 流程

    两个端:客户端,服务端
    首先在服务端创建ServerSocket,附加到端口上,服务器在该处监听连接。端口范围0 - 65536,但是0 - 1024是为特权服务保留的端口。
    客户端根据服务器的域名或者IP地址,加上端口,打开一个套接字。当服务器接受连接后,服务器和客户端的通信就想输入输出流一样操作。
    image.png

1.1.3 代码

1.1.3.1 服务端代码

  1. import java.io.IOException;
  2. import java.io.InputStream;
  3. import java.io.OutputStream;
  4. import java.net.ServerSocket;
  5. import java.net.Socket;
  6. import java.util.concurrent.ExecutorService;
  7. import java.util.concurrent.Executors;
  8. public class ServerDemo {
  9. public static void main(String[] args) {
  10. // 1. 创建线程池,有客户端连接就创建一个线程
  11. ExecutorService executorService = Executors.newCachedThreadPool();
  12. // 2. 创建 ServerSocket对象
  13. ServerSocket serverSocket = new ServerSocket(9999);
  14. while(true){
  15. // 3. 监听客户端
  16. Socket socket = serverSocket.accept();
  17. // 4. 开启新的线程处理
  18. executorService.execute(new Runnable(){
  19. @Override
  20. public void run() {
  21. handle(socket);
  22. }
  23. });
  24. }
  25. }
  26. public static void handle(Socket socket){
  27. try {
  28. // 从连接中取出输入流来接收消息
  29. InputStream is = socket.getInputStream();
  30. byte[] b = new byte[1024];
  31. int read = is.read(b);
  32. System.out.println("客户端: " + new String(b, 0, read));
  33. // 连接中取出输出流回话
  34. OutputStream os = socket.getOutputStream();
  35. os.write("没钱".getBytes());
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. } finally {
  39. try {
  40. // 关闭连接
  41. socket.close();
  42. } catch (IOException e) {
  43. e.printStackTrace();
  44. }
  45. }
  46. }
  47. }

1.1.3.1 客户端代码

  1. package com.lagou.client;
  2. import java.io.InputStream;
  3. import java.io.OutputStream;
  4. import java.net.Socket;
  5. import java.util.Scanner;
  6. public class ClientDemo {
  7. public static void main(String[] args) throws Exception {
  8. while (true) {
  9. // 1.创建 Socket 对象
  10. Socket s = new Socket("127.0.0.1", 9999);
  11. // 2.从连接中取出输出流并发消息
  12. OutputStream os = s.getOutputStream();
  13. System.out.println("请输入:");
  14. Scanner sc = new Scanner(System.in);
  15. String msg = sc.nextLine();
  16. os.write(msg.getBytes());
  17. // 3.从连接中取出输入流并接收回话
  18. InputStream is = s.getInputStream();
  19. byte[] b = new byte[1024];
  20. int read = is.read(b);
  21. System.out.println("老板说:" + new String(b, 0, read).trim());
  22. // 4.关闭
  23. s.close();
  24. }
  25. }
  26. }

1.2 IO模型

1.2.1 说明

简单理解:用什么样的通道进行数据的发送和接收,决定了程序通信的性能

阻塞与非阻塞:
访问IO的线程是否会等待线程访问资源,该资源是否准备就绪的一种处理方式
image.png
同步和异步:
指数据的请求方式,是访问数据的一种机制
image.png

1.2.2 BIO(同步 - 阻塞)

传统的socket编程。
blocking io:
同步阻塞,服务器实现模式为一个连接,一个线程,如果连接不做任何事情会造成线程开销,可以使用线程池进行改善。
举例:
人在烧水,这个人一直在观察水烧开没有,中途无法做其它事情
问题分析:
1、每个请求都需要线程
2、并发数较大时,需要大量线程,系统资源占用较大
3、连接建立后,如果线程暂时没有数据可读,线程会阻塞在read操作上,造成资源浪费

1.2.3 NIO(同步 - 非阻塞)

服务器实现模式为一个线程处理多个请求,客户端发送的连接请求都会注册到多路复用器上,多路复用器会轮训到连接有IO请求就进行处理
image.png
举例:
烧水时,一个人轮训查看多个烧水壶的状态,而不再是,只专注于一个烧水壶

1.2.4 AIO(异步 - 非阻塞)

引入了异步通道的概念,采用了Proactor模式,简化程序编写,有效的请求才会启动线程,特点是先由操作系统完成后才通知服务端程序去处理,一般适用于连接数较多且连接时间较长的应用
Proactor模式是一个消息异步通知的模式,Proactor通知的不是就绪事件,而是操作完成事件,这也就是操作系统异步IO的主要模型
image.png

1.2.5 BIO、NIO、AIO 场景分析

1、BIO适用于连接数较小且固定的架构,对服务器资源要求较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解
2、NIO适用于连接数较多且连接比较短(轻应用)的架构,比如聊天服务器,弹幕系统,服务器间通讯。编程复杂,JDK1.4开始支持
3、AIO适用于连接数较多且连接比较长(重应用)的架构,比如相册服务器,充分调用OS参与并发操作,编程复杂,JDK1.7开始支持

2. NIO编程

2.1 NIO介绍

全程non-blocking IO,JDK1.4开始,java提供了一些列改进的输入输出的新特性,统称为NIO,即new IO
1、三大核心:Channel,Buffer,Selector
2、面向缓冲区编程,数据读取到缓冲区中,需要时可在缓冲区中移动,增加处理过程的灵活性,可以提供非阻塞的高伸缩性网络
3、非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不获取,而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1w个请求,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,必须1w个。

2.2 NIO和BIO的比较

1、BIO以流的方式处理数据,而NIO以缓冲区的方式处理数据,缓冲区IO的效率比流IO高很多
2、BIO是阻塞的,NIO是非阻塞的
3、BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(例如:连接,数据到达等),因此使用单个线程就可以监听多个客户端通道。

2.3 NIO三大核心原理

image.png

1、每个Channel都会对应一个Buffer
2、Selector对应一个线程,一个线程对应多个Channel
3、每个Channel都注册到Selector选择器上
4、Selector不断轮询查看Channel上的事件,事件是Channel非常重要的概念
5、Selector会根据不同的事件,完成不同的处理操作
6、Buffer就是一个内存块,底层是一个数组
7、数据的读取写书通过Buffer完成,BIO通过输入输出流完成,不能双向,Buffer可以读写,channel是双向的

2.4 缓冲区(Buffer)

2.4.1 介绍

本质上是一个读写数据的内存块,这个对象提供了一些方法,更轻松的使用内存块,内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从网络读取数据的渠道,但是数据必须都经过Buffer。

2.4.2 常用API

  1. Buffer及其子类

image.png
在NIO中,Buffer是顶层父类,抽象类,常用缓冲区分别对应byte,short,int,long,float,double,char7种

  1. 缓冲区对象创建 | 方法名 | 说明 | | —- | —- | | static ByteBuffer allocate(长度) | 创建byte类型的指定长度的缓冲区 | | static ByteBuffer wrap(byte[] array) | 创建一个有内容的byte类型缓冲区 |

  2. 缓冲区对象添加数据 | 方法名 | 说明 | | —- | —- | | int position()/position(int newPosition) | 获得当前要操作的索引/修改当前要操作的索引位 置 | | int limit()/limit(int newLimit) | 最多能操作到哪个索引/修改最多能操作的索引位 置 | | int capacity() | 返回缓冲区的总长度 | | int remaining()/boolean hasRemaining() | 还有多少能操作索引个数/是否还有能操作 | | put(byte b)/put(byte[] src) | 添加一个字节/添加字节数组 |

图解:
image.png

  1. 缓冲区对象读取数据 | 方法名 | 介绍 | | —- | —- | | flip() | 写切换读模式 limit设置position位置, position设置0 | | get() | 读一个字节 | | get(byte[] dst) | 读多个字节 | | get(int index) | 读指定索引的字节 | | rewind() | 将position设置为0,可以重复读 | | clear() | 切换写模式 position设置为0 , limit 设置为 capacity | | array() | 将缓冲区转换成字节数组返回 |

图解:flip方法
image.png
图解:clear()方法
image.png

  1. 注意事项
    1. capacity:容量(长度)limit: 界限(最多能读/写到哪里)posotion:位置(读/写 哪个索引)
    2. 获取缓冲区里面数据之前,需要调用flip方法
    3. 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了 才会消失。

2.5 通道(Channel)

2.5.1 介绍

  1. 通道可以读也可以写,流基本是单向的
  2. 通道可以异步读写
  3. 通道总是使用缓冲区进行读写

    2.5.2 Channel常用类介绍

  4. Channel接口,实现类

    1. FileChannel,文件处理
    2. DatagramChannel,UDP数据处理
    3. ServerSocketChannel,TCP数据处理
    4. SocketChannel,TCP数据处理
  5. SocketChannel与ServerSocketChannel
    1. 类似Socket与ServerSocket,可以实现客户端与服务端的通信工作

      2.5.3 ServerSocketChannel

      步骤:
      1、打开一个服务端通道
      2、绑定端口号
      3、通道默认阻塞,设置为非阻塞
      4、检查是否有客户端连接,有连接返回对应通道
      5、获取客户端传递过来的数据,把数据放入ByteBuffer中
      6、给客户端返回数据
      7、释放资源
      代码: ```java package com.lagou.channel; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; /**
  • 服务端 */ public class NIOServer { public static void main(String[] args) throws IOException, InterruptedException {
    1. //1. 打开一个服务端通道
    2. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    3. //2. 绑定对应的端口号
    4. serverSocketChannel.bind(new InetSocketAddress(9999));
    5. //3. 通道默认是阻塞的,需要设置为非阻塞
    6. // true 为通道阻塞 false 为非阻塞
    7. serverSocketChannel.configureBlocking(false);
    8. System.out.println("服务端启动成功..........");
    9. while (true) {
    10. //4. 检查是否有客户端连接 有客户端连接会返回对应的通道 , 否则返回null
    11. SocketChannel socketChannel = serverSocketChannel.accept();
    12. if (socketChannel == null) {
    13. System.out.println("没有客户端连接...我去做别的事情");
    14. Thread.sleep(2000);
    15. continue;
    16. }
    17. //5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
    18. ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    19. //返回值:
    20. //正数: 表示本次读到的有效字节个数.
    21. //0 : 表示本次没有读到有效字节.
    22. //-1 : 表示读到了末尾
    23. int read = socketChannel.read(byteBuffer);
    24. System.out.println("客户端消息:" +
    25. new String(byteBuffer.array(), 0, read,
    26. StandardCharsets.UTF_8));
    27. //6. 给客户端回写数据
    28. socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
    29. //7. 释放资源
    30. socketChannel.close();
    31. }
    } } ```

    2.5.4 SocketChannel

    步骤:
    1、打开通道
    2、设置连接IP和端口
    3、写出数据
    4、读取服务器写回的数据
    5、释放资源
    代码: ```java package com.lagou.channel; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; /**
  • 客户端 */ public class NIOClient { public static void main(String[] args) throws IOException {
    1. //1.打开通道
    2. SocketChannel socketChannel = SocketChannel.open();
    3. //2.设置连接IP和端口号
    4. socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
    5. //3.写出数据
    6. socketChannel.write(ByteBuffer.wrap("老板, 该还钱拉!".getBytes(StandardCharsets.UTF_8)));
    7. //4.读取服务器写回的数据
    8. ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    9. int read=socketChannel.read(readBuffer);
    10. System.out.println("服务端消息:" + new String(readBuffer.array(), 0, read,
    11. StandardCharsets.UTF_8));
    12. //5.释放资源
    13. socketChannel.close();
    } } ```

2.6 Selector(选择器)

2.6.1 介绍

使用一个线程处理多个客户端的连接,就会使用到NIO的Selector,Selector能够检测多个注册的服务端通道上是否有事件发生,如果有,便获取事件然后针对每个事件进行响应的处理。这样就可以使用一个线程去管理多个通道,也就是可以管理多个连接和请求

没有选择器时:
每个连接对应一个处理线程,但是连接并不能马上就会发送消息,所以还会产生资源浪费
image.png
有选择器时:
在通道有读写事件发生时,才会进行读写,就很大程度的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免线程之间的上下文切换导致的开销。
image.png

2.6.2 常用API

  1. Selector抽象类
    1. Selector.open():得到一个选择器对象
    2. Selector.select():阻塞,监控所有注册的通道,当有事件发生时,会将SelectionKey放入集合内部并返回事件数量
    3. Selector.select(1000):阻塞1000ms
    4. Selector.selectedKeys():返回存有selectionKey的集合
  2. SelectionKey
    1. isAcceptable:是否连接继续事件
    2. isConnectable:是否连接就绪事件
    3. isReadable:是否读就绪事件
    4. isWriteable:是否写就绪事件
  3. SelectionKey中定义的4种事件
    1. OP_ACCEPT:连接继续,表示服务器监听到了客户连接,服务器可以接收这个连接
    2. OP_CONNECT:连接就绪,表示客户端与服务器的连接已经建立
    3. OP_READ:读就绪事件,表示通道中有可读的数据,可以执行读操作
    4. OP_WRITE:写就绪事件,表示可以向通道写数据

      2.6.3 Selector编码

      步骤:
      1、打开服务端通道
      2、绑定端口
      3、默认阻塞,设置为非阻塞
      4、创建选择器
      5、将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
      6、检查选择器是否有事件
      7、获取事件集合
      8、判断事件是否是客户端连接事件SelectionKey.isAcceptable()
      9、得到客户端通道,并将通道注册到选择器上,并指定监听事件为OP_READ
      10、判断是否是客户端读就绪事件SelectionKey.isReadable()
      11、得到客户端通道,读取数据到缓冲区
      12、给客户端回写数据
      13、从集合中删除对应的事件,因为防止二次处理
      代码: ```java package com.lagou.selector; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.Set; /**
  • 服务端-选择器 */ public class NIOSelectorServer { public static void main(String[] args) throws IOException, InterruptedException {
    1. //1. 打开一个服务端通道
    2. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    3. //2. 绑定对应的端口号
    4. serverSocketChannel.bind(new InetSocketAddress(9999));
    5. //3. 通道默认是阻塞的,需要设置为非阻塞
    6. serverSocketChannel.configureBlocking(false);
    7. //4. 创建选择器
    8. Selector selector = Selector.open();
    9. //5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
    10. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    11. System.out.println("服务端启动成功...");
    12. while (true) {
    13. //6. 检查选择器是否有事件
    14. int select = selector.select(2000);
    15. if (select == 0) {
    16. continue;
    17. }
    18. //7. 获取事件集合
    19. Set<SelectionKey> selectionKeys = selector.selectedKeys();
    20. Iterator<SelectionKey> iterator = selectionKeys.iterator();
    21. while (iterator.hasNext()) {
    22. //8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
    23. SelectionKey key = iterator.next();
    24. //9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
    25. if (key.isAcceptable()) {
    26. SocketChannel socketChannel = serverSocketChannel.accept();
    27. System.out.println("客户端已连接......" + socketChannel);
    28. //必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
    29. socketChannel.configureBlocking(false);
    30. //并指定监听事件为OP_READ
    31. socketChannel.register(selector, SelectionKey.OP_READ);
    32. }
    33. //10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
    34. if (key.isReadable()) {
    35. //11.得到客户端通道,读取数据到缓冲区
    36. SocketChannel socketChannel = (SocketChannel) key.channel();
    37. ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    38. int read = socketChannel.read(byteBuffer);
    39. if (read > 0) {
    40. System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read,
    41. StandardCharsets.UTF_8));
    42. //12.给客户端回写数据
    43. socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
    44. socketChannel.close();
    45. }
    46. }
    47. //13.从集合中删除对应的事件, 因为防止二次处理.
    48. iterator.remove();
    49. }
    50. }
    } } ```

其他文章

3. Netty核心原理
4. Netty高级应用
5. Netty核心源码剖析
6. 自定义RPC框架