深入学习Netty(1)——传统BIO编程
前言
之前看过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编程的更多相关文章
- 深入学习Netty(3)——传统AIO编程
前言 之前已经整理过了BIO.NIO两种I/O的相关博文,每一种I/O都有其特点,但相对开发而言,肯定是要又高效又简单的I/O编程才是真正需要的,在之前的NIO博文(深入学习Netty(2)--传统N ...
- 深入学习Netty(4)——Netty编程入门
前言 从学习过BIO.NIO.AIO编程之后,就能很清楚Netty编程的优势,为什么选择Netty,而不是传统的NIO编程.本片博文是Netty的一个入门级别的教程,同时结合时序图与源码分析,以便对N ...
- 深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?
前言 学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的. 在此博文前,可以先学习了解前几篇博文: 深入 ...
- 深入学习Netty(2)——传统NIO编程
前言 学习Netty编程,避免不了从了解Java 的NIO编程开始,这样才能通过比较让我们对Netty有更深的了解,才能知道Netty大大的好处.传统的NIO编程code起来比较麻烦,甚至有遗留Bug ...
- Java IO编程全解(二)——传统的BIO编程
前面讲到:Java IO编程全解(一)——Java的I/O演进之路 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口 ...
- Netty 中的异步编程 Future 和 Promise
Netty 中大量 I/O 操作都是异步执行,本篇博文来聊聊 Netty 中的异步编程. Java Future 提供的异步模型 JDK 5 引入了 Future 模式.Future 接口是 Java ...
- 08.十分钟学会JSP传统标签编程
一.认识标签 1,说明:传统标签编程在开发中基本用不到,学习标签编程主要还是为了完善知识体系. 2,标签的主要作用:移除或减少jsp中的java代码 3,标签的主要组成部分及运行原理 4,简单标签示例 ...
- Java IO学习笔记五:BIO到NIO
作者:Grey 原文地址: Java IO学习笔记五:BIO到NIO 准备环境 准备一个CentOS7的Linux实例: 实例的IP: 192.168.205.138 我们这次实验的目的就是直观感受一 ...
- 学习ASP.NET Core Razor 编程系列二——添加一个实体
在Razor页面应用程序中添加一个实体 在本篇文章中,学习添加用于管理数据库中的书籍的实体类.通过实体框架(EF Core)使用这些类来处理数据库.EF Core是一个对象关系映射(ORM)框架,它简 ...
随机推荐
- Android面试必问!View 事件分发机制,看这一篇就够了!
在 Android 开发当中,View 的事件分发机制是一块很重要的知识.不仅在开发当中经常需要用到,面试的时候也经常被问到. 如果你在面试的时候,能把这块讲清楚,对于校招生或者实习生来说,算是一块不 ...
- C#类中方法的执行顺序
有些中级开发小伙伴还是搞不太明白在继承父类以及不同场景实例化的情况下,父类和子类的各种方法的执行顺序到底是什么,下面通过场景的举例来重新认识下方法的执行顺序: (下面内容涉及到了C#中的继承,构造函数 ...
- [c++] 分号的使用
加分号的情况: 语句结束加分号(否则编译器不知道在哪里结束语句,编译器不识别换行,写代码时换行和退格只是为了看着舒服,但本质上代码是写给编译器看的) 声明语句后加分号(也是一种语句) 结构体.类定义后 ...
- python类传参示例
1 class f(): 2 3 def __init__(self, *args, **kwargs): 4 print('args Is', args) # args Is ('5', 'fff' ...
- docker总结复习
一.概念 1.容器( container-based )虚拟化方案,充分利用了操作系统本身已有的机制和特性,以实现轻量级的虚拟化(每个虚拟机安装的不是完整的虚拟机),甚至有人把他称为新一代的虚拟化技术 ...
- 003.kubernets对于namespace的管理
一 Kuberbetes的架构简单介绍 1.1 云计算的传统分类 1.2 kubernetes基础架构 工作机制 用户通过kubectl向api-server提交需要运行的pod描述 api-serv ...
- Java和JDK版本的关系-(转载)
JAVA的版本最开始是1995年的JDK Alpha and Beta版本,第二年发布JDK1.0版本之后就是JDK1.1,JDK1.2.到1998年,不再叫JDK了,而是叫J2SE,但是版本号还是继 ...
- Lua中的面向对象编程详解
简单说说Lua中的面向对象 Lua中的table就是一种对象,看以下一段简单的代码: 复制代码代码如下: local tb1 = {a = 1, b = 2}local tb2 = {a = 1, b ...
- MyBatis 高级查询之一对一查询(九)
高级查询之一对一查询 查询条件:根据游戏角色ID,查询账号信息 我们在之前创建的映射器接口 GameMapper.java 中添加接口方法,如下: /** * 根据角色ID查询账号信息 * @para ...
- Centos双网卡配置默认路由
Centos6.5 双网卡,我们只需要一个默认路由,如果两个都有或都没有会有一系列的问题 [root@centos]# vi /etc/sysconfig/network修改以下内容NETWORKIN ...