1.1 Netty案例:客户端与服务器之间通信
目标:使用 Netty 开发一个网络应用程序,实现服务端和客户端之间的数据通信。
实现步骤:
- 导入依赖
- 编写Netty服务端程序:配置线程组、配置自定义业务处理类、绑定端口号、启动Server,等待Client连接
- 编写服务端-业务处理类Handler:继承ChannelInboundHandlerAdapter,并重写三个方法:读取事件、读取完成事件、异常捕获事件
- 编写客户端程序:配置线程组、配置自定义业务处理类,启动Client,连接Server
- 编写客户端-业务处理类:继承ChannelInBoundHandlerAdapter,重写2个方法:通道就绪事件、读取时间
导入依赖
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.8.Final</version>
</dependency>
</dependencies>
服务端程序Server
package com.example.netty1.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServer {
public static void main(String[] args) throws Exception {
// 1. 创建一个线程组:接收服务端连接
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
// 2. 创建一个线程组:处理网络操作
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
// 3. 创建服务端助手,来配置参数
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
// 4. 设置两个线程组
.group(bossGroup, workerGroup)
// 5. 使用NioServerSocketChannel作为服务端通道的实现
.channel(NioServerSocketChannel.class)
// 6. 设置线程队列中等待连接的个数
.option(ChannelOption.SO_BACKLOG, 128)
// 7. 保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 8. 创建一个通道初始化对象
.childHandler(new ChannelInitializer<SocketChannel>() {
public void initChannel(SocketChannel sc) {
// 9. 往pipeline中添加一个自定义handler类
sc.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println(".......服务端启动中 int port:9999......");
// 10. 绑定端口 bind()方法是异步的 sync方法是同步阻塞的
ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
System.out.println("......服务端启动成功......");
// 11. 关闭通道,关闭线程组
channelFuture.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
服务端自定义业务处理类
package com.example.netty1.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.nio.charset.StandardCharsets;
/**
* 服务端的业务处理类
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数据事件,msg为客户端发过来的信息
*/
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 用缓冲区接受数据
ByteBuf buf = (ByteBuf) msg;
// 转成字符串
System.out.println("Client msg: " +buf.toString(CharsetUtil.UTF_8));
}
/**
* 数据读取完成事件,读完客户端数据后回复客户端
* @param ctx 事件处理器上下文
*/
public void channelReadComplete(ChannelHandlerContext ctx) {
// Unlooped.copiedBuffer 获取到缓冲区
ctx.writeAndFlush(Unpooled.copiedBuffer("宝塔镇河妖", StandardCharsets.UTF_8));
}
/**
* 异常发生事件
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
// 异常时,关闭ChannelHandlerContext,它是相关信息的汇总,关闭他,其他的也就关闭了
ctx.close();
}
}
客户端
package com.example.netty1.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
public static void main(String[] args) throws Exception {
// 1.创建一个线程组
EventLoopGroup group = new NioEventLoopGroup();
// 2.创建一个客户端助手,完成相关配置
Bootstrap bootstrap = new Bootstrap();
// 3.设置线程组
bootstrap.group(group)
// 4.设置客户端通道的实现类
.channel(NioSocketChannel.class)
// 5.创建一个通道初始化对象
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 6.往pipeline添加自定义的handler
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端就绪... msg发射...");
// 7.启动客户端去连接服务端 connect方法是异步的,sync方法是非阻塞的
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9999).sync();
// 8.关闭连接(异步非阻塞)
channelFuture.channel().closeFuture().sync();
}
}
客户端自定义事件处理类
package com.example.netty1.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就绪事件
*/
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush(Unpooled.copiedBuffer("天王盖地虎", CharsetUtil.UTF_8));
}
/**
* 读取数据事件
*/
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
System.out.println("Server msg: " + buf.toString(CharsetUtil.UTF_8));
}
}
1.2 Netty案例:网络聊天室V2.0
-
编写聊天程序服务端:配置线程组,配置编解码器,配置自定义业务处理类,绑定端口号,启动Server,等待Client连接。
-
编写服务端-业务处理类:
(1)当通道就绪,输出上线
(2)当通道未就绪,输出离线
(3)当通道发来数据时,读取数据,进行广播
-
编写聊天程序客户端:配置线程组,配置编解码器,配置自定义业务处理类,启动Client,连接Server。
(1)连接服务端成功后,获取客户端与服务端建立的Channel
(2)获取系统键盘输入,将用户输入信息通过channel发送给客户端
-
编写客户端-业务处理类:
(1)读取事件:监听服务端广播消息
聊天程序服务端ChatServer:
往 Pipeline 链中添加了处理字符串的编码器和解码器,它们加入到 Pipeline 链中后会自动工作,使得服务端读写字符串数据时更加方便,不用人工处理 编解码操作。
package com.example.netty1.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyChatServer {
//服务端口号
private int port;
public NettyChatServer(int port) {
this.port = port;
}
public void run() {
// 1. 配置线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置两个线程组
serverBootstrap.group(bossGroup, workerGroup)
// 使用NioServerSocketChannel作为通道实现
.channel(NioServerSocketChannel.class)
// 设置线程队列中等待连接个数
.option(ChannelOption.SO_BACKLOG, 128)
// 保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new NettyChatServerHandler());
}
});
System.out.println("网络聊天室Server启动。。。");
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
System.out.println("网络聊天室Server关闭。。。");
}
}
public static void main(String[] args) throws Exception {
new NettyChatServer(9999).run();
}
}
服务端业务处理类ChatServerHandler
上述代码通过继承 SimpleChannelInboundHandler 类自定义了一个服务端业务处理类,并在该类中重写了三个方法。
当通道就绪时,输出上线
当通道未就绪时,输出离线
当通道发来数据时,读取数据,进行广播
package com.example.netty1.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
import java.util.ArrayList;
import java.util.List;
public class NettyChatServerHandler extends SimpleChannelInboundHandler<String> {
public static List<Channel> channelList = new ArrayList<>();
// 通道就绪
@Override
public void channelActive(ChannelHandlerContext ctx) {
Channel inChannel = ctx.channel();
channelList.add(inChannel);
System.out.println("[Server]: " + inChannel.remoteAddress().toString().substring(1) + "上线");
}
// 通道未就绪
@Override
public void channelInactive(ChannelHandlerContext ctx) {
Channel channel = ctx.channel();
channelList.remove(channel);
System.out.println("[Server]: " + channel.remoteAddress().toString().substring(1) + "离线");
}
// 读取数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) {
Channel inChannel = ctx.channel();
System.out.println("s = " +s);
for (Channel channel : channelList) {
if (channel != inChannel) {
channel.writeAndFlush("[" + channel.remoteAddress().toString().substring(1) + "] 说:" + s + "\n");
}
}
}
}
聊天程序客户端ChatClient
通过 Netty 编写了一个客户端程序。客户端同样需要配置编解码器
package com.example.netty1.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyChatServer {
//服务端口号
private int port;
public NettyChatServer(int port) {
this.port = port;
}
public void run() {
// 1. 配置线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置两个线程组
serverBootstrap.group(bossGroup, workerGroup)
// 使用NioServerSocketChannel作为通道实现
.channel(NioServerSocketChannel.class)
// 设置线程队列中等待连接个数
.option(ChannelOption.SO_BACKLOG, 128)
// 保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new NettyChatServerHandler());
}
});
System.out.println("网络聊天室Server启动。。。");
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
System.out.println("网络聊天室Server关闭。。。");
}
}
public static void main(String[] args) throws Exception {
new NettyChatServer(9999).run();
}
}
客户端业务处理类ChatClientHandler
上述代码通过继承 SimpleChannelInboundHandler 自定义了一个客户端业务处理类,重写了一个方法用来读取服务端发过来的数据。
package com.example.netty1.netty;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println(s.trim());
}
}
1.3 编码和解码
为什么要编码和解码:因为计算机数据传输的是二进制的字节数据。
- 解码:字节数据 -》 字符串(字符数据)
- 编码:字符串(字符数据) -》 字节数据
1.3.1 概述
我们在编写网络应用程序的时候需要注意codec(编解码器),因为数据在网络中传输都是二进制字节码数据,而我们拿到的目标数据往往不是字节码数据。因此在发送数据的时候就需要编码,在接收数据的时候就需要解码。
codec的组成部分有2个:decoder(解码器)和encoder(编码器)
decoder负责将字节数据转为业务数据
encoder负责将业务数据转为字节数据
其中Java的序列化技术就可以作为coder使用,但是他的硬伤太多:
- 无法跨语言,应该是Java序列化最大的问题
- 序列化后的体积太大,是二进制编码的5倍多
- 序列化性能太差
Netty自身提供了一些编解码器,如下:
- StringEncoder对字符串数据进行编码
- ObjectEncoder对Java对象进行编码
Netty自带的ObjectEncoder和StringEncoder可以用来实现POJO对象或各种业务对象的编码和解码,但其内部使用的还是Java的序列化技术,所以在某些场景下不适用。对于POJO对象或各种业务对象要实现编码和解码,我们需要更高效更强的技术。
1.3.2 Google的Protobuf
Protobuf是Google发布的开源项目,全称Google Protoful Buffers,特点如下:
- 支持跨平台、多语言(支持目前大部分语言,如Java、Python、C++等)
- 高性能、高可靠性
- 使用protoful编码器能自动生成代码,protoful是将类的定义使用.proto文件进行描述,然后通过protoc.exe编辑器根据.proto自动生成Java文件。
在使用Netty开发时,经常会结合Protoful作为codec去使用,具体用法如下:
使用步骤:
- 将传递数据的实体类生成(根据构建者模式设计)
- 配置编解码器
- 传递数据使用生成后的实体类
1.3.3 导入Protoful依赖
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.6.1</version>
</dependency>
1.3.4 proto 文件
假设我们要处理的数据是图书信息,那就需要为此编写 proto 文件
syntax = "proto3";
option java_outer_classname = "BookMessage";
message Book{
int32 id = 1;
string name = 2;
}
1.3.5 生成Java类
通过 protoc.exe 根据描述文件生成 Java 类
cd C:\protoc-3.6.1-win32\bin
protoc --java_out=. Book.proto
1.3.6 客户端
在编写客户端程序时,要向 Pipeline 链中添加 ProtobufEncoder 编码器对象。
package com.example.netty1.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.util.Scanner;
public class NettyEncoderDecoderClient {
private final String host;
private final Integer port;
public NettyEncoderDecoderClient(String host, Integer port) {
this.host = host;
this.port = port;
}
public void run () {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast(new NettyEncoderDecoderClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
Channel channel = channelFuture.channel();
System.out.println("-----" + channel.localAddress().toString().substring(1) + "-----");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
channel.writeAndFlush(msg + "\n");
}
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) {
new NettyEncoderDecoderClient("127.0.0.1", 9999).run();
}
}
1.3.7 客户端业务类
在往服务端发送图书(POJO)时就可以使用生成的 BookMessage 类搞定
package com.example.netty1.netty;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyEncoderDecoderClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
BookMessage.Book book = BookMessage.Book.newBuilder().setId(1).setName("天王盖地虎").build();
ctx.writeAndFlush(book);
}
}
1.3.8 服务端
在编写服务端程序时,要向 Pipeline 链中添加 ProtobufDecoder 解码器对象。
package com.example.netty1.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyEncoderDecoderServer {
//服务端口号
private int port;
public NettyEncoderDecoderServer(int port) {
this.port = port;
}
public void run() {
// 1. 配置线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 设置两个线程组
serverBootstrap.group(bossGroup, workerGroup)
// 使用NioServerSocketChannel作为通道实现
.channel(NioServerSocketChannel.class)
// 设置线程队列中等待连接个数
.option(ChannelOption.SO_BACKLOG, 128)
// 保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder", new ProtobufDecoder(BookMessage.Book.getDefaultInstance()));
pipeline.addLast(new NettyEncoderDecoderServerHandler());
}
});
System.out.println("网络聊天室Server启动。。。");
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
System.out.println("网络聊天室Server关闭。。。");
}
}
public static void main(String[] args) throws Exception {
new NettyEncoderDecoderServer(9999).run();
}
}
1.3.9 服务端业务类
在服务端接收数据时,直接就可以把数据转换成 POJO 使用
package com.example.netty1.netty;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class NettyEncoderDecoderServerHandler extends ChannelInboundHandlerAdapter {
// 读取数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
BookMessage.Book book = (BookMessage.Book) msg;
System.out.println("客户端说:" +book.getName());
}
}