Socket
本身有“插座”的意思,不是Java中特有的概念,而是一个语言无关的标准,任何可以实现网络编程的编程语言都有Socket
。在Linux
环境下,用于表示进程间网络通信的特殊文件类型,其本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。
与管道类似的,Linux
系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
可以这么理解:Socket
就是网络上的两个应用程序通过一个双向通信连接实现数据交换的编程接口API。
Socket
通信的基本流程具体步骤如下所示:
(1)服务端通过Listen
开启监听,等待客户端接入。
(2)客户端的套接字通过Connect
连接服务器端的套接字,服务端通过Accept
接收客户端连接。在connect-accept
过程中,操作系统将会进行三次握手。
(3)客户端和服务端通过write
和read
发送和接收数据,操作系统将会完成TCP
数据的确认、重发等步骤。
(4)通过close
关闭连接,操作系统会进行四次挥手。
针对Java编程语言,java.net
包是网络编程的基础类库。其中ServerSocket
和Socket
是网络编程的基础类型。
SeverSocket
是服务端应用类型。Socket
是建立连接的类型。当连接建立成功后,服务器和客户端都会有一个Socket
对象示例,可以通过这个Socket
对象示例,完成会话的所有操作。对于一个完整的网络连接来说,Socket
是平等的,没有服务器客户端分级情况。
对于一次IO操作,数据会先拷贝到内核空间中,然后再从内核空间拷贝到用户空间中,所以一次read
操作,会经历两个阶段:
(1)等待数据准备
(2)数据从内核空间拷贝到用户空间
基于以上两个阶段就产生了五种不同的IO模式。
这五种IO模式不难发现存在这两对关系:同步和异步、阻塞和非阻塞。那么稍微解释一下:
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞是针对进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。
如果组合后的同步阻塞(blocking-IO
)简称BIO
、同步非阻塞(non-blocking-IO
)简称NIO
和异步非阻塞(asynchronous-non-blocking-IO
)简称AIO
又代表什么意思呢?
java
中的 BIO
、NIO
和AIO
理解为是 Java 语言
在操作系统层面对这三种 IO
模型的封装。程序员在使用这些 封装API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码,只需要使用Java
的API就可以了。由此,为了使读者对这三种模型有个比较具体和递推式的了解,并且和本文主题NIO
有个清晰的对比,下面继续延伸。
BIO
编程方式通常是是Java的上古产品,自JDK 1.0-JDK1.4就有的东西。编程实现过程为:首先在服务端启动一个ServerSocket
来监听网络请求,客户端启动Socket
发起网络请求,默认情况下SeverSocket
会建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。大致结构如下:
如果要让 BIO
通信模型能够同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()
、socket.read()
、 socket.write()
涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过线程池机制改善,线程池还可以让线程的创建和回收成本相对较低。使用线程池机制改善后的 BIO
模型图如下:
BIO
方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,是JDK1.4以前的唯一选择,但程序直观简单易懂。Java BIO
编程示例网上很多,这里就不进行coding举例了,毕竟后面NIO
才是重点。
NIO
(New IO或者No-Blocking IO),从JDK1.4 开始引入的非阻塞IO
,是一种非阻塞
+ 同步
的通信模式。这里的No Blocking IO
用于区分上面的BIO
。
NIO
本身想解决 BIO
的并发问题,通过Reactor模式
的事件驱动机制来达到Non Blocking
的。当 socket
有流可读或可写入 socket
时,操作系统会相应的通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。
也就是说,这个时候,已经不是一个连接就 要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
当一个连接创建后,不需要对应一个线程,这个连接会被注册到 多路复用器
上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器
进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
NIO
提供了与传统BIO模型中的Socket
和ServerSocket
相对应的SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现,如下图结构所示。这里涉及的Reactor
设计模式、多路复用Selector
、Buffer
等暂时不用管,后面会讲到。
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局 限于应用中,编程复杂,JDK1.4 开始支持。同时,NIO
和普通IO的区别主要可以从存储数据的载体、是否阻塞等来区分:
与 NIO
不同,当进行读写操作时,只须直接调用 API 的 read
或 write
方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read
方 法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write
方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write
方法都是异步的,完成后会主动调用回调函数。在 JDK7
中,提供了异步文件通道和异步套接字通道的实现,这部分内容被称作 NIO
.
AIO
方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS
参与并发操作,编程比较复杂,JDK7
开始支持。
目前来说 AIO
的应用还不是很广泛,Netty
之前也尝试使用过 AIO
,不过又放弃了。
在NIO
中,基本所有的IO操作都是从Channel
开始的,Channel
通过Buffer(缓冲区)
进行读写操作。
read()
表示读取通道中数据到缓冲区,write()
表示把缓冲区数据写入到通道。
Channel
有好多实现类,这里有三个最常用:
SocketChannel
:一个客户端发起TCP连接的ChannelServerSocketChannel
:一个服务端监听新连接的TCP Channel,对于每一个新的Client连接,都会建立一个对应的SocketChannelFileChannel
:从文件中读写数据其中SocketChannel
和ServerSocketChannel
是网络编程中最常用的,一会在最后的示例代码中会有讲解到具体用法。
Buffer
也被成为内存缓冲区,本质上就是内存中的一块,我们可以将数据写入这块内存,之后从这块内存中读取数据。也可以将这块内存封装成NIO Buffer
对象,并提供一组常用的方法,方便我们对该块内存进行读写操作。
Buffer
在java.nio
中被定义为抽象类:
我们可以将Buffer
理解为一个数组的封装,我们最常用的ByteBuffer
对应的数据结构就是byte[]
Buffer
中有4个非常重要的属性:capacity、limit、position、mark
针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。
下面的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图),有需要的可以戳这里免费领取
最新整理电子书
资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图),有需要的可以戳这里免费领取
[外链图片转存中…(img-fGVI68CH-1624794780601)]
最新整理电子书