参考文章
同步异步关注的是消息通信机制。同步,就是在发出一个调用时,没有得到结果之前,该调用就不返回。就是由调用者主动等待这个调用的结果。
异步相反,调用在发出后就返回了,没有返回结果。也就是说,一个异步过程调用后,调用者不会立刻得到结果。而是在调用后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
关注的是程序在等待调用结果(消息,返回值)时的状态
阻塞调用是指调用结果返回之前,当前线程挂起。调用线程只有在结果返回之后才会返回。
非阻塞调用指在不能立刻得到结果时,该调用不会阻塞当前线程。
同步阻塞IO,数据的读取写入必须阻塞在一个线程内等待其完成。
活动链接数不高的情况下,可以让每个连接专注自己的IO且编程模型简单。当面对大量IO连接时,就无能为力。
并发量高,来一个客户端连接就开启一个线程;同时阻塞方法多,很多线程都在等待,CPU在线程之间反复切换,因此效率低。
package com.cloud.bio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress("127.0.0.1",8888));//开启一个插座,指定端口和地址(里面有很多插口供客户端连接)
while(true){
Socket s = ss.accept(); //阻塞方法,等在此处等待有客户端接入ss,如果没有线程就进入wait,直到客户端接入才被唤醒
new Thread(()->{ //一旦有客户端接入,开启一个新线程处理,主线程继续等待
handle(s);
}).start();
}
}
public static void handle(Socket s){
try{
byte[] bytes = new byte[1024];
int len = s.getInputStream().read(bytes);// 此处的read方法也是阻塞方法,如果客户端没发来消息,就会一直wait
System.out.println(new String (bytes,0,len));
s.getOutputStream().write(bytes,0,len); //write也是阻塞方法,如果客户端不接收,就阻塞在此处
s.getOutputStream().flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.cloud.bio;
import java.io.IOException;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
Socket s = new Socket("127.0.0.1",8888); //连接服务器
s.getOutputStream().write("helloServer".getBytes()); //s是客户端这边看到的channel
s.getOutputStream().flush();
System.out.println("write over, waiting for msg back..");
byte[] bytes = new byte[1024];
int len = s.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
s.close();
}
}
单线程:Server 端有一个Selector选择器只专注客户端的连接,读取,和写入,(会盯在Server端的插座旁边,如果有客户连接,就接收进来建立连接,如果有客户写入,就读取进来。即发现一件事就处理一件事)
package com.cloud.nio;
import java.io.IOException;
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.util.Iterator;
import java.util.Set;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
//ServerSocketChannel是NIO对ServerSocket进行的封装,这个是双向的,可以同时读,同时写
//而BIO需要拿到InputStream,和OutputStream才能读和写。
ssc.socket().bind(new InetSocketAddress("127.0.0.1",8888));
ssc.configureBlocking(false);//设定为非阻塞
System.out.println("server started listening on " +ssc.getLocalAddress());
Selector selector = new Selector.open();//打开一个Selector
ssc.register(selector, SelectionKey.OP_ACCEPT);//这句话意思是注册感兴趣的事,OP_ACCEPT就是对客户端发起连接这件事
while (true){ //轮询
selector.select();//
Set<SelectionKey> keys = selector.selectedKeys();//key就是服务器插座上的每一个插头(相当于channel),如果有客户端建立连接,该插头就会被选出来,进入selector里去。
Iterator<SelectionKey> it =keys.iterator();
while (it.hasNext()){
SelectionKey key = it.next();
it.remove();//取出来处理之后就把key从it里remove掉,避免反复处理
handle(key);
}
}
}
public static void handle(SelectionKey key){
if(key.isAcceptable()){//说明这个key是有客户端要连上的
try{
ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //获取channel
SocketChannel sc = ssc.accept(); //接收连接,获得连接的通道 与BIO处代码类似
sc.configureBlocking(false);
sc.register(key.selector(),SelectionKey.OP_READ);//给这个channel注册一个用来监听READ事件
} catch (IOException e) {
e.printStackTrace();
}
}else if(key.isReadable()){
SocketChannel sc = null;
try{
sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(512); //BIO读数据是一个字节一个字节的,效率低。
// 而NIO每一个channel都和一个Buffer(内存中的字节数组)放在一起,从字节数组中读和写,效率高
//
buffer.clear();
int len =sc.read(buffer);
if(len!=-1){
System.out.println(new String(buffer.array(),0,len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(sc!=null){
try{
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
/**
* ServerSocket建立一个插座,Selector对每一个插口放一个key,这个key记录着对哪件事感兴趣,第一个感兴趣的事件是ACCEPT,就是客户端尝试往这个插口上连接的事件。
* Selector进行轮询,哪个key有事件发生就把key取出来,挨个进行进行处理
* 如果处理时,发现是有客户端要建立连接,就给他注册一个channel,并再注册一个感兴趣事件READ。
*/
单线程的缺陷:如果在处理某一个客户端的读写请求时阻塞,那么就无法为其他客户端提供服务。
多线程:
reactor模式,响应式编程
Selector只负责客户端的连接,客户端的读和写交给线程池处理。(这就是netty的做法,只是netty做了很好的封装)
package com.cloud.nio;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
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.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PoolServer {
ExecutorService pool = Executors.newFixedThreadPool(50);
private Selector selector;
public static void main(String[] args) throws IOException {
PoolServer server = new PoolServer();
server.initServer(8000);
server.listen();
}
public void initServer(int port) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
this.selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功");
}
public void listen() throws IOException {
while(true){
selector.select();
Iterator ite = this.selector.selectedKeys().iterator();
while(ite.hasNext()){
SelectionKey key = (SelectionKey) ite.next();
ite.remove();
if(key.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(this.selector,SelectionKey.OP_READ);
}else if(key.isReadable()){
key.interestOps(key.interestOps()&(-SelectionKey.OP_READ));
pool.execute(new ThreadHandlerChannel(key));
}
}
}
}
}
class ThreadHandlerChannel extends Thread{
private SelectionKey key;
ThreadHandlerChannel(SelectionKey key){
this.key = key;
}
@Override
public void run() {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
int size = 0;
while ((size=channel.read(buffer))>0){
buffer.flip();
baos.write(buffer.array(),0,size);
buffer.clear();
}
baos.close();
byte[] content = baos.toByteArray();
ByteBuffer writeBuf = ByteBuffer.allocate(content.length);
writeBuf.put(content);
writeBuf.flip();
channel.write(writeBuf);
if(size == -1){
channel.close();
} else{
key.interestOps(key.interestOps()|SelectionKey.OP_READ);
key.selector().wakeup();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
应用场景:
该接口是netty对网络操作的抽象类,它包括基本的IO操作,如bind(),connect(), read(), write() 等
常见的Channel接口实现类是NioServerSocketChannel(服务器)和NioSocketChannel(客户端),这两个Channel可以和BIO编程模型中的ServerSocket和Socket两个概念对应上。netty中Channel提供的API降低了直接使用Socket的复杂性。
EventLoop(事件循环)接口是netty最核心的概念,定义了netty的核心抽象,用于处理连接的生命周期中所发生的事件。
主要作用就是:负责监听网络事件并调用事件处理器进行相关的IO操作。
Channel与EventLoop的联系:
Channel为netty网络操作的抽象类,EventLoop负责处理注册到其上的Channel处理IO操作。
netty中所有IO操作都为异步,无法立刻得到操作是否成功,可以通过ChannelFuture接口的addListener()方法注册一个ChannelFutureListener,当操作执行成功或失败时,监听就会自动触发返回结果。
另外,该接口的channel()方法可以获取相关联的Channel。
另外,该接口的sync()方法可以使异步的操作变成同步的。
public interface ChannelFuture extends Future<Void>{
Channel channel();
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
ChannelFuture sync() throws InterruptedException;
}
前者是消息的具体处理器,负责处理读写操作,客户端连接等。
后者为前者的链,Channel创建时自动分配一个ChannelPipline,ChannelPIPline可以通过addLast()方法添加多个ChannelHandler,因为一个数据或事件可以被多个Handler处理,一个处理完就交给下一个Handler。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iGgDFZKv-1592447304109)(C:\Users\Wensheng\AppData\Roaming\Typora\typora-user-images\image-20200616181408833.png)]
EventLoopGroup包含多个EventLoop(每一个EventLoop通常内部包含一个线程,它的任务是监听网络事件并调用事件处理器来进行IO操作)
上图是一个服务器端对EventLoopGroup使用的大致模块图,bossEventLoopGroup用于客户端连接请求处理,而workerEventLoopGroup用于具体处理器IO相关操作。
前者是客户端的启动引导类,使用如下
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).
......
//建立连接
ChannelFuture f = b.connect(host,port).sync();
f.channel().closeFuture().sync();
}finally{
group.shutdownGracefully();
}
后者是服务器端的启动类,使用如下:
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(boss,worker).
.....
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
}finally{
boss.shutdownGracefully();
worker.shutdownGracefully();
}
可以看出:
大部分网络框架都是基于Reactor模式设计开发的。
Reactor模式基于事件驱动,采用多路复用将事件分发给相应的Handler处理。
在netty主要靠NIOEventLoopGroup线程池实现具体的线程模型。
单线程模型:
一个线程处理所有的accept、read、decode、process、encode、send事件。不适用高负载高并发的场景。
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(worker,worker).
多线程模型
一个Acceptor线程只负责监听客户端的连接,一个NIO线程池负责具体处理:accept、read、decode、Process、encode、send事件。适合并发连接量不大的场景。
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(boss,worker).
主从多线程模型
从主NIO线程池中选一个线程作为Acceptor线程,绑定监听端口,接收客户端连接,其他线程负责后续接入认证工作。从NIO线程池负责具体处理IO读写。
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(boss,worker).
服务器端
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
// (非必备)打印日志
.handler(new LoggingHandler(LogLevel.INFO))
// 4.指定 IO 模型
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定义客户端消息的业务处理逻辑
p.addLast(new HelloServerHandler());
}
});
// 6.绑定端口,调用 sync 方法阻塞知道绑定完成
ChannelFuture f = b.bind(port).sync();
// 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
f.channel().closeFuture().sync();
} finally {
//8.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
基于TCP发送数据时,出现多个字符串粘在一起或者一个字符串被拆开的情况。
解决:
使用netty自带的解码器
LineBasedFrameDecoder
: 发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder
的工作原理是它依次遍历 ByteBuf
中的可读字节,判断是否有换行符,然后进行相应的截取。DelimiterBasedFrameDecoder
: 可以自定义分隔符解码器,LineBasedFrameDecoder
实际上是一种特殊的 DelimiterBasedFrameDecoder
解码器。FixedLengthFrameDecoder
: 固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。LengthFieldBasedFrameDecoder
自定义序列化编解码器
在 Java 中自带的有实现 Serializable
接口来实现序列化,但由于它性能、安全性等原因一般情况下是不会被使用到的。
通常情况下,我们使用 Protostuff、Hessian2、json 序列方式比较多,另外还有一些序列化性能非常好的序列化方式也是很好的选择:
短连接: server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。管理和实现都比较简单,但每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
长连接:client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。
是什么:
在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制 。
心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.
TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE
。但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler
。
零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。
在 OS 层面上的 Zero-copy
通常指避免在 用户态(User-space)
与 内核态(Kernel-space)
之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。
Netty 中的零拷贝体现在以下几个方面:
CompositeByteBuf
类, 可以将多个ByteBuf
合并为一个逻辑上的 ByteBuf
, 避免了各个 ByteBuf
之间的拷贝。ByteBuf
支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf
, 避免了内存的拷贝。FileRegion
包装的FileChannel.tranferTo
实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel
, 避免了传统通过循环 write 方式导致的内存拷贝问题.