Socket
Socket是处于应用层和传输层中间的软件,是一个接口,我们应用层可以直接调用接口来实现网络连接,Socket帮我们实现了传输层和网络层复杂逻辑。每创建个连接就会创建一个Socket。
image-20230215220048528
BIO
BIo即Blocking I/O,阻塞I/O。
在Socket中存在两种阻塞,一个accept,另外一个是Socket I/O的读,
accept是阻塞在应用层的,当有连接进来的时候,服务器接受信息(连接信息),并且发送中断信号唤醒accept线程.它不会影响传输层TCP的连接.
Socket I/O每创建socket时候都会创建两个socket缓存区.分别是读写,每当有数据过来时候会写入到Socket读取缓冲区内.发送中断信号唤醒客户端线程进行读取.
BIO相比较传输效率更快,但是服务器必须为每个客户端单开一个线程,客户端连接成本大,不能只能大量的连接.
服务器
public class Server {
private static ExecutorService executorService
= Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
public static void main(String[] args) throws IOException {
//服务端启动必备
ServerSocket serverSocket = new ServerSocket();
//表示服务端在哪个端口上监听
serverSocket.bind(new InetSocketAddress(6666));
System.out.println("服务端开启 ... ... ");
try {
while (true) {
//阻塞,等待有新的连接过来
//如果TCP,传输层有新的连接,才会进入到这里,这里阻塞是应用层的阻塞,不影响TCP连接
Socket accept = serverSocket.accept();
executorService.execute(new Thread(new ServerTask(accept)));
}
} finally {
serverSocket.close();
}
}
/**
* 每有客户端建立连接,都会有对应的Task来处理
*/
private static class ServerTask implements Runnable {
private Socket socket = null;
public ServerTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (
//实例化与客户端通信的输入输出流
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())
) {
//如果webSocket输入流中没有数据,阻塞
String text = inputStream.readUTF();
System.out.println("服务器收到的消息:" + text);
outputStream.writeUTF("服务器返回的消息:" + text);
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端
public class Client {
public static void main(String[] args) throws IOException {
//客户端启动必备
Socket socket = null;
//实例化与服务端通信的输入输出流
ObjectOutputStream output = null;
ObjectInputStream input = null;
//服务器的通信地址
InetSocketAddress addr
= new InetSocketAddress("127.0.0.1", 6666);
try {
socket = new Socket();
socket.connect(addr);
output = new ObjectOutputStream(socket.getOutputStream());
input = new ObjectInputStream(socket.getInputStream());
/*向服务器输出请求*/
output.writeUTF("你好服务器... ...");
output.flush();
//这里读取是阻塞的.
System.out.println(input.readUTF());
} finally {
if (socket != null) socket.close();
if (output != null) output.close();
if (input != null) input.close();
}
}
}
阻塞IO和非阻塞IO
阻塞IO,指的是需要等待内核IO准备好后,才返回到用户空间执行用户的操作。阻塞是指用户空间的执行状态。
非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间执行用户操作,即处于非阻塞IO状态,内核空间会立即返回给用户一个状态值。调用线程拿到内核返回的状态值后,IO操作能干就干,不能就干别的事情。
JAVA I/O的各种流是阻塞的,意味着read()或write()时,线程阻塞,直到一些数据被读取,或者数据完全写入。再此期间线程不能干任何事情。
JAVA NIO的非阻塞模式,线程从通道读取数据,仅能得到可用的数据,如果没有数据,什么都不会获取,线程不会阻塞。
针对网络IO的操作,可以分成两个阶段,准备阶段和操作阶段。
1,准备阶段是判断是否能够操作(即等待数据是否可用),在内核进程完成的;
2,操作阶段则执行实际的IO调用,数据从内核缓冲区拷贝到用户进程缓冲区。
一个read()操作发生时,它会经历下面两个阶段:
A, 等待数据准备,数据是否拷贝到内核缓冲区;
B, 将数据从内核拷贝到用户进程空间
那么在Socket.read()阻塞在当前操作系统的准备阶段,我还没准备好,稍等会。
Buffer如果里边有数据就返回,没有数据就不返回,不需要等待它有没有准备好。
Reactor模式
Reactor翻译过来是反应堆模式,大致是将要关注的事件扔到反应堆里边,当有关注的事件发生时候,反应堆就会告诉我们。
在内核中新开启个线程Selector,这个线程是阻塞状态,把socket放入到这个Selector线程里边,当有socket发生事件就会给Selector线程发送中断信号来唤醒select。
Reactor模式,是一种高效的异步IO模式特征是回调。
与观察者模式区别是,观察者模式是对单个事件感兴趣,Reactor是对多个事件感兴趣。
单线程Reactor模式
Reactor放入一个关注连接的事件,当有客户端发生连接时候,Reactor就会关注到,并且唤醒accept接受连接,产生一个Socket,这个socket关注读并放入到Reactor中,当我们socket读取的时候会阻塞主线程,当有新的客户端连接过来的时候,由于主线程阻塞在read那里,导致服务器没有办法响应新客户端的连接,但是这个时候TCP其实已经连接上,客户端也收到连接成功。
image-20230216123720734
多线程Reactor模式
多线程Reactor相比较于单线程,它会为每一个新连接的客户端Socket分配一个线程,这样socket读取不会影响主线程。如果客户连接相对较多就会分配更多的线程。
image-20230216124456447
主从Reactor模式
在相比较多线程模式下,当有大量的连接过来,那么在反应堆中就会存在大量的Socket线程关注着读写,然而关注连接的相比较会太少,所以将关注连接的和关注读写的反应堆分开
image-20230216124923655
NIO三大核心组件
Selector:选择器,Channel:管道,buffer:缓冲区
Selector:也被叫做事件订阅器,NIO可以通过Selector来管理多个channel
channel:是程序和系统交换数据的渠道。
buffer:面向缓冲的,非阻塞。用于与channel交换数据。
image-20230216163501615
其中7,8是应用程序主动调用的。只有Socket创建的两个缓冲区是操作系统主动调用。
NIO代码
需要注意的是:
1:客户端再connect后需要判断是否连接上,因为connect是异步操作,可能没连接上需要注册连接事件
2:for循环keys去除key应该先从Iterator中删除,因为他不会自己删除,下次再来的话还会再此处理它。
keys是保存在sun.nio.ch.SelectorImpl#publicSelectedKeys,Set publicSelectedKeys;如果不手动剔除的话,不会自动剔除。
SelectKey
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
socketChannel.register(selector,SelectionKey.OP_CONNECT);
SelectKey表示channel和selector建立了关系,并维护了channel事件。
selectKey事件有四种:读,写,连接,就绪
OP_READ:读,当socket的读缓冲区有数据时候触发。
OP_WRITE:写,当socket的写缓冲区空闲时候触发,没必要注册该事件,啥时候写自己知道,不用操作系统通知。
OP_CONNECT:就绪,当请求连接成功后触发。该操作只给客户端使用。
OP_ACCEPT:当接受到一个客户端连接请求时触发,只给操作系统用。
直接内存
image-20230216170228528
channel.write(writeBuffer);
当writeBuffer大于Data(Socket缓冲区的时候)write将不会返回(如果是同步的,将阻塞,直到所有数据发送到缓冲区)。write返回数据,仅仅表示buffer写入缓冲区成功,仅仅表示可以重新使用原来的缓冲区,TCP对端不一定收到消息。
I/O读写基本要求,一个地址通过JNI传递给C库的时候,这个地址不能失效。所以在JDK在写的时候必须先创建一个堆外内存DirectBuffer,并且把数据复制到堆外内存,一个GC管不了的地方。因为JDK堆内存有GC地址会变化。DirectBuffer只有当执行老年代Full GC的时候才会顺便回收直接内存。
image-20230216172743640
堆外内存的优缺点
优点:
1:避免重复复制。节省一次复制,节省一次用户态转内核态。
2:减少了垃圾回收的工作。
缺点:
1:内存泄漏,不好排查。
2:不适合存储复杂对象。
3:堆外内存创建比堆内内存创建复杂。
零拷贝
零拷贝是指CPU不需要将数据从某处复制到另一处。
减少不必要的中间拷贝,不是说的不拷贝,只是说减少冗余的拷贝。 例如:Netty,kafka,Rocketmq… …
我cpu一秒钟几亿上下,你却让我陪你吃杂碎面
DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依于CPU的大量中断负载。否则,CPU需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU对于其他的工作来说就无法使用。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,能使CPU 的效率大为提高。DMA内核中的操作。
程序IO读取数据:
1:DMA将数据准备好,从磁盘(硬件层)读取到内核空间。
2:用户程序,从内核拷贝到用户空间。
byffer = file.read();
Socket.send(buffer);
image-20230216203531487
传统发送文件需要四次上下文切换,五次拷贝。
MMAP内存映射
将应用程序上的数据和应用缓冲区进行映射。建立一一对应关系。减少一次DMA拷贝。
image-20230218180656458
sendfile
DMA将磁盘数据复制到内核中的kernel buffer,然后将内核中的kernel buffer拷贝到socket buffer,将kernel buffer直接拷贝到Socket buffer中,并不是真正的拷贝,只是将内存地址和长度拷贝到缓冲区。DMA将数据直接从内核传递给协议引擎。需要硬件支持。
NIO利用的是sendfile().NIO提供FileChannel拥有transferTo和transferFrom两个方法。可以直接把FileChannel中的数据直接拷贝到另外一个Channel。或者Channel拷贝到FileChannel。
image-20230216203737075
splice
不需要硬件支持,从linux 2.6.17开始。数据从磁盘读取到内核OS,在内核直接可以与Socket buffer建立pipe管道。
image-20230216204121953