NIO同步阻塞与同步非阻塞
BIO与NIO
IO(BIO)和NIO区别:其本质就是阻塞和非阻塞的区别
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止。
非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。
IO为同步阻塞形式,NIO为同步非阻塞形式,NIO并没有实现异步,在JDK1.7后升级NIO库包,支持异步非阻塞
同学模型NIO2.0(AIO)
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪:
或者采用轮训的策略实时检查数据的就绪状态,如果就绪则获取数据.
异步时,则所有的IO读写操作交给操作系统,与我们的应用程序没有直接关系,我们程序不需要关系IO读写,当操作
系统完成了IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据极即可。
伪异步
由于BIO一个客户端需要一个线程去处理,因此我们进行优化,后端使用线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大的线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
原理:
当有新的客户端接入时,将客户端的Socket封装成一个Task(该Task任务实现了java的Runnable接口)投递到后端的线程池中进行处理,由于线程池可以设置消息队列的大小以及线程池的最大值,因此,它的资源占用是可控的,无论多少个客户端的并发访问,都不会导致资源的耗尽或宕机。
使用多线程支持多个请求
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善
//tcp服务器端...
class TcpServer {
public static void main(String[] args) throws IOException {
System.out.println("socket tcp服务器端启动....");
ServerSocket serverSocket = new ServerSocket(8080);
// 等待客户端请求
try {
while (true) {
Socket accept = serverSocket.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = accept.getInputStream();
// 转换成string类型
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String str = new String(buf, 0, len);
System.out.println("服务器接受客户端内容:" + str);
} catch (Exception e) {
// TODO: handle exception
}
}
}).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
serverSocket.close();
}
}
}
public class TcpClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("socket tcp 客户端启动....");
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我是蚂蚁课堂".getBytes());
socket.close();
}
}
使用线程池管理线程
//tcp服务器端...
class TcpServer {
public static void main(String[] args) throws IOException {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
System.out.println("socket tcp服务器端启动....");
ServerSocket serverSocket = new ServerSocket(8080);
// 等待客户端请求
try {
while (true) {
Socket accept = serverSocket.accept();
//使用线程
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
InputStream inputStream = accept.getInputStream();
// 转换成string类型
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String str = new String(buf, 0, len);
System.out.println("服务器接受客户端内容:" + str);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
serverSocket.close();
}
}
}
public class TcpClient {
public static void main(String[] args) throws UnknownHostException, IOException {
System.out.println("socket tcp 客户端启动....");
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("我是蚂蚁课堂".getBytes());
socket.close();
}
}
IO模型关系
什么是阻塞
阻塞概念:应用程序在获取网络数据的时候,如果网络传输很慢,那么程序就一直等着,直接到传输完毕。
什么是非阻塞
应用程序直接可以获取已经准备好的数据,无需等待.
IO为同步阻塞形式,NIO为同步非阻塞形式。NIO没有实现异步,在JDK1.7之后,升级了NIO库包
,支持异步费阻塞通讯模型NIO2.0(AIO)
Netty快速入门
什么是Netty
Netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。
Netty应用场景
1.分布式开源框架中dubbo、Zookeeper,RocketMQ底层rpc通讯使用就是netty。
2.游戏开发中,底层使用netty通讯。
为什么选择netty
在本小节,我们总结下为什么不建议开发者直接使用JDK的NIO类库进行开发的原因:
1) NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
2) 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;
3) 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大;
4) JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决。
Netty通信实践
Netty服务器端
package com.xiaofeng.netty.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
/**
* @author xiaofeng
* @version V1.0
* @title: NettyServer
* @package: com.xiaofeng.netty.server
* @description: netty server
* @date 2019/12/17 11:24
*/
public class NettyServer {
private static class SingletionNettyServer {
static final NettyServer instance = new NettyServer();
}
public static NettyServer getInstance() {
return SingletionNettyServer.instance;
}
private EventLoopGroup mainGroup;
private EventLoopGroup subGroup;
private ServerBootstrap server;
private ChannelFuture future;
public NettyServer() {
//主线程组,用于接受客户端的连接,但是不做任何处理,跟老板一样,不做事
mainGroup = new NioEventLoopGroup();
//从线程组, 老板线程组会把任务丢给他,让手下线程组去做任务
subGroup = new NioEventLoopGroup();
//server启动类
server = new ServerBootstrap();
//建立联系
server.group(mainGroup, subGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new NettyServerInitialzer());
}
public void start() {
this.future = server.bind(9999);
System.err.println("netty server 启动完毕...");
}
public static void main(String[] args) {
NettyServer.getInstance().start();
}
}
package com.xiaofeng.netty.server;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* @author xiaofeng
* @version V1.0
* @title: NettyServerInitialzer
* @package: com.xiaofeng.netty.server
* @description: 服务端初始化器
* @date 2019/12/17 11:30
*/
public class NettyServerInitialzer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("stringD", new StringDecoder());
pipeline.addLast("stringC", new StringEncoder());
pipeline.addLast("http", new HttpClientCodec());
// 自定义的handler
pipeline.addLast(new ServerChatHandler());
}
}
package com.xiaofeng.netty.server;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
/**
* @author xiaofeng
* @version V1.0
* @title: ServerChatHandler
* @package: com.xiaofeng.netty.server
* @description: 处理消息的handler
* @date 2019/12/17 11:38
*/
public class ServerChatHandler extends SimpleChannelInboundHandler<String> {
// 用于记录和管理所有客户端的channle
public static ChannelGroup users =
new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s)
throws Exception {
Channel channel = ctx.channel();
for (Channel ch : users) {
if (ch == channel) {
System.out.println("收到来自客户端[" + channel.id().asShortText() + "]的消息:" + s);
ctx.writeAndFlush("[你说]:" + s + "\n");
} else {
ctx.writeAndFlush("[" + channel.remoteAddress() + "]" + s + "\n");
}
}
}
/**
* 当客户端连接服务端之后(打开连接)
* 获取客户端的channle,并且放到ChannelGroup中去进行管理
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
String channelId = ctx.channel().id().asShortText();
System.out.println("客户端加入,channelId为:" + channelId);
users.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
String channelId = ctx.channel().id().asShortText();
System.out.println("客户端被移除,channelId为:" + channelId);
// 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
users.remove(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除
ctx.channel().close();
users.remove(ctx.channel());
}
}
Netty客户端
package com.xiaofeng.netty.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author xiaofeng
* @version V1.0
* @title: NettyClient
* @package: com.xiaofeng.netty.client
* @description: netty client
* @date 2019/12/17 11:14
*/
public class NettyClient {
private static class SingletionNettyClient {
static final NettyClient instance = new NettyClient();
}
public static NettyClient getInstance() {
return SingletionNettyClient.instance;
}
private EventLoopGroup eventGroup;
private Bootstrap client;
public NettyClient() {
eventGroup = new NioEventLoopGroup();
//创建客户端启动类
client = new Bootstrap();
client.group(eventGroup)
.channel(NioSocketChannel.class)
.handler(new NettyClientInitialzer());
}
public void start() throws InterruptedException, IOException {
Channel channel = client.connect("127.0.0.1", 9999).sync().channel();
while (true) {
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in));
String input = reader.readLine();
if (input != null) {
if ("quit".equals(input)) {
System.exit(1);
}
channel.writeAndFlush(input);
}
}
}
public static void main(String[] args) throws InterruptedException, IOException {
NettyClient.getInstance().start();
}
}
package com.xiaofeng.netty.client;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* @author xiaofeng
* @version V1.0
* @title: NettyClientInitialzer
* @package: com.xiaofeng.netty.client
* @description: 客户端初始化器
* @date 2019/12/17 11:29
*/
public class NettyClientInitialzer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel channel) throws Exception {
//获取管道
ChannelPipeline pipeline = channel.pipeline();
//添加编解码器
pipeline.addLast("stringD", new StringDecoder());
pipeline.addLast("stringC", new StringEncoder());
pipeline.addLast("http", new HttpClientCodec());
// 自定义的handler
pipeline.addLast(new ClientChatHandler());
}
}
package com.xiaofeng.netty.client;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @author xiaofeng
* @version V1.0
* @title: ClientChatHandler
* @package: com.xiaofeng.netty.client
* @description: 处理消息的handler
* @date 2019/12/17 11:38
*/
public class ClientChatHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s)
throws Exception {
System.out.println(s);
}
}
TCP粘包、拆包问题解决方案
什么是粘包/拆包
一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题。
下面可以看一张图,是客户端向服务端发送包:
1. 第一种情况,Data1和Data2都分开发送到了Server端,没有产生粘包和拆包的情况。
2. 第二种情况,Data1和Data2数据粘在了一起,打成了一个大的包发送到Server端,这个情况就是粘包。
3. 第三种情况,Data2被分离成Data2_1和Data2_2,并且Data2_1在Data1之前到达了服务端,这种情况就产生了拆包。
由于网络的复杂性,可能数据会被分离成N多个复杂的拆包/粘包的情况,所以在做TCP服务器的时候就需要首先解决拆包/
解决办法
1.消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
sc.pipeline().addLast(new FixedLengthFrameDecoder(10));
2.包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
ByteBuf buf = Unpooled.copiedBuffer("_mayi".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段