Java NIO梳理,Java Socket梳理,Java IO流学习总结

几个要点:

  • 解决什么问题?在此之前怎么解决的?
  • 怎么使用?
  • 什么原理?

Java IO 与 Java NIO:

  • Java传统IO是基于流(Stream)的IO
  • 从JDK1.4开始提供基于块的IO,即NIO(New IO)。

IO本质上是byte的流动,而流(Stream)是byte移动的载体,从源点向目的点移动数据。源点和目的点,既可以是内存区域、磁盘文件、也可以是一个URL,只要能代表位置就可以。根据流的方向,流可以分为:输入流和输出流,从输入流读取数据,向输出流写入数据。

Java对io的支持主要集中在io包下,两类:

  • 基于byte的io接口:InputStream、OutputStream;
  • 基于char的io接口:Writer、Reader;(基于byte的io接口 + 编码方式)

无论网络传输还是磁盘读写,最小的存储单元都是byte。但程序中操作的数据大都是char形式的,所以Java也提供了char的Stream。上述是IO流本身的支持:流的形态、流内部装的什么,但流并不等于IO,还有重要的一点:数据从哪里来?写到哪里去?主要是一下两种:

  • 基于磁盘操作的io接口:File
  • 基于网络操作的io接口:Socket

io中数据写到何处也是重要的一点,其中最主要的就是将数据持久化到磁盘。数据在磁盘上最小的描述就是文件,上层应用对磁盘的读和写都是针对文件而言的。在java中,以File类来表示文件,如:

  1. File file = new File("D:/test.txt");

但是严格来说,File并不表示一个真实的存在于磁盘上的文件。就像上面代码的文件其实并不存在,File做的只是根据你所提供的文件描述符,返回某一路径的虚拟对象,它并不关心文件或路径是否存在,可能存在,也可能是捏造的。就好象一张名片,名片的背后代表的是人。为什么要这么设计?在我看来还是要提高访问磁盘的效率,有点延迟加载的意思。大部分情况下,我们最关心的并不是文件存不存在,而是文件要如何操作。比如你手里有很多名片,你可能更关心的是有没有某某局长的名片,而只有在需要联系时,才发现名片是假的。也就是关心名片本身要强过名片的真伪。

以FileInputStream读取文件为例,过程是这样的:当传入一个文件路径时,会根据这个路径创建File对象,作为这个文件的一个“名片”。当我们试图通过FileInputStream对象去操作文件的时候,将会真正创建一个关联真实存在的磁盘文件的文件描述符FileDescriptor,通过FileInputStream构造方法可以看出:

  1. fd = new FileDescriptor();

如果说File是文件的名片,那么FileDescriptor就是真正指向了一个打开的文件,可以操作磁盘文件。例如FileDescriptor.sync()方法可以将缓存中的数据强制刷新到磁盘文件中。如果我们需要读取的是字符,还需要通过StreamDecoder类将字节解码成字符。至于如何从物理磁盘上读取数据,那就是操作系统做的事情了。过程如图(图摘自网上):

Socket要说起来并不那么形象,它的中文翻译是“插座”,至于“套接字”这个翻译我实在不知道从何而来。可以这样理解插座的概念,由于本身有电网的存在,如果我们买了一台新电器,我们只要插上插座连接到电网上就能够使用。Socket就像一个插座,计算机通过Socket就能和网络或者其他计算机上进行通讯;当有数据通讯的需求时,只需要建立一个Socket“插座”,通过网卡与其他计算机相连获取数据。

Socket位于传输层和应用层之间,向应用层统一提供编程接口,应用层不必知道传输层的协议细节。Java中对Socket的支持主要是以下两种:

  • 基于TCP的Socket:提供给应用层可靠的流式数据服务,使用TCP的Socket应用程序协议:BGP,HTTP,FTP,TELNET等。优点:基于数据传输的可靠性。
  • 基于UDP的Socket:适用于数据传输可靠性要求不高的场合。基于UDP的Socket应用程序协议:RIP,SNMP,L2TP等。

大部分情况下我们使用的都是基于TCP/IP协议的流Socket,因为它是一种稳定的通信协议。以此为例:

一台计算机要和另一台计算机进行通讯,获取其上应用程序的数据,必须通过Socket建立连接,要知道对方的IP和端口号。建立一个Socket连接需要通过底层TCP/IP协议来建立TCP连接,而建立TCP连接必须通过底层IP协议根据给定的IP在网络中找到目标主机。目标计算机上可能跑着多个应用,所以我们必须要根据端口号来制定目标应用程序,这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。

那么Socket是如何建立通讯链路的呢?

假设有一台计算机作为客户端,另一台作为服务端。当客户端需要向服务端通信,客户端首先要创建一个Socket实例:

  1. Socket socket = new Socket("127.0.0.1",1234);

若没有指定端口号,操作系统将为这个Socket实例分配一个没有被使用的本地端口号。此外创建了一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭,代码如下:

  1. public Socket(String host, int port)
  2. throws UnknownHostException, IOException
  3. {
  4. this(host != null ? new InetSocketAddress(host, port) :
  5. new InetSocketAddress(InetAddress.getByName(null), port),
  6. (SocketAddress) null, true);
  7. }

客户端试图和服务端建立TCP连接,此时会进行三次握手。

  • 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
  • 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

完成三次握手后Socket的构造函数成功返回,Socket实例创建完毕。

互联网是一种尽力而为(best-effort)的网络,客户端的起始消息或服务器端的回复消息都可能在传输过程中丢失。出于这个原因,TCP 协议实现将以递增的时间间隔重复发送几次握手消息。如果TCP客户端在一段时间后还没有收到服务器的返回消息,则发生超时并放弃连接。这种情况下,构造函数将抛出IOException 异常。

而服务端也需要创建与之对应的ServerSocket,ServerSocket的创建比较简单,只需要指定端口号:

  1. ServerSocket serverSocket = new ServerSocket(10001);

   同时操作系统也会为ServerSocket实例创建一个底层数据结构:

  1. bind(new InetSocketAddress(bindAddr, port), backlog);  //见构造方法(backlog:最大客户端等待队列)

这个数据结构中包含指定监听的端口号和包含监听地址的通配符,通常情况下是监听所有地址,下面是比较典型的ServerSocket代码:

  1. public void testSocket() throws Exception
  2. {
  3. ServerSocket serverSocket = new ServerSocket(10002);
  4. Socket socket = null;
  5. try
  6. {
  7. while (true)
  8. {
  9. socket = serverSocket.accept();
  10. System.out.println("socket连接:" + socket.getRemoteSocketAddress().toString());
  11. BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  12. while(true)
  13. {
  14. String readLine = in.readLine();
  15. System.out.println("收到消息" + readLine);
  16. if("end".equals(readLine))
  17. {
  18. break;
  19. }
  20. }
  21. in.close();
  22. socket.close();
  23. }
  24. }
  25. catch (SocketException se)
  26. {
  27. System.out.println("客户端断开连接");
  28. }
  29. catch (IOException e)
  30. {
  31. e.printStackTrace();
  32. }
  33. finally
  34. {
  35. System.out.println("socket关闭:" + socket.getRemoteSocketAddress().toString());
  36. socket.close();
  37. }
  38. }

当调用accept()方法时,服务端将进入阻塞状态,等待客户端的请求。当有客户端请求到来时,将为这个链接创建一个套接字数据结构,包括请求客户端的地址和端口号。该数据结构将被关联到ServerSocket实例的一个未连接列表里。此时连接并没有成功建立,处于三次握手阶段,Socket构造函数并未成功返回。当三次握手成功后,会将Socket实例对应的数据结构从未完成列表移到完成列表中。所以 ServerSocket 所关联的列表中每个数据结构,都代表与一个客户端的建立的 TCP 连接。(client链接,两个列表:等待列表、完成列表)

当连接成功创建后,我们要做的就是传输数据,这才是主要目的。如上例代码,在客户端和服务端都有一个Socket实例,而每个Socket实例都会拥有一个InputStream和OutputStream,我们正是通过它们传输数据。当Socket对象创建时,操作系统将会为InputStream和OutputStream分别分配一定大小的缓冲区,数据的写入和读取都是通过缓存区完成的。发送端的缓冲区称之为SendQ,是一个FIFO的队列,接收端的缓冲区称之为RecvQ,同样也是FIFO队列。

数据传输时,发送端将数据写入到OutputStream对应的SendQ队列中,以字节为单位发送到接收端InputStream的RecvQ队列中。当SendQ队列填满时,发送端的write方法将会阻塞住;而当RecvQ队列中没有数据时,接收端的read方法也将被阻塞。

几个疑问:

  • server端有单独的监听Thread?RE:一定有;
  • 每一个client发起一个连接请求,server端都新建一个Thread?RE:不一定,看具体处理措施;
  • socket对应OSI七层协议模型中的哪一层?RE:socket不是协议,是应用层与传输层之间的抽象层,是编程接口;(七层协议:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)

下图中,服务器端ServerSocket独占一个线程,负责监听Client发送过来的连接请求,并为每个Client请求新建一个处理线程;Socket工作模式的细节,参考:Java Socket梳理

为什么会产生Java NIO(JDK 1.4+)?因为传统Java IO,特别是基于网络的IO操作(socket),有几个特点:

  • 阻塞IO:
    • 负责监听的线程:ServerSocket.accept()线程阻塞,一直等待新的client连接请求;(阻塞:没有数据也保持监听)
    • 处理client连接请求的线程:clientSocket.getInputStream.read(),是阻塞的;即使clientSocket.getInputStream – InputStreamReader – BufferedReader.readLine()也是阻塞的;
  • 阻塞IO,同时带来Thread间频繁的上下文切换,并且大部分上下文切换是无意义的;(NIO减少不必要的Thread上下文切花)
  • 每接收一个请求,都新建一个Thread;(NIO也是这样)

简要对比如下表:

Java IO Java NIO 说明
面向流 面向缓冲区 面向缓冲区,数据移动方便,处理灵活,但处理复杂
阻塞IO 非阻塞IO 用户线程read()、write()操作不阻塞
- 选择器 单线程管理多通道,提升效率

简单来说:

NIO使人们只用一个或几个单线程,就可以管理多个通道(网络连接或文件),但代价是解析数据可能比从一个阻塞流中读取数据更为复杂。

适用场景:Java IO和Java NIO的适用场景如下:

  • Java NIO:连接数多、每次传输数据量小,例如,聊天服务器,每天有成千上万的连接,这类连接每次发送少量数据;
  • Java IO:只有少量连接,但传输数据量大(占用带宽高),一次发送大量数据,例如数据库连接;

对于网络I/O,传统的阻塞式I/O,一个线程对应一个连接,采用线程池的模式在大部分场景下简单高效。当连接数茫茫多时,并且数据的移动非常频繁,NIO无疑是更好的选择。

NIO标榜的是高速、可伸缩的I/O,因为它更亲近操作系统。当需求很平凡,没有太高的效率要求的时候,你看不出它的好,反而觉得NIO代码实现复杂,不易理解。选择与否全看使用的场景,这点就看使用者的权衡了。

面向流与面向缓冲

Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

思考:下面几个理解,Java NIO 缓冲区的利弊:

  • 缓冲区,在接收到的数据中进行前后移动,处理更灵活;
  • 缓冲区中增加数据时,需要判断是否覆盖原始未处理数据;
  • 传统Java IO中BufferedInputStream也有缓冲区,但此处的缓冲区是程序包装来的,而Java NIO中对应的缓冲区是由操作系统实现的,效率高;

阻塞与非阻塞IO

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

思考:下面几个理解

  • 阻塞IO、非阻塞IO:是针对用户线程来说的,只是将阻塞部分交给了系统线程;
  • 非阻塞IO的基础:缓冲区、事件机制;
    • 非阻塞 write:实际是用户线程,将数据放入缓冲区,系统线程负责从缓冲区中发送数据;
    • 非阻塞 read:用户线程发起一个read事件,主动读取,读到多少算多少;
  • 单线程管理多个通道:由于用户线程不阻塞,因此可以同时管理多个输入、输出通道(channel);

选择器(Selectors)

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

Java NIO是在jdk1.4开始使用的,它既可以说成“新I/O”,也可以说成非阻塞式I/O。下面是java NIO的工作原理:

  1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。
  2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
  3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。

具体NIO原理,参考下图:

java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。一共有以下四种事件:

事件名 对应值
服务端接收客户端事件 SelectionKey.OP_ACCEPT(16)
客户端连接服务端事件 SelectionKey.OP_CONNECT(8)
读事件 SelectionKey.OP_READ(1)
写事件 SelectionKey.OP_WRITE(4)

服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。下面是java NIO的通信模型示意图:

此处的Demo代码已经提交到GitHub上simple-web-demo下learn-java-basic工程中top.ningg.java.nio包下。

NIOServer

构造selector,并为selector上绑定channel:

  1. package top.ningg.java.nio;
  2. import java.io.IOException;
  3. import java.net.InetSocketAddress;
  4. import java.nio.ByteBuffer;
  5. import java.nio.channels.SelectionKey;
  6. import java.nio.channels.Selector;
  7. import java.nio.channels.ServerSocketChannel;
  8. import java.nio.channels.SocketChannel;
  9. import java.util.Iterator;
  10. /*
  11. * NIO服务端
  12. */
  13. public class NIOServer {
  14. private Selector selector; // 通道管理器
  15. /*
  16. * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
  17. */
  18. public void initServer(int port) throws IOException {
  19. ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 获得一个ServerSocket通道
  20. serverChannel.configureBlocking(false); // 设置通道为非阻塞
  21. serverChannel.socket().bind(new InetSocketAddress(port)); // 将该通道对应的ServerSocket绑定到port端口
  22. this.selector = Selector.open(); // 获得一个通道管理器
  23. // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
  24. // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
  25. serverChannel.register(selector, SelectionKey.OP_ACCEPT);
  26. }
  27. /*
  28. * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
  29. */
  30. @SuppressWarnings("unchecked")
  31. public void listen() throws IOException {
  32. System.out.println("服务端启动成功!");
  33. while (true) { // 轮询访问selector
  34. selector.select(); // 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
  35. Iterator ite = this.selector.selectedKeys().iterator(); // 获得selector中选中的项的迭代器,选中的项为注册的事件
  36. while (ite.hasNext()) {
  37. SelectionKey key = (SelectionKey) ite.next();
  38. ite.remove();
  39. if (key.isAcceptable()) { // 客户端请求连接事件
  40. ServerSocketChannel server = (ServerSocketChannel) key.channel();
  41. SocketChannel channel = server.accept(); // 获得和客户端连接的通道
  42. channel.configureBlocking(false);
  43. channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
  44. // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
  45. channel.register(this.selector, SelectionKey.OP_READ);
  46. } else if (key.isReadable()) { // 获得了可读的事件
  47. read(key);
  48. }
  49. }
  50. }
  51. }
  52. /*
  53. * 处理读取客户端发来的信息 的事件
  54. */
  55. public void read(SelectionKey key) throws IOException {
  56. SocketChannel channel = (SocketChannel) key.channel(); // 服务器可读取消息:得到事件发生的Socket通道
  57. ByteBuffer buffer = ByteBuffer.allocate(10); // 创建读取的缓冲区
  58. channel.read(buffer);
  59. byte[] data = buffer.array();
  60. String msg = new String(data).trim();
  61. System.out.println("服务端收到信息:" + msg);
  62. }
  63. public static void main(String[] args) throws IOException {
  64. NIOServer server = new NIOServer();
  65. server.initServer(8000);
  66. server.listen();
  67. }
  68. }

NIOClient

构造selector,并为其绑定channel以及事件:

  1. package top.ningg.java.nio;
  2. import java.io.IOException;
  3. import java.net.InetSocketAddress;
  4. import java.nio.ByteBuffer;
  5. import java.nio.channels.SelectionKey;
  6. import java.nio.channels.Selector;
  7. import java.nio.channels.SocketChannel;
  8. import java.util.Iterator;
  9. /*
  10. * NIO客户端
  11. */
  12. public class NIOClient {
  13. private Selector selector;
  14. public void initClient(String ip, int port) throws IOException {
  15. SocketChannel channel = SocketChannel.open();
  16. channel.configureBlocking(false);
  17. this.selector = Selector.open();
  18. channel.connect(new InetSocketAddress(ip, port));
  19. channel.register(selector, SelectionKey.OP_CONNECT);
  20. }
  21. @SuppressWarnings("unchecked")
  22. public void listen() throws IOException {
  23. while (true) {
  24. selector.select();
  25. Iterator ite = this.selector.selectedKeys().iterator();
  26. while (ite.hasNext()) {
  27. SelectionKey key = (SelectionKey) ite.next();
  28. ite.remove();
  29. if (key.isConnectable()) {
  30. SocketChannel channel = (SocketChannel) key.channel();
  31. if (channel.isConnectionPending()) {
  32. channel.finishConnect();
  33. }
  34. channel.configureBlocking(false);
  35. channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));
  36. channel.register(this.selector, SelectionKey.OP_READ);
  37. } else if (key.isReadable()) { // 获得了可读的事件
  38. read(key);
  39. }
  40. }
  41. }
  42. }
  43. /*
  44. * 处理读取服务端发来的信息 的事件
  45. */
  46. public void read(SelectionKey key) throws IOException {
  47. SocketChannel channel = (SocketChannel) key.channel(); // 客户端可读取消息:得到事件发生的Socket通道
  48. ByteBuffer buffer = ByteBuffer.allocate(10); // 创建读取的缓冲区
  49. channel.read(buffer);
  50. byte[] data = buffer.array();
  51. String msg = new String(data).trim();
  52. System.out.println("客户端收到信息:" + msg);
  53. }
  54. /*
  55. * 启动客户端测试
  56. */
  57. public static void main(String[] args) throws IOException {
  58. NIOClient client = new NIOClient();
  59. client.initClient("localhost", 8000);
  60. client.listen();
  61. }
  62. }

在传统的网络服务设计模式中,有两种比较经典的模式:

  • 多线程;
  • 线程池;

对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件,如下图所示:

这种模式虽然处理起来简单方便,但是由于服务器为每个client的连接都采用一个线程去处理,使得资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,直接会导致资源瓶颈,严重的可能会直接导致服务器崩溃。

因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。因此这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。

但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。

因此便出现了下面的两种高性能IO设计模式:ReactorProactor

在Reactor模式中,会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示:

从这里可以看出,上面的Java NIO就是采用Reactor模式。注意,上面的图中展示的 是顺序处理每个事件,当然为了提高事件处理速度,可以通过多线程或者线程池的方式来处理事件。

在Proactor模式中,当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成,可以得知,异步IO模型采用的就是Proactor模式。

Java NIO使用及原理分析 (四)_降龍-CSDN博客

连接->传输数据->关闭连接

一般长连接相对短连接而言的,长连接在传输完数后不关闭连接,而不断的发送包保持连接等待处理下一个数据包。

连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。

短连接、长连接的适用场景如下:

短连接:连接数多、每次传输数据量少,例如,WEB网站的http服务采用短连接,因为WEB网站的连接为频繁连接,数量成千上万,而长连接会耗费更多资源,短连接更合适; 长连接:连接数少、传输数据量大,例如,数据库连接使用长连接,如果用短连接频繁通信会造成socket错误,而且频繁的socket创建也是对资源的浪费;

对于即时类应用或者即时类游戏,HTTP协议很多时候无法满足我们的需求。这时,Socket对于我们来说就比较使用。Socket实际上以一个IP:PORT,是通信句柄。

Java Socket原理类似于打电话过程:

  1. 前提条件:通话两端都有一个电话,在上诉模型中就是Sokcet模型;
  2. 接通路线:一方拔打电话,试图建立连接,在上述模型中就是客户端建立Java Socket对象;另一方随时监听有没有呼叫,当有呼叫到来时,摘机,在上述模型中就是在服务器端建立一个Java Socket对象,然后用其accept()方法监听客户端的连接请求,当有连接请求时accept方法返回客户端的Socket,于是双方就建立起连接;
  3. 进行通话:双方通话,过程中双方都可以说和听,在上述模型中,每个Socket可以利用输入输出流进行读和写两种操作;在电话中一方听到的是对方说出的,反之亦然;上述模型中,一方读出的也是对方写入的,而写入的则是对方要读出的
  4. 挂断

首先,在服务器端建立一个ServerSocket对象,用于监听客户端的连接请求:

  1. ServerSocket server
  2. servernew ServerSocket(5432);

在服务器端建立ServerSocket对象时必须进行异常处理,以便程序出错时及时作出响应。生成ServerSocket对象时必须选择一个端口注册,以和其它服务器程序分开,使互不干扰。应使用1024以上的端口进行通信,以免和常规通信发生端口冲突。

其次,在服务器端调用ServerSocket的accept()方法进行监听,等待其它程序的连接请求。在连接请求收到之前一直阻塞调用线程,当有一个连接请求时,返回请求连接的Java Socket对象:

  1. Socket socket
  2. socketserver.accept()

当接到一个连接请求时,accept方法返回客户端的socket对象,于是连接成功。正常情况下,通过交换,由另外的线程去处理该连接,而server释放出来继续监听下一个连接请求。

最后,在客户端建立一个Java Socket对象,请求建立连接:

  1. Socket socket
  2. socketnew Socket("localhost", 5432);

在客户端建立Java Socket对象时也必须时行异常处理,主机名和端口号与连接的服务器名和提供该服务的服务程序的监听端口必须一致。

Socket与ServerSocket的交互过程如下:

Socket的构造函数如下:

  1. Socket()
  2. Socket(InetAddress address, int port)throws UnknownHostException, IOException
  3. Socket(InetAddress address, int port, InetAddress localAddress, int localPort)throws IOException
  4. Socket(String host, int port)throws UnknownHostException, IOEx
  5. Socket(String host, int port, InetAddress localAddress, int localPort)throws IOExceptionception

除去第一种不带参数的之外,其它构造函数会尝试建立与服务器的连接。如果失败会抛出IOException错误。如果成功,则返回Socket对象。 InetAddress是一个用于记录主机的类,其静态getHostByName(String msg)可以返回一个实例,其静态方法getLocalHost()也可以获得当前主机的IP地址,并返回一个实例。Socket(String host, int port, InetAddress localAddress, int localPort)构造函数的参数分别为目标IP、目标端口、绑定本地IP、绑定本地端口。

Socket的方法如下:

  • getInetAddress();  远程服务端的IP地址
  • getPort();     远程服务端的端口
  • getLocalAddress() 本地客户端的IP地址
  • getLocalPort()  本地客户端的端口
  • getInputStream(); 获得输入流
  • getOutStream(); 获得输出流

值得注意的是,在这些方法里面,最重要的就是getInputStream()getOutputStream()了。

Socket几个方法:

  • isClosed(); //连接是否已关闭,若关闭,返回true;否则返回false
  • isConnected(); //如果曾经连接过,返回true;否则返回false
  • isBound(); //如果Socket已经与本地一个端口绑定,返回true;否则返回false

如果要确认Socket的状态是否处于连接中,下面语句是很好的判断方式。

  1. boolean isConnection=socket.isConnected() && !socket.isClosed(); //判断当前是否处于连接

很多时候,我们并不知道在获得的输入流里面到底读多长才结束。下面是一些比较普遍的方法:

  • 自定义标识符(譬如下面的例子,当受到“bye”字符串的时候,关闭Socket)
  • 告知读取长度(有些自定义协议的,固定前几个字节表示读取的长度的)
  • 读完所有数据
  • 当Socket调用close的时候关闭的时候,关闭其输入输出流

半关闭Socket,是指一方shutdown read,这样,另一方就不能write了,但此时,另一方仍可以read。

ServerSocket的构造函数:

  1. ServerSocket()throws IOException
  2. ServerSocket(int port)throws IOException
  3. ServerSocket(int port, int backlog)throws IOException
  4. ServerSocket(int port, int backlog, InetAddress bindAddr)throws IOException

注意点:

  1. port服务端要监听的端口;backlog客户端连接请求的队列长度;bindAddr服务端绑定IP
  2. 如果端口被占用或者没有权限使用某些端口会抛出BindException错误。譬如1~1023的端口需要管理员才拥有权限绑定。
  3. 如果设置端口为0,则系统会自动为其分配一个端口;
  4. bindAddr用于绑定服务器IP,为什么会有这样的设置呢,譬如有些机器有多个网卡。
  5. ServerSocket一旦绑定了监听端口,就无法更改。ServerSocket()可以实现在绑定端口前设置其他的参数。
  1. public void service(){
  2. while(true){
  3. Socket socket=null;
  4. try{
  5. socket=serverSocket.accept();//从连接队列中取出一个连接,如果没有则等待
  6. System.out.println("新增连接:"+socket.getInetAddress()+":"+socket.getPort());
  7. ...//接收和发送数据
  8. }catch(IOException e){e.printStackTrace();}finally{
  9. try{
  10. if(socket!=null) socket.close();//与一个客户端通信结束后,要关闭Socket
  11. }catch(IOException e){e.printStackTrace();}
  12. }
  13. }
  14. }

多线程的好处不用多说,而且大多数的场景都是多线程的,无论是我们的即时类游戏还是IM,多线程的需求都是必须的。下面说说实现方式:

  • 主线程会循环执行ServerSocket.accept();
  • 当拿到客户端连接请求的时候,就会将Socket对象传递给多线程,让多线程去执行具体的操作;

实现多线程的方法要么继承Thread类,要么实现Runnable接口。当然也可以使用线程池,但实现的本质都是差不多的。

这里举例: 下面代码为服务器的主线程。为每个客户分配一个工作线程:

  1. public void service(){
  2. while(true){
  3. Socket socket=null;
  4. try{
  5. socket=serverSocket.accept(); //主线程获取客户端连接
  6. Thread workThread=new Thread(new Handler(socket)); //创建线程
  7. workThread.start(); //启动线程
  8. }catch(Exception e){
  9. e.printStackTrace();
  10. }
  11. }
  12. }

当然这里的重点在于如何实现Handler这个类。Handler需要实现Runnable接口:

  1. class Handler implements Runnable{
  2. private Socket socket;
  3. public Handler(Socket socket){
  4. this.socket=socket;
  5. }
  6. public void run(){
  7. try{
  8. System.out.println("新连接:"+socket.getInetAddress()+":"+socket.getPort());
  9. Thread.sleep(10000);
  10. }catch(Exception e){e.printStackTrace();}finally{
  11. try{
  12. System.out.println("关闭连接:"+socket.getInetAddress()+":"+socket.getPort());
  13. if(socket!=null)socket.close();
  14. }catch(IOException e){
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. }

当然实现多线程还有其它的方式,譬如线程池,或者JVM自带的线程池都可以。这里就不说明了。

此处的Demo代码已经提交到GitHub上simple-web-demo下learn-java-basic工程中top.ningg.java.socket包下。

SocketOfServer.java文件,服务器端监听Socket连接:

  1. package top.ningg.java.socket;
  2. import java.io.IOException;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;
  5. public class SocketOfServer {
  6. public SocketOfServer() throws IOException {
  7. int clientNum = 0;
  8. ServerSocket server = null;
  9. server = new ServerSocket(7777);
  10. System.out.println("Server started.");
  11. while (true) {
  12. clientNum++;
  13. Socket socket = server.accept();
  14. new ServerHandler(socket).start();
  15. System.out.println("Client Num is: " + clientNum);
  16. }
  17. }
  18. public static void main(String[] args) throws IOException {
  19. new SocketOfServer();
  20. }
  21. }

SocketOfServer.java文件,服务器端处理socket连接:

  1. package top.ningg.java.socket;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.PrintWriter;
  6. import java.net.Socket;
  7. public class ServerHandler extends Thread{
  8. private Socket socket;
  9. public ServerHandler(Socket socket){
  10. this.socket = socket;
  11. }
  12. public void run() {
  13. try {
  14. BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  15. PrintWriter out = new PrintWriter(socket.getOutputStream());
  16. BufferedReader sysin = new BufferedReader(new InputStreamReader(System.in));
  17. String singleLine = null;
  18. System.out.println("[Client]: " + in.readLine());
  19. singleLine = sysin.readLine();
  20. while (!"bye".equals(singleLine)) {
  21. out.println(singleLine);
  22. out.flush();
  23. System.out.println("[Server]: " + singleLine);
  24. System.out.println("[Client]: " + in.readLine());
  25. singleLine = sysin.readLine();
  26. }
  27. out.close();
  28. in.close();
  29. socket.close();
  30. sysin.close();
  31. } catch (IOException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }

SocketOfClient.java客户端,向服务器端发起socket连接:

  1. package top.ningg.java.socket;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.PrintWriter;
  6. import java.net.Socket;
  7. import java.net.UnknownHostException;
  8. public class SocketOfClient {
  9. public SocketOfClient() {
  10. try {
  11. Socket socket = new Socket("localhost", 7777);
  12. System.out.println("Established a connection...");
  13. BufferedReader sysin = new BufferedReader(new InputStreamReader(System.in));
  14. PrintWriter out = new PrintWriter(socket.getOutputStream());
  15. BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  16. String singleLine = null;
  17. String singleLineFromServer = null;
  18. singleLine = sysin.readLine();
  19. System.out.println("[Client]: " + singleLine);
  20. while(!"bye".equals(singleLineFromServer)){
  21. out.println(singleLine);
  22. out.flush();
  23. singleLineFromServer = in.readLine();
  24. System.out.println("[Server]: " + singleLineFromServer);
  25. singleLine = sysin.readLine();
  26. System.out.println("[Client]: " + singleLine);
  27. }
  28. out.close();
  29. in.close();
  30. socket.close();
  31. sysin.close();
  32. } catch (UnknownHostException e) {
  33. e.printStackTrace();
  34. } catch (IOException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. public static void main(String[] args) {
  39. new SocketOfClient();
  40. }
  41. }

Java IO流学习总结

Java流类图结构:

 

流的概念和作用

流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。 

 

IO流的分类

  • 根据处理数据类型的不同分为:字符流和字节流
  • 根据数据流向不同分为:输入流和输出流
 

字符流和字节流

字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。 字节流和字符流的区别:

  • 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
  • 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

结论:只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。

 

输入流和输出流

对输入流只能进行读操作,对输出流只能进行写操作,程序中需要根据待传输数据的不同特性而使用不同的流。  

 

Java IO流对象

1.输入字节流InputStreamIO 中输入字节流的继承图可见上图,可以看出:

  1. InputStream 是所有的输入字节流的父类,它是一个抽象类。
  2. ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从Byte 数组、StringBuffer、和本地文件中读取数据。PipedInputStream 是从与其它线程共用的管道中读取数据,与Piped 相关的知识后续单独介绍。
  3. ObjectInputStream 和所有FilterInputStream 的子类都是装饰流(装饰器模式的主角)。

 

2.输出字节流OutputStream

IO 中输出字节流的继承图可见上图,可以看出:

  1. OutputStream 是所有的输出字节流的父类,它是一个抽象类。
  2. ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向Byte 数组、和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据,
  3. ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流。

 

3.字节流的输入与输出的对应

 

图中蓝色的为主要的对应部分,红色的部分就是不对应部分。紫色的虚线部分代表这些流一般要搭配使用。从上面的图中可以看出Java IO 中的字节流是极其对称的。“存在及合理”我们看看这些字节流中不太对称的几个类吧!

  1. LineNumberInputStream 主要完成从流中读取数据时,会得到相应的行号,至于什么时候分行、在哪里分行是由改类主动确定的,并不是在原始中有这样一个行号。在输出部分没有对应的部分,我们完全可以自己建立一个LineNumberOutputStream,在最初写入时会有一个基准的行号,以后每次遇到换行时会在下一行添加一个行号,看起来也是可以的。好像更不入流了。
  2. PushbackInputStream 的功能是查看最后一个字节,不满意就放入缓冲区。主要用在编译器的语法、词法分析部分。输出部分的BufferedOutputStream 几乎实现相近的功能。
  3. StringBufferInputStream 已经被Deprecated,本身就不应该出现在InputStream 部分,主要因为String 应该属于字符流的范围。已经被废弃了,当然输出部分也没有必要需要它了!还允许它存在只是为了保持版本的向下兼容而已。
  4. SequenceInputStream 可以认为是一个工具类,将两个或者多个输入流当成一个输入流依次读取。完全可以从IO 包中去除,还完全不影响IO 包的结构,却让其更“纯洁”――纯洁的Decorator 模式。
  5. PrintStream 也可以认为是一个辅助工具。主要可以向其他输出流,或者FileInputStream 写入数据,本身内部实现还是带缓冲的。本质上是对其它流的综合运用的一个工具而已。一样可以踢出IO 包!System.out 和System.out 就是PrintStream 的实例!

 

4.字符输入流Reader

在上面的继承关系图中可以看出:

  1. Reader 是所有的输入字符流的父类,它是一个抽象类。
  2. CharReader、StringReader 是两种基本的介质流,它们分别将Char 数组、String中读取数据。PipedReader 是从与其它线程共用的管道中读取数据。
  3. BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它Reader 对象。
  4. FilterReader 是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号。
  5. InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream 转变为Reader 的方法。我们可以从这个类中得到一定的技巧。Reader 中各个类的用途和使用方法基本和InputStream 中的类使用一致。后面会有Reader 与InputStream 的对应关系。

 

5.字符输出流Writer

在上面的关系图中可以看出:

  1. Writer 是所有的输出字符流的父类,它是一个抽象类。
  2. CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。PipedWriter 是向与其它线程共用的管道中写入数据,
  3. BufferedWriter 是一个装饰器为Writer 提供缓冲功能。
  4. PrintWriter 和PrintStream 极其类似,功能和使用也非常相似。
  5. OutputStreamWriter 是OutputStream 到Writer 转换的桥梁,它的子类FileWriter 其实就是一个实现此功能的具体类(具体可以研究一SourceCode)。功能和使用和OutputStream 极其类似,后面会有它们的对应图。

 

6.字符流的输入与输出的对应

 

7.字符流与字节流转换

转换流的特点:

  1. 其是字符流和字节流之间的桥梁
  2. 可对读取到的字节数据经过指定编码转换成字符
  3. 可对读取到的字符数据经过指定编码转换成字节

何时使用转换流?

  1. 当字节和字符之间有转换动作时;
  2. 流操作的数据需要编码或解码时。

具体的对象体现:

  1. InputStreamReader:字节到字符的桥梁
  2. OutputStreamWriter:字符到字节的桥梁

这两个流对象是字符体系中的成员,它们有转换作用,本身又是字符流,所以在构造的时候需要传入字节流对象进来。

 

8.File类

File类是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹。 File类保存文件或目录的各种元数据信息,包括文件名、文件长度、最后修改时间、是否可读、获取当前文件的路径名,判断指定文件是否存在、获得当前目录中的文件列表,创建、删除文件和目录等方法。  

9.RandomAccessFile类

该对象并不是流体系中的一员,其封装了字节流,同时还封装了一个缓冲区(字符数组),通过内部的指针来操作字符数组中的数据。 该对象特点:

  1. 该对象只能操作文件,所以构造函数接收两种类型的参数:a.字符串文件路径;b.File对象。
  2. 该对象既可以对文件进行读操作,也能进行写操作,在进行对象实例化时可指定操作模式(r,rw)

注意:该对象在实例化时,如果要操作的文件不存在,会自动创建;如果文件存在,写数据未指定位置,会从头开始写,即覆盖原有的内容。 可以用于多线程下载或多个线程同时写数据到文件。

TCP协议

1.BIO、NIO 和 AIO 的区别?
BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。线
程开销大。
伪异步 IO:将请求连接放入线程池,一对多,但线程还是很宝贵的资源。
NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用
器轮询到连接有 I/O 请求时才启动一个线程进行处理。
AIO:一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去
启动线程进行处理,
BIO 是面向流的,NIO 是面向缓冲区的;BIO 的各种流是阻塞的。而 NIO 是非阻塞的;BIO
的 Stream 是单向的,而 NIO 的 channel 是双向的。
NIO 的特点:事件驱动模型、单线程处理多任务、非阻塞 I/O,I/O 读写不再阻塞,而是返
回 0、基于 block 的传输比基于流的传输更高效、更高级的 IO 函数 zero-copy、IO 多路复用
大大提高了 Java 网络应用的可伸缩性和实用性。基于 Reactor 线程模型。
在 Reactor 模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,事件分发
器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操
作。如在 Reactor 中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事
件、事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操
作,处理读到的数据,注册新的事件,然后返还控制权。
2.NIO 的组成?
Buffer:与 Channel 进行交互,数据是从 Channel 读入缓冲区,从缓冲区写入 Channel 中的
flip 方法 : 反转此缓冲区,将 position 给 limit,然后将 position 置为 0,其实就是切换读
写模式
clear 方法 :清除此缓冲区,将 position 置为 0,把 capacity 的值给 limit。
rewind 方法 : 重绕此缓冲区,将 position 置为 0
DirectByteBuffer 可减少一次系统空间到用户空间的拷贝。但 Buffer 创建和销毁的成本更
高,不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本
机 I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下,可以考虑
使用 heapBuffer,由 JVM 进行管理。
Channel:表示 IO 源与目标打开的连接,是双向的,但不能直接访问数据,只能与 Buffer
进行交互。通过源码可知,FileChannel 的 read 方法和 write 方法都导致数据复制了两次!
Selector 可使一个单独的线程管理多个 Channel,open 方法可创建 Selector,register 方法向
多路复用器器注册通道,可以监听的事件类型:读、写、连接、accept。注册事件后会产
生一个 SelectionKey:它表示 SelectableChannel 和 Selector 之间的注册关系,wakeup 方
法:使尚未返回的第一个选择操作立即返回,唤醒的原因是:注册了新的 channel 或者事
件;channel 关闭,取消注册;优先级更高的事件触发(如定时器事件),希望及时处理。
Selector 在 Linux 的实现类是 EPollSelectorImpl,委托给 EPollArrayWrapper 实现,其中三个

native 方法是对 epoll 的封装,而 EPollSelectorImpl. implRegister 方法,通过调用 epoll_ctl
向 epoll 实例中注册事件,还将注册的文件描述符(fd)与 SelectionKey 的对应关系添加到
fdToKey 中,这个 map 维护了文件描述符与 SelectionKey 的映射。
fdToKey 有时会变得非常大,因为注册到 Selector 上的 Channel 非常多(百万连接);过期
或失效的 Channel 没有及时关闭。fdToKey 总是串行读取的,而读取是在 select 方法中进行
的,该方法是非线程安全的。
Pipe:两个线程之间的单向数据连接,数据会被写到 sink 通道,从 source 通道读取
NIO 的服务端建立过程:Selector.open():打开一个 Selector;ServerSocketChannel.open():
创建服务端的 Channel;bind():绑定到某个端口上。并配置非阻塞模式;register():注册
Channel 和关注的事件到 Selector 上;select()轮询拿到已经就绪的事件
3.Netty 的特点?
一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持
使用更高效的 socket 底层,对 epoll 空轮询引起的 cpu 占用飙升在内部进行了处理,避免
了直接使用 NIO 的陷阱,简化了 NIO 的处理方式。
采用多种 decoder/encoder 支持,对 TCP 粘包/分包进行自动化处理
可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持
可配置 IO 线程数、TCP 参数, TCP 接收和发送缓冲区使用直接内存代替堆内存,通过内存
池的方式循环利用 ByteBuf
通过引用计数器及时申请释放不再引用的对象,降低了 GC 频率
使用单线程串行化的方式,高效的 Reactor 线程模型
大量使用了 volitale、使用了 CAS 和原子类、线程安全类的使用、读写锁的使用
4.Netty 的线程模型?
Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,
boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收
到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work
线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。
单线程模型:所有 I/O 操作都由一个线程完成,即多路复用、事件分发和处理都是在一个
Reactor 线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请
求或应答/响应消息。一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,速度
慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
多线程模型:有一个 NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接
请求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线
程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发
操作问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性
能不足问题。
主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel
从主线程池的 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上,用于

处理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作;
5.TCP 粘包/拆包的原因及解决方法?
TCP 是以流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可
能把小的封装成一个大的数据包发送。
TCP 粘包/分包的原因:
应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写
入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘
包现象;
进行 MSS 大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包
以太网帧的 payload(净荷)大于 MTU(1500 字节)进行 ip 分片。
解决方法
消息定长:FixedLengthFrameDecoder 类
包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder 或自定义分隔符类 :
DelimiterBasedFrameDecoder
将消息分为消息头和消息体:LengthFieldBasedFrameDecoder 类。分为有头部的拆包与粘
包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
6.了解哪几种序列化协议?
序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久
化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要
用于网络传输对象的解码,以便完成远程调用。
影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能
(CPU 资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。
Java 默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的性能差
XML,优点:人机可读性好,可指定元素或特性的名称。缺点:序列化数据只包含数据本
身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列
化方法;文件庞大,文件格式复杂,传输占带宽。适用场景:当做配置文件存储数据,实
时数据转换。
JSON,是一种轻量级的数据交换格式,优点:兼容性高、数据格式比较简单,易于读写、
序列化后数据较小,可扩展性好,兼容性好、与 XML 相比,其协议比较简单,解析速度比
较快。缺点:数据的描述性比 XML 差、不适合性能要求为 ms 级别的情况、额外空间开销
比较大。适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于 Web
browser 的 Ajax 请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
Fastjson,采用一种“假定有序快速匹配”的算法。优点:接口简单易用、目前 java 语言中
最快的 json 库。缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不
全。适用场景:协议交互、Web 输出、Android 客户端 Thrift,不仅是序列化协议,还是一个 RPC 框架。优点:序列化后的体积小, 速度快、支持
多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编
码。缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困
难、不能与其他传输层协议共同使用(例如 HTTP)、无法支持向持久层直接读写数据,即
不适合做数据持久化序列化协议。适用场景:分布式系统的 RPC 解决方案
Avro,Hadoop 的一个子项目,解决了 JSON 的冗长和没有 IDL 的问题。优点:支持丰富的
数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可
压缩的二进制数据形式、可以实现远程过程调用 RPC、支持跨编程语言实现。缺点:对于
习惯于静态类型语言的用户不直观。适用场景:在 Hadoop 中做 Hive、Pig 和 MapReduce
的持久化数据格式。
Protobuf,将数据结构以.proto 文件进行描述,通过代码生成工具可以生成对应数据结构的
POJO 对象和 Protobuf 相关的方法和属性。优点:序列化后码流小,性能高、结构化数据存
储格式(XML JSON 等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档
更容易管理和维护。缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持
Java 、C++ 、python。适用场景:对性能要求高的 RPC 调用、具有良好的跨防火墙的访问
属性、适合应用层对象的持久化
其它
protostuff 基于 protobuf 协议,但不需要配置 proto 文件,直接导包即可
Jboss marshaling 可以直接序列化 java 类, 无须实 java.io.Serializable 接口
Message pack 一个高效的二进制序列化格式
Hessian 采用二进制协议的轻量级 remoting onhttp 工具
kryo 基于 protobuf 协议,只支持 java 语言,需要注册(Registration),然后序列化
(Output),反序列化(Input)
7.如何选择序列化协议?
具体场景
对于公司间的系统调用,如果性能要求在 100ms 以上的服务,基于 XML 的 SOAP 协议是一
个值得考虑的方案。
基于 Web browser 的 Ajax,以及 Mobile app 与服务端之间的通讯,JSON 协议是首选。对于
性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON
也是非常不错的选择。
对于调试环境比较恶劣的场景,采用 JSON 或 XML 能够极大的提高调试效率,降低系统开
发成本。
当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro 之间具有一定的竞争关系。
对于 T 级别的数据的持久化应用场景,Protobuf 和 Avro 是首要选择。如果持久化后的数据
存储在 hadoop 子项目里,Avro 会是更好的选择。
对于持久层非 Hadoop 项目,以静态类型语言为主的应用场景,Protobuf 会更符合静态类
型语言工程师的开发习惯。由于 Avro 的设计理念偏向于动态类型语言,对于动态语言为主
的应用场景,Avro 是更好的选择。 

如果需要提供一个完整的 RPC 解决方案,Thrift 是一个好的选择。
如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,
Protobuf 可以优先考虑。
protobuf 的数据类型有多种:bool、double、float、int32、int64、string、bytes、enum、
message。protobuf 的限定符:required: 必须赋值,不能为空、optional:字段可以赋值,也
可以不赋值、repeated: 该字段可以重复任意次数(包括 0 次)、枚举;只能用指定的常量
集中的一个值作为其值;
protobuf 的基本规则:每个消息中必须至少留有一个 required 类型的字段、包含 0 个或多
个 optional 类型的字段;repeated 表示的字段可以包含 0 个或多个数据;[1,15]之内的标识
号在编码的时候会占用一个字节(常用),[16,2047]之内的标识号则占用 2 个字节,标识号
一定不能重复、使用消息类型,也可以将消息嵌套任意多层,可用嵌套消息类型来代替
组。
protobuf 的消息升级原则:不要更改任何已有的字段的数值标识;不能移除已经存在的
required 字段,optional 和 repeated 类型的字段可以被移除,但要保留标号不能被重用。
新添加的字段必须是 optional 或 repeated。因为旧版本程序无法读取或写入新增的
required 限定符的字段。
编译器为每一个消息类型生成了一个.java 文件,以及一个特殊的 Builder 类(该类是用来创
建消息类接口的)。如:UserProto.User.Builder builder =
UserProto.User.newBuilder();builder.build();
Netty 中的使用:ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类;
ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的 UserProto.java 文件中的解
码类;ProtobufVarint32LengthFieldPrepender 对 protobuf 协议的消息头上加上一个长度为
32 的整形字段,用于标志这个消息的长度的类;ProtobufEncoder 是编码类
将 StringBuilder 转换为 ByteBuf 类型:copiedBuffer()方法
8.Netty 的零拷贝实现?
Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读
写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM 会将堆内存
Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。ByteBuffer 由 ChannelConfig 分配,
而 ChannelConfig 创建 ByteBufAllocator 默认使用 Direct Buffer
CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过
内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。addComponents 方法将 header
与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在 CompositeByteBuf 内部都是单
独存在的, CompositeByteBuf 只是逻辑上是一个整体
通过 FileRegion 包装的 FileChannel.tranferTo 方法 实现文件传输, 可以直接将文件缓冲区
的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。 

通过 wrap 方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty
ByteBuf 对象, 进而避免了拷贝操作。
Selector BUG:若 Selector 的轮询结果为空,也没有 wakeup 或新消息处理,则发生空轮
询,CPU 使用率 100%,
Netty 的解决办法:对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进
行一次计数,若在某个周期内连续发生 N 次空轮询,则触发了 epoll 死循环 bug。重建
Selector,判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的
Selector 上去除注册,重新注册到新的 Selector 上,并将原来的 Selector 关闭。
9.Netty 的高性能表现在哪些方面?
心跳,对服务端:会定时清除闲置会话 inactive(netty5),对客户端:用来检测会话是否断
开,是否重来,检测网络延迟,其中 idleStateHandler 类 用来检测会话状态
串行无锁化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样
就避免了多线程竞争和同步锁。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不
够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,
这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
可靠性,链路有效性检测:链路空闲检测机制,读/写空闲超时机制;内存保护机制:通过
内存池重用 ByteBuf;ByteBuf 的解码保护;优雅停机:不再接收新消息、退出前的预处理操
作、资源的释放操作。
Netty 安全性:支持的安全协议:SSL V2 和 V3,TLS,SSL 单向认证、双向认证和第三方 CA
认证。
高效并发编程的体现:volatile 的大量、正确使用;CAS 和原子类的广泛使用;线程安全容
器的使用;通过读写锁提升并发性能。IO 通信性能三原则:传输(AIO)、协议(Http)、线
程(主从多线程)
流量整型的作用(变压器):防止由于上下游网元性能不均衡导致下游网元被压垮,业务
流中断;防止由于通信模块接受消息过快,后端业务线程处理不及时导致撑死问题。
TCP 参数配置:SO_RCVBUF 和 SO_SNDBUF:通常建议值为 128K 或者 256K;
SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止
大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要
关闭该优化算法;
10.NIOEventLoopGroup 源码?
NioEventLoopGroup(其实是 MultithreadEventExecutorGroup) 内部维护一个类型为
EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化

EventExecutor 时 NioEventLoopGroup 重载 newChild 方法,所以 children 元素的实际类型为
NioEventLoop。
线程启动时调用 SingleThreadEventExecutor 的构造方法,执行 NioEventLoop 类的 run 方
法,首先会调用 hasTasks()方法判断当前 taskQueue 是否有元素。如果 taskQueue 中有元
素,执行 selectNow() 方法,最终执行 selector.selectNow(),该方法会立即返回。如果
taskQueue 没有元素,执行 select(oldWakenUp) 方法
select ( oldWakenUp) 方法解决了 Nio 中的 bug,selectCnt 用来记录 selector.select 方法的
执行次数和标识是否执行过 selector.selectNow(),若触发了 epoll 的空轮询 bug,则会反复
执行 selector.select(timeoutMillis),变量 selectCnt 会逐渐变大,当 selectCnt 达到阈值(默
认 512),则执行 rebuildSelector 方法,进行 selector 重建,解决 cpu 占用 100%的 bug。
rebuildSelector 方法先通过 openSelector 方法创建一个新的 selector。然后将 old selector 的
selectionKey 执行 cancel。最后将 old selector 的 channel 重新注册到新的 selector 中。
rebuild 后,需要重新执行方法 selectNow,检查是否有已 ready 的 selectionKey。
接下来调用 processSelectedKeys 方法(处理 I/O 任务),当 selectedKeys != null 时,调用
processSelectedKeysOptimized 方法,迭代 selectedKeys 获取就绪的 IO 事件的 selectkey 存
放在数组 selectedKeys 中, 然后为每个事件都调用 processSelectedKey 来处理它,
processSelectedKey 中分别处理 OP_READ;OP_WRITE;OP_CONNECT 事件。
最后调用 runAllTasks 方法(非 IO 任务),该方法首先会调用 fetchFromScheduledTaskQueue
方法,把 scheduledTaskQueue 中已经超过延迟执行时间的任务移到 taskQueue 中等待被执
行,然后依次从 taskQueue 中取任务执行,每执行 64 个任务,进行耗时检查,如果已执行
时间超过预先设定的执行时间,则停止执行非 IO 任务,避免非 IO 任务太多,影响 IO 任务
的执行。
每个 NioEventLoop 对应一个线程和一个 Selector,NioServerSocketChannel 会主动注册到某
一个 NioEventLoop 的 Selector 上,NioEventLoop 负责事件轮询。
Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,通过 Outbound 事
件进行通知,传播方向是 tail 到 head。Inbound 事件发起者是 unsafe,事件的处理者是
Channel, 是通知事件,传播方向是从头到尾。
内存管理机制,首先会预申请一大块内存 Arena,Arena 由许多 Chunk 组成,而每个 Chunk
默认由 2048 个 page 组成。Chunk 通过 AVL 树的形式组织 Page,每个叶子节点表示一个
Page,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被
分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都
已被分配了。大于 8k 的内存分配在 poolChunkList 中,而 PoolSubpage 用于分配小于 8k 的
内存,它会把一个 page 分割成多段,进行内存分配。
ByteBuf 的特点:支持自动扩容(4M),保证 put 方法不会抛出异常、通过内置的复合缓冲
类型,实现零拷贝(zero-copy);不需要调用 flip()来切换读/写模式,读取和写入索引分 

开;方法链;引用计数基于 AtomicIntegerFieldUpdater 用于内存回收;PooledByteBuf 采用
二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区
对象。UnpooledHeapByteBuf 每次都会新建一个缓冲区对象。 

 

posted @ 2021-12-12 02:00  CharyGao  阅读(50)  评论(0)    收藏  举报