阻塞, 非阻塞和异步,同步的理解
- 阻塞/非阻塞: 是相对于数据而言的, 即服务端数据没有准备好时, 阻塞情况下, 客户端要一直等待, 不能做其他操作, 直到服务端返回数据, 而非阻塞就是客户端此时可以暂时离开去做其他的处理操作;
- 同步/异步: 是相对于网络IO而言, 即在同步的时候, 如果此时服务端有未处理完的请求, 其他客户端是不能连接上服务端的, 要一直等待, 而异步就是, 即使服务端有未处理完的的请求, 客户端依然可以连接服务端, 进行请求处理;
BIO,NIO
- BIO:同步阻塞, java中Socket连接, 由于是阻塞式的, 实现通信的效率较低, 通常的实现方式是服务端每接受到一个请求就开启一个线程处理, 但是这种模式, 会因为线程数量而限制服务端的效率;
- NIO:异步非阻塞, java中的NioSocket连接, 为解决BIO的问题, NIO使用的是, NIO是基于selector线程模型设计, 即服务端存在一个线程不断轮询的注册链接过来的channel(可理解为客户端连接), 并对其进行监听, 当某个Channel上发生读或者写的操作时, 就会被轮询出来, 进行处理;
NIO实现代码
服务端
try {
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.configureBlocking(false);// 设置非阻塞
socketChannel.socket().bind(new InetSocketAddress(8081), 1024);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_ACCEPT); //准备接收消息
System.out.println("server is started");
while(true) {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel accept = channel.accept();
accept.configureBlocking(false);
accept.register(selector, SelectionKey.OP_READ); //设置为可读取
}
if (selectionKey.isReadable()) {
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = channel.read(allocate);
if (read > 0) {
allocate.flip();
byte[] bytes = new byte[allocate.remaining()];
allocate.get(bytes);
String content = new String(bytes, "UTF-8");
System.out.println("server recieve message "+content);
long timeMillis = System.currentTimeMillis();
byte[] response = (timeMillis + "").getBytes();
allocate.clear();
allocate.put(response);
allocate.flip();
channel.write(allocate);
} else {
channel.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
客户端
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
if (socketChannel.connect(new InetSocketAddress("127.0.0.1", 8081))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
while (true) {
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
if (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
SocketChannel channel = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (channel.finishConnect()) {
channel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
}else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read > 0) {
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String response = new String(bytes, "UTF-8");
System.out.println("client recieve message "+response);
}else {
key.cancel();
channel.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
private static void doWrite(SocketChannel socketChannel) throws IOException {
byte[] bytes = "query time order".getBytes();
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
buffer.flip();
socketChannel.write(buffer);
}
netty介绍
netty封装了java NIO, 并使用多线程实现了异步非阻塞, 同时还解决了NIO在linux系统下出现空轮询的bug(会造成CPU 100%问题), 主要是在selector.select(time)时, 添加一个计数器, 当空轮询超过512次的时候, 跳出循环, 将原来的channel注册在新的selector上, 原来的selector关闭;
netty解决粘包/拆包问题
发生拆包和粘包的原因主要是服务端只负责接受消息, 但并不知道客户端到底发送了什么, 以及发送的数据逻辑, 只是根据接收到的字节数据进行解码, 而服务端每次读取的数据量是不确定的, 因此会出现拆包和粘包;
通常的解决思路
- 发送定长200消息的包, 不够的使用空格补齐;
- 消息末尾使用空格换行符标记;
- 将一个消息分为消息头和消息体, 消息头中定义消息体的长度;
netty的解码器
- LineBasedFrameDecordor: 使用空格换行符(/r/n)对消息进行解码;
- DelimitorBaseFrameDecordor: 以分割符做为消息结束解码;
- FixedLengthBaseFrameDecordor: 定长解码器;
使用的方式就是将解码器加入到pipeline的末尾;
编解码技术
java原有序列化的缺点
- 不能跨语言
- 编解码的码流大, 占用空间多
- 效率低
业界主流的序列化技术
- 谷歌的protobuf: 体量小, 解析快;
- Facebook的Thrift
- MessagePack
netty的channel的执行顺序
inboundHandler是用来接收请求的
outboundHandler是用来发送请求的
1、InboundHandler是通过fire事件决定是否要执行下一个InboundHandler,如果哪个InboundHandler没有调用fire事件,那么往后的Pipeline就断掉了。
2、InboundHandler是按照Pipleline的加载顺序,顺序执行。
3、OutboundHandler是按照Pipeline的加载顺序,逆序执行。
4、有效的InboundHandler是指通过fire事件能触达到的最后一个InboundHander。
5、如果想让所有的OutboundHandler都能被执行到,那么必须把OutboundHandler放在最后一个有效的InboundHandler之前。
6、推荐的做法是通过addFirst加载所有OutboundHandler,再通过addLast加载所有InboundHandler。
7、OutboundHandler是通过write方法实现Pipeline的串联的。
8、如果OutboundHandler在Pipeline的处理链上,其中一个OutboundHandler没有调用write方法,最终消息将不会发送出去。
9、ctx.writeAndFlush是从当前ChannelHandler开始,逆序向前执行OutboundHandler。
10、ctx.writeAndFlush所在ChannelHandler后面的OutboundHandler将不会被执行。
netty服务端创建流程
netty客户端创建流程
netty的pipeline和channelHandler
- pipeline更像是一个容器, 里面维护了由channelHandler组成的链表;
- channelHandler分为in和out两种, in是用来接收请求的, out开头的是用来发送请求的;
- 通常情况下我们只需要继承ChannelHandlerAdapter接口即可, 也只需要关注其中的几个方法
他们的执行顺序
handlerAdded
channelRegistered
channelActive #连接完成时, 客户端可以用来发送数据
read
channelRead #读取数据, 用来读取数据
channelReadComplete #读取完成时, 调用flush方法将缓存中数据刷进socket中
read
channelReadComplete
read
exceptionCaught #发生异常时
channelInactive
handlerRemoved
* 使用firexxx方法触发下一个channel执行读事件
* 使用write方法返回给客户端
netty架构高性能的原因
- 使用java NIO非阻塞模式
- reactor线程模型的使用, 单线程模式, 多线程模式和主从模式, netty实现了者三种情况;
- 无锁化的串行机制
- 使用高性能序列化框架
- 零拷贝
* Netty提供了CompositeByteBuf(复合缓冲区),可以将多个ByteBuf合并成一个逻辑上的ByteBuf,避免各个ByteBuf间的拷贝;
* 通过wrap操作,可以将byte[]数组,ByteBuf,ByteBuffer等包装成一个Netty ByteBuf对象,进而避免拷贝操作;
* 直接使用堆外内存;
* 通过FileRegion包装的FileChannel的transferTo实现文件传输,直接将文件缓冲区的数据发送到目标channel,避免传统通过循环write方式导致的内存拷贝问题。
关于零拷贝
传统的拷贝
零拷贝原理
netty实现的零拷贝
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。