做网站和管理系统,dw怎么做网站教程,网站容量,利用百度快照搜索消失的网站#x1f52d; 嗨#xff0c;您好 #x1f44b; 我是 vnjohn#xff0c;在互联网企业担任 Java 开发#xff0c;CSDN 优质创作者 #x1f4d6; 推荐专栏#xff1a;Spring、MySQL、Nacos、Java#xff0c;后续其他专栏会持续优化更新迭代 #x1f332;文章所在专栏 嗨您好 我是 vnjohn在互联网企业担任 Java 开发CSDN 优质创作者 推荐专栏Spring、MySQL、Nacos、Java后续其他专栏会持续优化更新迭代 文章所在专栏网络 I/O 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识 向我询问任何您想要的东西IDvnjohn 觉得博主文章写的还 OK能够帮助到您的感谢三连支持博客 代词: vnjohn ⚡ 有趣的事实音乐、跑步、电影、游戏 目录 前言非阻塞式 I/O 模型图解分析源码实践Socket 服务端代码Socket 客户端代码流程说明configureBlocking客户端连接 C10K 问题源码流程分析错误排查 BIO vs NIO阻塞的套接字函数调用两者肉眼可见区别 NIO 为什么速度慢总结 前言
Unix/Linux 下可用的 I/O 模型有以下五种
阻塞式 I/O非阻塞式 I/OI/O 复用(select、poll)信号驱动式 I/O(SIGIO)异步 I/O
在 Linux 中操作内核时所有的无非三种操作分别是输入、输出、报错输出 0-输入 1-输出 2-报错输出 一个输入操作通常包括两个不同的阶段
等待数据准备好从内核向进程复制数据
对于一个套接字Socket的输入操作第一步通常涉及等待数据从网络中当所等待分组到达时它被复制到内核中的某个缓冲区第二步就是把数据从内核缓冲区复制到应用进程缓冲区
非阻塞式 I/O 模型
进程把一个套接字设置成非阻塞是在通知内核当所请求的 I/O 操作非得把本进程投入睡眠才能完成时不要把本进程投入睡眠而是返回一个错误 前三次调用 recvfrom 时没有数据可返回因此内核转而立即返回一个 EWOULDBLOCK 错误第四次调用 recvfrom 时已有一个数据报准备好它被复制到应用进程缓冲区于是 recvfrom 成功返回接着处理数据 EWOULDBLOCKE 是 ErrorWOULD BLOCK 是可能会被阻塞的意思 表示当前没有数据可读或没有缓冲区可写需要等待下一次读写事件再尝试读写非阻塞模式下可以继续尝试读写 当一个应用进程像这样对一个非阻塞描述符循环调用 recvfrom 时我们称之为轮询Polling应用进程持续轮询内核以查看某个操作是否就绪这么做往往耗费大量 CPU 时间不过这种模型偶尔也会遇到通常是在专门提供某一种功能的系统中才有.
图解分析 当有新的连接进来时主线程负责执行 accept 连接客户端clone 出一个子进程新的 sockfd 去 accept/read等待其他客户端连接时是非阻塞的读取客户端数据也是非阻塞的只是返回给客户端是 EWOULDBLOCK 状态NIO 采用的处理方式主线程阻塞去等待客户端连接以非阻塞的方式为每个客户端读取数据 NIO 核心的参数设置configureBlocking(false) 源码实践
Socket 服务端代码
package org.vnjohn.nio.server;import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;/*** author vnjohn* since 2023/12/2*/
public class SocketNIOServer {public static void main(String[] args) throws Exception {// 链表集合存放所有的 socket client 实例LinkedListSocketChannel clients new LinkedList();// 服务端开启监听接受客户端ServerSocketChannel serverSocketChannel ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(8090));
// serverSocketChannel.configureBlocking(true);// OS NONBLOCKING 只让接受客户端并不阻塞serverSocketChannel.configureBlocking(false);System.out.println(step1: new ServerSocketChannel(8090));while (true) {// 接受客户端的连接Thread.sleep(1000);// 不会阻塞// 在操作系统中返回-1在 Java 程序中返回 NULLSocketChannel socketChannel serverSocketChannel.accept();// accept 调用内核// 1、没有客户端连接进来返回值在 BIO 时一直卡/阻塞着但是在 NIO 不卡着返回 -1NULL// 如果来客户端的连接accept 返回的是这个客户端的 fd5if (socketChannel null) {System.out.println(.....);} else {// 服务端 listen socket 连接请求三次握手后通过 accept 得到连接的 sockfd连接 socket 连接后读写使用socketChannel.configureBlocking(false);int port socketChannel.socket().getPort();System.out.println(step2client\t port);clients.add(socketChannel);}// 堆内allocate - HeapByteBuffer// 堆外allocateDirect - DirectByteBufferByteBuffer buffer ByteBuffer.allocateDirect(4096);// 遍历已经链接进来的客户端读写数据// 这里采用串行化的方式进行接收可以更改为采用 BIO 方式一个客户端抛出一个线程进行接收处理for (SocketChannel client : clients) {// 不会阻塞返回 0、-1、0int num client.read(buffer);if (num 0) {buffer.flip();byte[] bufferByte new byte[buffer.limit()];buffer.get(bufferByte);String b new String(bufferByte);System.out.println(client.socket().getPort() : b);buffer.clear();}}}}
}Socket 客户端代码
package org.vnjohn.nio.client;import java.io.*;
import java.net.Socket;/*** author vnjohn* since 2023/12/2*/
public class SocketNIOClient {public static void main(String[] args) {try {Socket client new Socket(172.16.249.10, 8090);client.setSendBufferSize(20);client.setTcpNoDelay(true);// false 优化,true 不优化client.setOOBInline(false);OutputStream out client.getOutputStream();InputStream in System.in;BufferedReader reader new BufferedReader(new InputStreamReader(in));while (true) {String line reader.readLine();if (line ! null) {byte[] bb line.getBytes();for (byte b : bb) {out.write(b);}}}} catch (IOException e) {e.printStackTrace();}}
}流程说明 172.16.249.10 是之前作为 node1 节点所在 IP 将以上两个 java 源文件上传到 node1 虚拟节点上所在目录/opt/java 1、在虚拟节点上安装好 Java 环境 2、将源文件所在的 package 包名通过 vim 命令将 package 包名删除首行. 3、将 Java 源文件进行编译为 .class 文件 javac SocketServer.java、javac SocketClient.java configureBlocking
追踪应用程序与操作系统中的交互信息 cd /opt/java strace -ff -o out java SocketNIOServer 正常的流程都是先 socket()、bind()、listen()、accept() 关于这四个函数的详细介绍可以阅读博主「网络 I/O」专栏中的另外一篇博文 深入理解网络阻塞 I/OBIO 先设置 configureBlocking true代表当前设置 SocketChannel 是阻塞式运行的
若设置 configureBlocking true 时观察追踪到操作系统的操作信息能够详细看到我们的操作系统基于内核是一个阻塞态 accept(4,说明当前程序是阻塞运行的 再设置 configureBlocking false代表当前设置 SocketChannel 是非阻塞式运行的 accept(4, 0x7f8c0d4ee0, [28]) -1 EAGAIN (Resource temporarily unavailable) 在操作系统侧返回的是 -1说明是非阻塞运行的 按照代码执行的逻辑来看控制台会一直打印…说明当前 SocketChannel 是非阻塞运行的. 说明通过设置 configureBlocking false 就可以实现不阻塞若没有客户端进行连接在操作系统中返回的是 -1而在 Java 中返回是 null
客户端连接
在 node2 节点172.16.249.10运行 SocketNIOClient 程序代码 1、移除首行 package 包名 2、cd /opt/java编译 Java 源文件javac SocketNIOClient.java 3、运行 Java 可执行程序java SocketNIOClient 当客户端连接以后就会在 out.pid 文件中分配一个文件描述符给到当前这个客户端如下 accept(4, {sa_familyAF_INET6, sin6_porthtons(32972), inet_pton(AF_INET6, “::ffff:172.16.249.11”, sin6_a ddr), sin6_flowinfohtonl(0), sin6_scope_id0}, [28]) 5 当在客户端中发送数据时比如123456在 out.pid 文件中会触发系统调用 R/W 读写 在 out.pid 文件中会发现大量的 EAGAIN 字眼代表当次资源暂不可用这个操作可能等下次重试后可用 EAGAIN 官方定义“Resource temporarily unavailable.” The call might work if you try again later. The macro EWOULDBLOCK is another name for EAGAIN; they are always the same in the GNU C Library. EWOULDBLOCK 官方定义“Operation would block.” In the GNU C Library, this is another name for EAGAIN (above). The values are always the same, on every operating system. 两者都代表含义是一样的在 GUN C 库中EWOULDBLOCK 的另外一个名称称之为 EAGAIN C10K 问题
当 C10K 出现时若有 1W 个客户端建立连接在 BIO 时需要抛出 1W 个线程此时就会造成资源消耗越多任务调度就会变得越多内核态用户态之间的切换也会越多
当在 NIO 时进行使用在内核中又会出现另外一个问题接着向下分析
源码
package org.vnjohn.nio.client;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;/*** author vnjohn* since 2023/12/2*/
public class C10KClient {public static void main(String[] args) {LinkedListSocketChannel clients new LinkedList();InetSocketAddress serverAddr new InetSocketAddress(172.16.249.10, 8090);// 端口号的问题65535for (int i 10000; i 25000; i) {try {SocketChannel client SocketChannel.open();// 172.16.249.10:10000 172.16.249.10:8090client.bind(new InetSocketAddress(1172.16.249.10, i));client.connect(serverAddr);clients.add(client);} catch (IOException e) {e.printStackTrace();}}for (int i 25000; i 50000; i) {try {SocketChannel client SocketChannel.open();// 172.16.249.10:25000 172.16.249.10:8090client.bind(new InetSocketAddress(172.16.249.10, i));client.connect(serverAddr);clients.add(client);} catch (IOException e) {e.printStackTrace();}}System.out.println(total clients clients.size());try {System.in.read();} catch (IOException e) {e.printStackTrace();}}
}修改 SocketNIOServer 服务端代码将两行代码注释 其目的是为了让服务端能够快速与客户端建立连接而不是 1s 处理一个客户端连接以便我们在 Linux 中来分析最大连接数
流程分析
1、先将 SocketNIOServer 休整的代码重新编译执行strace -ff -o out java SocketNIOServer
2、先 C10K 问题的客户端代码编译为 class 文件然后通过 java 运行
3、接着观察几个情况
首先是观察 socket、TCP/IP 条目信息可以看到有很多条类目客户端-服务端之间的二元组信息「IP:端口」 观察服务端接收客户端连接的情况是否保持正常 客户端在满足条件时bind 客户端是处于正常状态的而一旦超过了内核所配置的最大的大小以后会发现会出现一个 Error 错误信息 java.net.SocketException: Too many open files 从异常信息上来看意思是文件描述符超过了限制的大小.
错误排查
该错误是可以被修复的但是修复了意义上不大这种非阻塞运行的方式对内核这块的资源损耗还是很大的 java.net.SocketException: Too many open files 可打开的文件描述符大小是受内核配置的限制的但它也是支持可配置的查看命令ulimit -a 或 ulimit -n open files 1024一个进程最多可以打开 1024 个文件描述符 可通过 ulimit -SHn Xxx 命令来临时调整大小
Ssoft 软Hhard 硬n文件描述符
调整后可以通过ulimit -n 再查看修改后的大小
在内核中会根据上限的物理内存估算出一个总文件描述符大小
[rootnode1 java]# cat /proc/sys/fs/file-max
146266BIO vs NIO
套接字的默认状态是阻塞的当你未设置参数 configureBlocking 时这就意味着当发出一个不能立即完成的套接字调用时其进程被投入睡眠状态等待相应操作完成
阻塞的套接字函数调用
可能阻塞的套接字调用可分为以下四类
输入操作包括read、readv、recv、recvfrom、recvmsg 五个函数 既然 TCP 是字节流协议该进程的唤醒只要有一些数据到达这些数据即可能是单个字节也可以是一个完整的 TCP 分节中的数据若想等到某个固定的数目的数据可读为止需要调用的 readn 函数或者指定 MSG_WAITALL 标志 既然 UDP 是数据报协议若一个阻塞的 UDP 套接字的接收缓冲区为空对它调用输入函数的进程将被投入睡眠直到有 UDP 数据报到达 若某个进程对一个阻塞的 TCP 套接字调用这些输入函数之一而且该套接字的接收缓冲区中没有数据可读该进程将被投入睡眠直到有一些数据到达 对于非阻塞的套接字若输入操作不能被满足对于 TCP 套接字即至少有一个字节的数据可读对于 UDP 套接字即有一个完整的数据报可读响应调用将立即返回一个 EWOULDBLOCK EAGAIN 错误 输出操作包括write、writev、send、sendto、sendmsg 五个函数对于一个 TCP 套接字内核将从应用进程的缓冲区到该套接字的发送缓冲区复制数据 对于阻塞的套接字若其发送缓冲区没有空间进程将投入睡眠直到有空间为止 对于一个非阻塞的 TCP 套接字若其发送缓冲区中根据没有空间输出函数调用将立即返回一个 EWOULDBLOCK EAGAIN 错误若其发送缓冲区有一些空间返回值将是内核能够复制到该缓冲区中的字节数该字节数也称之为不足计数 接受外来连接即 accept 函数 若对一个阻塞的套接字调用 accept 函数并且尚无新的连接到达调用进程将被投入睡眠 若对一个非阻塞的套接字调用 accept 函数并且尚无新的连接到达accept 调用将立即返回一个 EWOULDBLOCK EAGAIN 错误 发起外出连接即用于 TCP 的 connect 函数connect 同样可用于 UDP不过它不能使一个 “真正” 连接建立起来它只是使内核保存对端的 IP 地址、端口号 TCP 连接建立涉及到一个三次握手过程而且 connect 函数一直要等到客户收到对于自己的 SYN - ACK 为止才返回这意味着 TCP 的每个 connect 总会阻塞其调用进程至少有一个到服务器的 RT 时间 若对于一个非阻塞的 TCP 套接字调用 connect并且连接不会立即建立连接的建立能照样发起臂如送出 TCP 三次握手的第一个分组不过会返回一个 EINPROGRESS 错误 两者肉眼可见区别
BIO 会在主线程阻塞式的去接受一个 socket 客户端的连接在操作系统内核中不会返回 -1一直卡着不动 accept(4,
客户端读取数据时也是阻塞式的没有 -1、0、0所以在使用 BIO 时要单独开辟新的线程去专门读取来自客户端的数据而主线程只是阻塞式的负责接收来自客户端的连接当客户端的连接很多时线程的数量就会很多线程之间的切换和任务频繁的调度就显而易见了. 主线程阻塞式接收客户端的连接accept 每一个客户端有一个子线程负责去接收客户端的数据read、readv 在 NIO 中可配置式的支持配置阻塞或非阻塞通过configureBlocking 参数来进行配置true阻塞、false非阻塞
accept 调用了操作系统内核有以下两种情况 当没有客户端连接进来时在 BIO 时会一直阻塞着但是在 NIO 时操作系统内核层面会返回 -1而在 Java 应用程序中会返回 NULL 当有客户端连接进来时accept 返回的是分配给这个客户端的文件描述符 socketfdJava 中返回的是一个 SocketChannel 对象 socket 对象分为以下两种 服务端 ServerSocketChannel连接请求三次握手完成后该对象可以通过 accept 方法获取到客户端的 socket - SocketChannel 客户端 SocketChannel客户端与服务端之间建立好连接以后通过该 socket 来负责读写数据使用 两者 socket 都需要配置为非阻塞式运行才能够保证连接、读取数据都是非阻塞运行的 NIO 为什么速度慢
NIO 优势在于可以通过一个或多个线程来解决 N 个 IO 连接阻塞的问题
它的问题在于在本篇博文通过 C10K 问题来模拟当其到达了 1W 个客户端但是每次进行读取时都会循环一次会带有 O(n) 复杂度的 recv 系统函数调用可能在这些客户端中只有几个客户端是有数据的额所以在这期间会有很多系统函数调用是没有意义的浪费的只是系统的资源以及给操作系统内核带来没必要的压力 所以从 NIO 来看应该考虑的是只做那么有必要的事没必要的事应该尽量的去避免它发生 总结
该篇博文主要介绍的是 I/O 模型中的非阻塞 I/O - NIO简要分析了 NIO 非阻塞式 I/O 简要的模型通过图解分析的方式告知它与 BIO 之间的区别通过实践代码的方式来分析非阻塞 I/O 在系统调用中所涉及到的流程同时也介绍了 C10K 问题给非阻塞式 I/O 带来的不利之处最后介绍了上篇 BIO 博文与 NIO 之间的相关的区别以及 NIO 为什么速度会慢的原因希望能够得到你的支持感谢三连
四元组唯一源 IP、源端口、目标 IP、目标端口 愿你我都能够在寒冬中相互取暖互相成长只有不断积累、沉淀自己后面有机会自然能破冰而行 博文放在 网络 I/O 专栏里欢迎订阅会持续更新
如果觉得博文不错关注我 vnjohn后续会有更多实战、源码、架构干货分享
推荐专栏Spring、MySQL订阅一波不再迷路
大家的「关注❤️ 点赞 收藏⭐」就是我创作的最大动力谢谢大家的支持我们下文见