在了解netty之前我们首先搞清楚,netty是什么?为什么要使用netty?在搞清楚这两个问题之前,我们看看jdk自身对网络传输的解决方案。
1. 传统IO
简单演示一下使用传统IO,客户端向服务端发送一段字符串的代码
服务端
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8888);
// 开启接收客户端连接线程
new Thread(() -> {
// 不断主动轮询
while (true) {
try {
// 阻塞方法接收新连接
Socket clientSocket = serverSocket.accept();
// 开启线程专门负责处理新的连接
new Thread(() -> {
try {
invokeSocket(clientSocket);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
static void invokeSocket(Socket socket) throws Exception {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println("接收到客户端请求: " + new String(data, 0, len));
}
}
客户端:
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8888);
for (; ; ) {
System.out.println("向服务端写数据");
// 字节流写数据
socket.getOutputStream().write(("hello world").getBytes());
Thread.sleep(1000);
}
}
有上面的代码可以看到,客户端给服务端发送了一段字符串,服务端接收到一个socket,然后使用开一个线程进行处理,模型如下:
由上图以及代码可以看到,传统IO有以下弊端:
- 线程资源受限,一个客户端链接就需要开启一个线程处理,有一万一个客户端就需要开启一万个线程,线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起
- 服务器的CPU数量是极其有限的,在大量客户端连接服务器的时候,要进行业务处理时,需要进行客户端线程切换,系统性能会急剧下降
- 传统IO使用流进行处理数据,使用字节作为读取单位,读取慢,还不能重复读取
2. JDK的NIO编程
服务端
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8888));
// 取消阻塞模式
listenerChannel.configureBlocking(false);
// 注册轮询器
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
for (; ; ) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
for (; ; ) {
// 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 面向 Buffer,直接读取一块内存,效率比传统io高多了
clientChannel.read(byteBuffer);
// 用完需要清空内存块
byteBuffer.flip();
System.out.println("收到客户端请求" + Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
客户端:
private static SelectorProvider provider = SelectorProvider.provider();
private static boolean close = false;
private static void write(SocketChannel client, String msg) throws IOException {
byte[] bytes = msg.getBytes(Charset.forName("UTF-8"));
// 建立HeapByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
buffer.flip(); // 切换为读模式
client.write(buffer);
}
private static int read(SocketChannel client) throws IOException {
// ByteBuffer分配空间
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 直到没有数据 || buffer满
int len = client.read(buffer);
if (len > 0) {
// buffer.array():取HeapByteBuffer中的原始byte[]
System.out.println(new String(buffer.array(), 0, len, Charset.forName("UTF-8")));
}
return len;
}
public static void main(String[] args) throws IOException {
try (
Selector selector = provider.openSelector();
SocketChannel client = provider.openSocketChannel()
) {
// 非阻塞模式
client.configureBlocking(false);
// 注册
SelectionKey key = client.register(selector, 0, null);
// 连接成功
if (client.connect(new InetSocketAddress("127.0.0.1", 8888))) {
System.out.println("连接服务器成功...");
// 监听读就绪和写就绪(准备写数据)
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
// 连接失败(正常情况下)
else {
// 监听连接就绪
key.interestOps(SelectionKey.OP_CONNECT);
}
while (!close) {
// 监听就绪事件
selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
key = it.next();
// 从已选择键集移除key
it.remove();
// 连接就绪
if (key.isConnectable()) {
// 完成连接
client.finishConnect();
System.out.println("完成连接...");
// 取消监听连接就绪(否则selector会不断提醒连接就绪)
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT);
// 监听读就绪和写就绪
key.interestOps(key.interestOps() | SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else {
// 写就绪
if (key.isWritable()) {
System.out.println("开始写数据...");
write(client,"Hello NioServer!");
// 取消写就绪,否则会一直触发写就绪(写就绪为代码触发)
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
// key有效(避免在写就绪时关闭了channel或者取消了key) && 读就绪
if (key.isValid() && key.isReadable()) {
System.out.println("读数据...");
// 服务器已关闭socket
if (read(client) < 0) {
close = true;
}
}
}
}
}
}
}
Java NIO模型:
JDK NIO优点
NIO相对于传统IO, java的NIO采用多路复用模型,一条链接进来之后,注册到selector中,由selector(阻塞)进行轮询,批量监测出有数据可读的连接,就可以对链接中的数据进行处理了。举个栗子来说明,就比如去海底捞吃火锅,海底捞不会每一桌客人都配备一名服务员,而是客人有需要去通知服务员到来,或者是每个区域的服务员不停地轮询是否有客人需要服务,如果没有,他们可以去做其他事情。
由于IO是多路复用的,对于服务器大量链接来说,可以大大减少创建用户线程,减少线程间切换,提高了服务器性能
JDK NIO数据是面向buffer的,每次读取一块buffer的数据,读取效率也大大提高
NIO缺点
虽然NIO相比传统IO有诸多的有点,但是同样面临着许多问题:
NIO的类库和API对初学者非常不友好,你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等等的使用,并且代码冗长,就比如上面的那段demo,一般人即便是看也感觉非常吃力。
需要额外的知识点作为铺垫,想使用好NIO,要非常熟悉Java多线程编程,涉及到的reactor模型,需要掌握网络编程知识和多线程知识,并且JDK 没有帮助我们封装通用的处理方案,比如编解码、网络断开重连以及拆包粘包都需要我们自己去实现,增加开发难度。
JDK 的 NIO 底层由 epoll 实现,它会导致 Selector 空轮询,最终导致 CPU 100%。
官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本
解决。
3. netty
netty是什么,以下是netty官网对netty的介绍
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty是一个异步事件驱动的网络应用框架,相对JDK的NIO,它做了很多优雅的设计,让开发者可以更多的关注业务开发,不用太多的关注底层的网络层编码,总的来说,netty有以下特点:
- netty封装了多种IO模型,开发者可以随意切换,只需要改动参数就可以使用IO模型(传统IO, NIO)
- 使用reactor模型(图3.2),实现了非阻塞IO,并且解决了JDK空轮询的bug
- netty做了很多通用实现的封装,比如编解码以及拆包粘包以及http或者websocket等都有相应的处理器处理
- 使用bytebuf,减少了内存复制,减少资源消耗;最小化不必要的内存复制
- 社区活跃,不断更新,社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。
- 各大组件(框架)都在使用netty,性能饱经考验,健壮性无比强大
netty常见的使用场景
- 互联网行业。在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。
- 游戏行业。无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过 Netty 进行高性能的通信。
使用到netty的组件
netty服务端代码
public static void main(String[] args) {
// 服务端启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 创建两个线程池,boss专门负责客户端连接,worker处理每一条连接的数据读写的线程组
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 注册处理的线程池
serverBootstrap.group(bossGroup, workerGroup)
// option方法表示设置服务端channel的选项
// 表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数
.option(ChannelOption.SO_BACKLOG, 100)
// childOption 表示针对每一个客户端连接设置TCP连接属性
// ChannelOption.SO_KEEPALIVE 表示是否开启TCP底层心跳机制,true为开启
.childOption(ChannelOption.TCP_NODELAY, true)
// 表示是否开启Nagle算法,true表示关闭,false表示开启,如果需要高效实时的通信,就关闭,如果需要减少网络发送频率,就开启
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 决定使用使用哪种io模型,这里也可以使用传统阻塞io,但是我们没必要这样做,因为nio才是netty的特性
.channel(NioServerSocketChannel.class)
// 维护一个服务端使用的map,保存一些全局配置,通过channel.attr()获取设置的属性,一般情况下用不上它~
.attr(AttributeKey.newInstance("key"), "value")
// 为每一个链接设置属性
.childAttr(AttributeKey.newInstance("client"), "netty client")
// 针对每一个链接的业务进行处理
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRegistered(ChannelHandlerContext ctx) {
System.out.println("有客户端注册" + ctx.channel().remoteAddress());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到客户端消息: " + byteBuf.toString(Charset.forName("utf-8")));
ByteBuf response = ctx.alloc().buffer();
response.writeBytes("服务端收到回复".getBytes());
ctx.channel().writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println("服务端异常, 原因:" + cause.toString());
}
});
}
})
// 处理一些服务端启动过程中的一些逻辑,与childHandler方法相对,一般情况下用不上它~
.handler(new ChannelInitializer<NioServerSocketChannel>() {
@Override
protected void initChannel(NioServerSocketChannel ch) {
System.out.println("服务端启动");
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8888).addListener(
future -> {
if (future.isSuccess()) {
System.out.println("这里对绑定端口做监听");
} else {
System.out.println("绑定失败");
}
}
).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 优雅关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
netty客户端
public static void main(String[] args) {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
try {
bootstrap.group(nioEventLoopGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf result = (ByteBuf) msg;
System.out.println("收到服务器应答:" + result.toString(Charset.forName("utf-8")));
ctx.channel().writeAndFlush(ctx.alloc().buffer().writeBytes("收到服务端回复".getBytes()));
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) {
System.out.println("成功注册到服务端");
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 只能写butebuf
ByteBuf byteBuf = ctx.alloc().buffer();
ctx.channel().writeAndFlush(byteBuf.writeBytes("hello server".getBytes()));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("客户端异常: " + cause.toString());
}
});
}
}).connect("127.0.0.1", 8888).addListener(
future -> {
if (future.isSuccess()) {
System.out.println("链接服务器成功");
} else {
System.out.println("链接服务器失败,原因:" + future.cause());
System.exit(0);
}
}
).channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
nioEventLoopGroup.shutdownGracefully();
}
}
通过和传统IO和Java的NIO代码比较,netty的服务端以及客户端仅仅需要数行代码就可以完成一个高性能的网络服务应用,并且做了良好的封装,具体的代码解释都在注释中了。
代码地址