前言

  之前看过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. CentOS 7 设置日期和时间 timedatectl

    CentOS 7 设置日期和时间 在CentOS 6版本,时间设置有date.hwclock命令,从CentOS 7开始,使用了一个新的命令timedatectl. timedatectl [root ...

  2. 选择“保留window设置、个人文件及应用”或者“升级安装windows并保留文件设置和应用程序”的 处理干净以后用ghost备份

    个人经验 第一次装好以后 把所有常用软件什么的 还有系统的更新全部装好 删去乱七八糟的临时文件啊什么的 处理干净以后用ghost备份下次需要重装直接从ghost镜像恢复 然后更新软件 打补丁 再备份 ...

  3. Mysql体系结构管理

    1.客户端与服务端模型 1)数据是一个典型的C/S结构的服务 1.mysql自带的客户端工具 mysql mysqladmin mysqldump 3.mysql是一个二进制程序,后台守护进程 单进程 ...

  4. Sqoop 安装部署

    1. 上传并解压 Sqoop 安装文件 将 sqoop-1.4.7.bin__hadoop-2.6.0.tar.gz 安装包上传到 node-01 的 /root/ 目录下并将其解压 [root@no ...

  5. 061.Python前端Django组件用户认证组件

    一 auth认证组件 在使用pymysql,数据库迁移的时候.,默认生成有十张表如下 查看author_user表结构 mysql> desc auth_user; +------------- ...

  6. GPIO模式用法

    浮空,顾名思义就是浮在半空,输入直接与寄存器挂钩: 开漏,输出0的时候 PMOS管导通IO输出Vdd,输出1的时候 NMOS管导通IO输出Vss(Cmos场效应管): 推挽,输出时候电平确定,同样使用 ...

  7. STM32程序的启动

    普及: 不同位置启动首需要硬件上的配合:BOOT1与BOOT0 引脚电平配合,一般默认使用主闪存存储: 也就是BOOT0 = 0; 启动时将现在起始模式的初始地址映射到了0x0000 0000,内部S ...

  8. 4.3 Python3进阶-函数嵌套和嵌套调用

    >>返回主目录 源码 # 函数嵌套 def func1(): print("这是外部函数") def func2(): print("这是内部函数1" ...

  9. 使用Python操作InfluxDB时序数据库

    使用Python操作InfluxDB时序数据库 安装python包 influxdb,这里我安装的是5.3.0版本 pip install influxdb==5.3.0   使用 from infl ...

  10. 在gitlab网页上合并分支

    在gitlab网页上合并分支 使用gitlab网页将代码合并分 下面将dev分支代码合并至master 1.点击request merge 2.源分支为当前分支,目标分支默认为master,确认无误, ...