前言

  之前看过Dubbo源码,Nacos等源码都涉及到了Netty,虽然遇到的时候查查资料,后面自己也有私下学习Netty并实践,但始终没有形成良好的知识体系,Netty对想要在Java开发上不断深入是十分重要的。所以借此博客平台记录下自己的学习思考的过程,形成自己的知识体系,以后学习深入源码更加得心应手!

  参考资料《Netty In Action》、《Netty权威指南》(有需要的小伙伴可以评论或者私信我)

  博文中所有的代码都已上传到Github,欢迎Start、Fork


一、Linux中常见的I/O模型

1.5种I/O模型

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型

(1)阻塞I/O模型

  如果还未获取数据报则一直等待,直到数据报准备好了再进行复制数据报等接下来的操作。

(2)非阻塞I/O模型

  轮询检查是否有数据准备好的状态,如果数据准备好就进行接下来的操作

(3)I/O复用模型

  Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll顺序扫描fd是否就绪,但是select支持的fd数量有限(1024*8个)。Linux提供了epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。Java NIO的Selector基于epoll的多路复用技术实现。

(4)信号驱动I/O模型

  首先开启套接字信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数。当数据准备就绪时,就为进程生成一个SIGIO信号,通过信号回调通知应用程序调用来读取数据。

(5)异步I/O

  告知内核启动某个操作,并让内核在整个操作完成后通知我们,这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。

2.I/O多路复用技术

(1)I/O多路复用技术应用场景

  在处理多客户端连接I/O请求时,往往有两种方式:一种是传统的多线程处理,另一种就是I/O多路复用技术进行处理,但是与传统的多线程处理比较,I/O多路复用最大的优势就是在于系统开销小,不需要创建额外的线程或进程处理客户端连接,节省了系统资源,I/O多路复用技术主要的应用场景如下:

  • 服务器需要同时处理多个处于监听状态或多个连接状态的套接字
  • 服务器需要同时处理多种网络协议的套接字,比如又要处理UDP、又要处理TCP

(2)epoll的优势

  在Linux系统中,采用了I/O多路复用技术调用有select、poll、epoll,在Linux网络编程的过程中。select、poll、epoll介绍这里不再讲解,我也是参考了很多资料才把三者关系弄清楚,大家可以自行Google,起初使用select做轮询,但是select有一些缺陷,不得不选择epoll替代了select。epoll在select的基础上做了如下的改进:

 1)支持一个进程打开的socket描述符(FD)不受限制

  select最大缺陷就是单个进程打开FD是有限制的,epoll是没有的,而poll除了没有限制(基于链表存储,所有理论上没有限制)以外跟select没啥区别

 2)I/O效率不会随着FD数目的增加而线性下降

  传统的select/poll的有一个致命缺点:当有一个很大的socket集合时,由于网络延迟或链路空闲,任一时刻只有少部分的socket是“活跃”的,但是select/poll仍然会线性扫描全部的集合,导致效率降低。而epoll只会针对“活跃”的socket进行操作,这是因为epoll根据每个fd上的回调函数callback函数实现的,只有“活跃”的socket才会主动调用该函数,其它则不会

 3)使用mmap加速内核与用户空间的消息传递

  无论是select、poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制显得十分重要,epoll通过内核把用户空间mmap同一块内存来实现的。

 4)epoll使用的API更加简单

二、传统BIO编程

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。  

很明显,这种模型缺乏弹性伸缩能力,当客户端并发量增加后,线程数也随着增加,可能会造成线程堆栈溢出、创建新线程失败等问题

1.通信模型

  BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户客户端的连接,接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完之后通过输出流返回应答给客户端,最后线程销毁,这是典型的一请求一应答通信模型

    

2.代码实践

(1)服务端代码(代码已上传到Github

/**
* BIO通信服务端:
* 由一个独立的Acceptor线程负责监听客户客户端的连接,
* 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,
* 处理完之后通过输出流返回应答给客户端,最后线程销毁,这是典型的一请求一应答通信模型
*/
public class BioServer { public static void main(String[] args) {
bioServer(8082);
} /**
* @param port
*/
public static void bioServer(int port) {
ServerSocket server = null;
try {
// ServerSocket负责绑定IP地址,启动监听端口
server = new ServerSocket(port);
System.out.println("The bio server is start in port : " + port);
// Socket负责发起连接操作
Socket socket = null;
// 无限循环监听客户端的连接,若没有则主线程阻塞在ServerSocket的accept操作上
while (true) {
socket = server.accept();
new Thread(new BioServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (server != null) {
System.out.println("The bio server close");
}
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
server = null;
}
}
}

(2)客户端代码(代码已上传到Github

/**
* BIO通信客户端:
* 由一个独立的Acceptor线程负责监听客户客户端的连接,
* 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,
* 处理完之后通过输出流返回应答给客户端,最后线程销毁,这是典型的一请求一应答通信模型
*/
public class BioClient { public static void main(String[] args) throws InterruptedException {
String host = "127.0.0.1";
int port = 8082;
bioClient(host, port);
} public static void bioClient(String host, int port) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket(host, port);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
// 发送查询当前时间指令
out.println("QUERY CURRENT TIME ORDER");
System.out.println("Client send order to server succeed.");
// 返回应答
String resp = in.readLine();
System.out.println("Now is : " + resp);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
out.close();
out = null;
}
// 释放socket套接字句柄资源
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
} }
}

(3)处理器(代码已上传到Github

public class BioServerHandler implements Runnable {

    private Socket socket;

    public BioServerHandler(Socket socket) {
this.socket = socket;
} @Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String currentTime = null;
String body = null;
while (true) {
body = in.readLine();
if (body == null) {
break;
}
System.out.println("The server receive order: " + body);
currentTime = "QUERY CURRENT TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() :
"BAD ORDER";
out.println(currentTime); }
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
out.close();
out = null;
}
// 释放socket套接字句柄资源
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
this.socket = null;
}
}
}
}

(4)测试结果

运行服务端,再运行客户端:

服务端Console:

客户端Console:

同时netstat命令查看TCP监听端口8082

此外,通过dump thread查看,发现服务端线程一直阻塞在accept方法上,处于RUNNABLE状态

  为了解决同步阻塞I/O的缺点(处理链路:功能线程=1:1),后端通过一个线程池处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N。这就是通过Java线程池处理任务,而不是每次生成一个Thread。在《Netty权威指南》中称之为“伪异步I/O”。

深入学习Netty(1)——传统BIO编程的更多相关文章

  1. 深入学习Netty(3)——传统AIO编程

    前言 之前已经整理过了BIO.NIO两种I/O的相关博文,每一种I/O都有其特点,但相对开发而言,肯定是要又高效又简单的I/O编程才是真正需要的,在之前的NIO博文(深入学习Netty(2)--传统N ...

  2. 深入学习Netty(4)——Netty编程入门

    前言 从学习过BIO.NIO.AIO编程之后,就能很清楚Netty编程的优势,为什么选择Netty,而不是传统的NIO编程.本片博文是Netty的一个入门级别的教程,同时结合时序图与源码分析,以便对N ...

  3. 深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?

    前言 学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的. 在此博文前,可以先学习了解前几篇博文: 深入 ...

  4. 深入学习Netty(2)——传统NIO编程

    前言 学习Netty编程,避免不了从了解Java 的NIO编程开始,这样才能通过比较让我们对Netty有更深的了解,才能知道Netty大大的好处.传统的NIO编程code起来比较麻烦,甚至有遗留Bug ...

  5. Java IO编程全解(二)——传统的BIO编程

    前面讲到:Java IO编程全解(一)——Java的I/O演进之路 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口 ...

  6. Netty 中的异步编程 Future 和 Promise

    Netty 中大量 I/O 操作都是异步执行,本篇博文来聊聊 Netty 中的异步编程. Java Future 提供的异步模型 JDK 5 引入了 Future 模式.Future 接口是 Java ...

  7. 08.十分钟学会JSP传统标签编程

    一.认识标签 1,说明:传统标签编程在开发中基本用不到,学习标签编程主要还是为了完善知识体系. 2,标签的主要作用:移除或减少jsp中的java代码 3,标签的主要组成部分及运行原理 4,简单标签示例 ...

  8. Java IO学习笔记五:BIO到NIO

    作者:Grey 原文地址: Java IO学习笔记五:BIO到NIO 准备环境 准备一个CentOS7的Linux实例: 实例的IP: 192.168.205.138 我们这次实验的目的就是直观感受一 ...

  9. 学习ASP.NET Core Razor 编程系列二——添加一个实体

    在Razor页面应用程序中添加一个实体 在本篇文章中,学习添加用于管理数据库中的书籍的实体类.通过实体框架(EF Core)使用这些类来处理数据库.EF Core是一个对象关系映射(ORM)框架,它简 ...

随机推荐

  1. [刷题] 剑指offer 面试题18:删除链表节点

    要求 给定单向链表的头指针和一个节点指针,在O(1)时间内删除该节点 常规思路:从头节点a开始顺序遍历,发现p指向要删除的节点i,然后把p的m_pNext指向i的下一个节点j,时间复杂度O(n) O( ...

  2. Qt事件与常用事件处理、过滤

    转载: https://blog.csdn.net/apollon_krj/article/category/6939539 https://blog.csdn.net/qq_41072190/art ...

  3. fail to start File System Check

    fail to start File System Check 方法A: 输入root的密码 cd /etc 1 vim /etc/fstab 将所有分区最后的数字 1和2 全都改为0 reboot ...

  4. Shell脚本 /dev/null 2>&1详解

    Shell脚本---- /dev/null 2>&1详解   1.可以将/dev/null看作"黑洞". 它非常等价于一个只写文件. 所有写入它的内容都会永远丢失. ...

  5. Linux性能监控与分析之--- CPU

    Linux性能监控与分析之--- CPU 望月成三人关注 2016.07.25 18:16:12字数 1,576阅读 2,837 CPU性能指标 用户进程使用CPU的比率 系统进程使用CPU的比率 W ...

  6. Web网站实现用户认证访问(加密访问)

    Web网站实现用户认证访问,有效减少流量的访问,具体的实现步骤如下: 我们使用httpd作为测试对象,体现安装好httpd服务,并且可以在浏览器访问测试首页(可以关闭防火墙:如果不关闭防火墙,则需要开 ...

  7. k8s集群部署(2)

    一.利用ansible部署kubernetes准备阶段 1.集群介绍 基于二进制方式部署k8s集群和利用ansible-playbook实现自动化:二进制方式部署有助于理解系统各组件的交互原理和熟悉组 ...

  8. 生成多个ssh密钥-(转自 破男孩)

    如果你已经有了一套名为 id_rsa 的公秘钥,将要生成另外一个公钥,比如 aysee ,你也可以使用任何你喜欢的名字. 步骤如下: 1.生成一个新的自定义名称的公钥: 1 ssh-keygen -t ...

  9. Chrome版本与chromedriver版本映射表

    chromedriver版本 支持的Chrome版本 v2.36 v64-66 v2.35 v62-64 v2.34 v61-63 v2.33 v60-62 v2.32 v59-61 v2.31 v5 ...

  10. python 中的变量内存以及关于is ==、 堆栈、

    在工作学习中会碰到一些python中变量与内存层面的问题理解,虽然是在不断的解决,但是并没有做过这方面的总结. 变量:用来标识(identify)一块内存区域.为了方便表示内存,我们操作变量实质上是在 ...