引导开始 :高性能的网络编程都绕不开反应器模式 nginx ,redis, netty
这个是个简单的测试 阻塞的BIO 也就是服务端的阻塞
public static void main(String[] args) {
Set<Socket> sockets = new HashSet<>();
try {
System.out.println("线程:"+Thread.currentThread().getName()+"正在执行当前的任务");
long start = System.currentTimeMillis();
ServerSocket serverSocket = new ServerSocket(8905);
long end = System.currentTimeMillis();
System.out.println("执行这次任务的时间是"+(end-start));
//获取到对应的客户端
while(true){
Socket socket = serverSocket.accept();//接收客户端,BIO会阻塞吗? 假如会阻塞如何处理,不会阻塞如何处理
System.out.println("有客户端连接进入");
// TimeUnit.SECONDS.sleep(10);//线程休眠卡住一个和客户端判断是否会阻塞
InputStream inputStream = socket.getInputStream();
while(true){
byte [] data = new byte[1024];
int len;
while ((len = inputStream.read(data))!=-1){
String message = new String(data,0,len);
System.out.println("客户端传来消息:"+message);
socket.getOutputStream().write("我是服务端,我收到了数据".getBytes());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
但是上面的经过测试发现可以接收多个客户端的连接,但是在处理客户端发送的数据时候,会阻塞所以我们想到使用多线程去处理下面的代码就是经过优化后的代码
public class Server {
public static void main(String[] args) {
//服务端
Server server = new Server(8000);
//服务端的start
server.start();
}
//服务套接字
private ServerSocket serverSocket;
/**
*
* @param port
*/
public Server(final int port) {
try {
//每次使用构造器,都会创建一个serverSocket对象
this.serverSocket= new ServerSocket(port);
System.out.println("服务端启动成功,端口:"+port);
} catch (IOException e) {
System.out.println("服务端启动失败");
// e.printStackTrace();
}
}
public void start(){
//利用java的虚拟线程来进行异步非阻塞来处理客户端连接
new Thread(
new Runnable() {
@Override
public void run() {
doStart();
}
}
).start();
}
private void doStart(){
/**
* 对数据的一个抽象
* 套接字长链接监听端口对应netty就是一个
* NioEventLoop 同步非阻塞线程组
*/
while (true){
try {
Socket client = serverSocket.accept();
//创建多个处理器
new ClientHandler(client).start();
} catch (IOException e) {
System.out.println("服务端异常");
// e.printStackTrace();
}
}
}
}
为了解决这个问题,我开起了多个线程去处理,本来我想这下完美了,这样每次来一个连接由一个线程去处理,阻塞也只是阻塞执行的线程,我可以接收很多的请求了,但是还是 知识 限制了我们想法
当我学习了netty之后,我知道了 我之前的洋洋得意只是 BIO编程,在不考虑性能的情况下,实可以解决很多问题,但是当我看了 netty原理 我再也不想写这个样子的代码了;
先抛出问题??
每次一个客户端都连接过来,我就创建一个线程取处理,会总成大量的性能消耗
插入一点操作系统相关的: 我们的软件是用户态 每次操作硬件都需要 经过操作系统的 kernel , 系统调用(守护线程的保护)来回复制数据到我们的进程,但是 执行这个进程的线程,其实也是一个轻量级的进程(还有一个协程的概念自己去看看把),它是占用硬件资源的,如果有1000个线程没有关系
但是如果有10000个客户端连接,你需要去创建一万条线程,而且线程如果断开,你需要去销毁,GC回收,这个也会造成大量的性能消耗,多次的上下文切换也会造成性能损耗
接下来看看netty是怎么处理的
package com.venus.note;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
*NIO: 基于BIO的阻塞,性能低 处理效率也低
* 多路复用:解决我们的线程多的问题,可以一个线程执行多个IO
*/
public class NIO {
private Selector selector = null;
private static ServerSocketChannel server = null;
private int port = 8000;
private Charset charset = Charset.forName("GBK");
public static void main(String[] args) {
try {
//创建一个多路复用器
/**
* selector(选择器) 是java NIO中能够检测到一到多个NIO通道,并且能够知晓通道是否为
* 诸如读写事件做好准备的组件,一个单独的线程可以管理多个channel 从而管理多个网络连接。
* selector是NIO实现的核心,简单来讲,selector会不断的轮询注册在上面的channel
* 弱国channel上有新的TCP连接加入,读和写事件,这个channel处于就绪状态,会被Selector轮询
* 然后通过SelectionKey可以获取就绪的Channel的集合,进行和后续的I/O操作
*/
Selector selector = Selector.open();
selector.select(500);
//创建一个和NIO的socketChannel,配置 非阻塞 端口等
server = ServerSocketChannel.open();
server.configureBlocking(false);/*设置成非阻塞,这里的非阻塞配置的是 OS 操作系统的内核的非阻塞*/
server.socket().setReuseAddress(true);/*同一个主机上关闭了服务器程序,紧接着载启动该服务器程序时可以顺利绑定到相同的接口 */
server.socket().bind(new InetSocketAddress(8905)); /*把服务器和本地的一个端口绑定*/
//给ServerSocketChannel注册复用器事件
/**
* 1 读 OP_READ
* 4写 OP_WRITE
* 8连接OP_CONNECT
* 16接受
* |连接符接受多个
*/
server.register(selector, SelectionKey.OP_ACCEPT);
while(true){
int count = selector.select();/*获取到当前就绪IO的键值*/
if(count==0)continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 获取已就绪键集
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isAcceptable()) {
// 接受处理 socket底层是tcp 接收就是tcp的三次挥手
System.out.println("接受处理");
SocketChannel client = server.accept();//接收客户端的连接
client.configureBlocking(false);//设置客户端的非阻塞 OS 内核实现的非阻塞
client.register(selector,SelectionKey.OP_READ);
} else if (key.isConnectable()) {
// 连接处理
System.out.println("连接处理");
} else if (key.isReadable()) {
// 读取处理
System.out.println("读取处理");
//若读就绪
SocketChannel SChannel = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = SChannel.read(buf))>0){
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
} else if (key.isWritable()) {// 注意:key.isWritable只要建立连接,就会触发,所以在设置可写入事件时,在写入之后要改回可监听事件,否则
// 写入处理
System.out.println("写入处理");
}
iterator.remove(); // 移除键,防止下次重复处理
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里需要解释一下,首先说之前看到NIO代码 之前的每来一个客户端都有一个
while(true){
server.accept(); 这里会造成一直循环阻塞
}
想一想10万个客户端 10 万条线程循环阻塞一直运行,是不是很刺激,所以netty中引入了NIO的概念
当然阻塞这个问题 是操作系统处理的 就是上边的 这个设置成非阻塞,windows linux内部都是非阻塞的,你可以尝试 2个客户端请求同一个服务端,尝试一下 是并行的不会阻塞。这就是所谓的非阻塞,至于为什么非阻塞,需要你去看一看操作系统内核这块相关的一些知识或者用 strace 指令去跟踪一个服务就能看到
接下来阻塞问题解决了 但是我们还是启动了1万条线程去处理1万个连接怎么办,netty模仿了操作系统的多路复用器的概念
引出多路复用: 一个线程去执行2个3个或者多个IO的连接和读写事件,是不是很有趣,但是这些的基础是建立在我们的操作系统非阻塞的支持上的。
实现的方法:引入了设计模式 reactor 反应器模式去实现一个线程执行多个IO连接或者读写
上边说了折磨多就是为了引出Reactor这个模式
1 什么是反应器模式呢?:
反应器 由reactor反应器线程,任务分发器 ,handler处理器等角色组成,
2 反应器的职责是什么?
负责响应IO事件,并且分发到handlers(自定义处理器)去处理具体的业务逻辑
3 handler处理器的职责(非阻塞的执行业务逻辑的代码片段)
这下我们都知道要做什么,这个模式的产生我们是不是就知道了
------------------------------后续把这个模式的代码贴出来