##
###六、 实战: 客户端和服务端双向通信
本节我们要实现的功能是客户端连接成功后,向服务端写出一段数据 , 服务端收到数据后打印 , 并向客户端回复一段数据 。
我们先做一个代码框架 , 然后在框架上面做修改
public class Test_07_客户端和服务端双向通信 {
public static void main(String[] args) {
Test_07_Server.start(8000);
Test_07_Client.start("127.0.0.1" , 8000 ,5);
}
}
class Test_07_Client{
public static void start(String IP , int port ,int maxRetry){
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
}
});
connect(bootstrap , IP , port , maxRetry);
}
private static void connect(Bootstrap bootstrap, String IP, int port, int maxRetry , int... retryIndex) {
bootstrap.connect(IP , port).addListener(future ->{
int[] finalRetryIndex;
if(future.isSuccess()) {
System.out.println("连接成功");
}else if(maxRetry ==0) {
System.out.println("达到最大重试此时,放弃重试");
}else {
// 初始化 重试计数
if(retryIndex.length == 0) {
finalRetryIndex = new int[]{0};
}else {
finalRetryIndex = retryIndex;
}
// 计算时间间隔
int delay = 1 << finalRetryIndex[0];
// 执行重试
System.out.println(new Date() +" 连接失败,剩余重试次数:"+ maxRetry + ","+delay+"秒后执行重试");
bootstrap.config().group().schedule(()->{
connect(bootstrap , IP, port , maxRetry -1 , finalRetryIndex[0]+1);
}, delay, TimeUnit.SECONDS);
}
});
}
}
class Test_07_Server{
public static void start(int port){
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new Test_07_ServerHandler());
}
});
bind(serverBootstrap, port);
}
private static void bind(ServerBootstrap serverBootstrap, int port) {
serverBootstrap
.bind(port)
.addListener(future -> {
if(future.isSuccess()) {
System.out.println("服务端:端口【"+port+"】绑定成功!");
}else {
System.out.println("服务端:端口【"+port+"】绑定失败,尝试绑定【"+(port+1)+"】!");
bind(serverBootstrap, port+1);
}
});
}
}
客户端发送数据到服务端
在《客户端启动流程》这一小节 , 我们提到 客户端相关的数据读写逻辑是通过BootStrap的handler()方法指定
bootstrap.group(workerGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
}
});
现在我们在initChannel()中给客户端添加一个逻辑处理器 , 这个处理器的作用就是负责向服务端写数据
bootstrap.group(workerGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 添加业务处理逻辑 可以添加自定义的业务处理逻辑也可以添加 Netty自带的简单通用的处理逻辑
ch.pipeline().addLast(new Test_07_ClientHandler());
}
});
ch.pipeline()方法返回的是和这条连接相关的逻辑处理链 , 采用了责任链处理模式 , 这里不理解没关系 , 后面会讲到。
然后再调用addLast()方法添加一个逻辑处理器 , 这个逻辑处理器为的就是在客户端建立连接成功之后向服务端写数据 , 下面是这个逻辑处理器的代码:
class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(new Date() + " 客户端写出数据...");
// 1. 获取数据
ByteBuf buffer = getByteBuf(ctx);
// 2. 写数据
ctx.channel().writeAndFlush(buffer);
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
// 获取二进制抽象 ByteBuffer
ByteBuf buf = ctx.alloc().buffer();
// 准备数据
byte[] bs = "你好,奥特曼!".getBytes(Charset.forName("UTF-8"));
// 把数据填充到 buf
buf.writeBytes(bs);
return buf;
}
}
服务端读取客户端数据
服务端的数据处理逻辑 是通过ServerBootStrap 的childHandler()方法指定
serverBootStrtap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// TODO Auto-generated method stub
}
})
现在 , 我们在initChannel() 中 给服务端添加一个逻辑处理器 , 这个处理器 的作用就是负责客户端读数据
serverBootStrtap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new Test_07_ServerHandler());
}
})
这个方法里的逻辑和客户端类似 , 获取服务端关于这条连接的逻辑处理链pipeline , 然后添加一个逻辑处理器 , 负责读取客户端发来的数据
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));
}
}
运行测试
完整代码
import java.nio.charset.Charset;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class Test_07_客户端和服务端双向通信 {
public static void main(String[] args) throws Exception {
Test_07_Server.start(8000);
Test_07_Client.start("127.0.0.1", 8000, 5);
}
}
class Test_07_Client {
public static void start(String IP, int port, int maxRetry) {
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 添加业务处理逻辑 可以添加自定义的业务处理逻辑也可以添加 Netty自带的简单通用的处理逻辑
ch.pipeline().addLast(new Test_07_ClientHandler());
}
});
connect(bootstrap, IP, port, maxRetry);
}
private static void connect(Bootstrap bootstrap, String IP, int port, int maxRetry, int... retryIndex) {
bootstrap.connect(IP, port).addListener(future -> {
int[] finalRetryIndex;
if (future.isSuccess()) {
System.out.println("客户端连接【"+IP+":"+port+"】成功");
} else if (maxRetry == 0) {
System.out.println("达到最大重试此时,放弃重试");
} else {
// 初始化 重试计数
if (retryIndex.length == 0) {
finalRetryIndex = new int[] { 0 };
} else {
finalRetryIndex = retryIndex;
}
// 计算时间间隔
int delay = 1 << finalRetryIndex[0];
// 执行重试
System.out.println(new Date() + " 连接失败,剩余重试次数:" + maxRetry + "," + delay + "秒后执行重试");
bootstrap.config().group().schedule(() -> {
connect(bootstrap, IP, port, maxRetry - 1, finalRetryIndex[0] + 1);
}, delay, TimeUnit.SECONDS);
}
});
}
}
class Test_07_Server {
public static void start(int port) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new Test_07_ServerHandler());
}
});
bind(serverBootstrap, port);
}
private static void bind(ServerBootstrap serverBootstrap, int port) {
serverBootstrap
.bind(port)
.addListener(future -> {
if(future.isSuccess()) {
System.out.println("服务端:端口【"+port+"】绑定成功!");
}else {
System.out.println("服务端:端口【"+port+"】绑定失败,尝试绑定【"+(port+1)+"】!");
bind(serverBootstrap, port+1);
}
});
}
}
class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String content = "你好,奥特曼!";
System.out.println(new Date() + " 客户端写出数据:"+content);
// 1. 获取数据
ByteBuf buffer = getByteBuf(ctx , content);
// 2. 写数据
ctx.channel().writeAndFlush(buffer);
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx , String content ) {
// 获取二进制抽象 ByteBuffer
ByteBuf buf = ctx.alloc().buffer();
// 准备数据
byte[] bs = content.getBytes(Charset.forName("UTF-8"));
// 把数据填充到 buf
buf.writeBytes(bs);
return buf;
}
}
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));
}
}
运行结果:
服务端回复数据给客户端
服务端向客户端写数据的逻辑与客户端向服务端写数据的逻辑一样 , 先创建一个ByteBuf , 然后填充二进制数据 , 最后调用writeAndFlush()方法写出去
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));
// 向客户端回复数据
String content = "你好,田先森!";
System.out.println(new Date() +":服务端写出数据-> "+content);
ByteBuf byteBuf = getByteBuf(ctx , content);
ctx.channel().writeAndFlush(byteBuf);
}
private static ByteBuf getByteBuf(ChannelHandlerContext cxt , String content) {
// 获取 二进制抽象 ByteBuf
ByteBuf byteBuf = cxt.alloc().buffer();
// 准备数据
byte[] bs = content.getBytes(Charset.forName("UTF-8"));
// 把数据填充到buf中
byteBuf.writeBytes(bs);
return byteBuf;
}
}
现在轮到客户端了 , 客户端读取数据的逻辑和服务端读数据的逻辑一样 , 同样是覆盖channelRead() 方法
class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(new Date()+": 客户端读到数据 ->"+ byteBuf.toString(Charset.forName("UTF-8")));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String content = "你好,奥特曼!";
System.out.println(new Date() + " 客户端写出数据:"+content);
// 1. 获取数据
ByteBuf buffer = getByteBuf(ctx , content);
// 2. 写数据
ctx.channel().writeAndFlush(buffer);
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx , String content ) {
// 获取二进制抽象 ByteBuffer
ByteBuf buf = ctx.alloc().buffer();
// 准备数据
byte[] bs = content.getBytes(Charset.forName("UTF-8"));
// 把数据填充到 buf
buf.writeBytes(bs);
return buf;
}
}
现在 客户端和服务端就实现了双向通信
完整代码:
import java.nio.charset.Charset;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class Test_07_客户端和服务端双向通信 {
public static void main(String[] args) throws Exception {
Test_07_Server.start(8000);
Test_07_Client.start("127.0.0.1", 8000, 5);
}
}
class Test_07_Client {
public static void start(String IP, int port, int maxRetry) {
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 添加业务处理逻辑 可以添加自定义的业务处理逻辑也可以添加 Netty自带的简单通用的处理逻辑
ch.pipeline().addLast(new Test_07_ClientHandler());
}
});
connect(bootstrap, IP, port, maxRetry);
}
private static void connect(Bootstrap bootstrap, String IP, int port, int maxRetry, int... retryIndex) {
bootstrap.connect(IP, port).addListener(future -> {
int[] finalRetryIndex;
if (future.isSuccess()) {
System.out.println("客户端连接【"+IP+":"+port+"】成功");
} else if (maxRetry == 0) {
System.out.println("达到最大重试此时,放弃重试");
} else {
// 初始化 重试计数
if (retryIndex.length == 0) {
finalRetryIndex = new int[] { 0 };
} else {
finalRetryIndex = retryIndex;
}
// 计算时间间隔
int delay = 1 << finalRetryIndex[0];
// 执行重试
System.out.println(new Date() + " 连接失败,剩余重试次数:" + maxRetry + "," + delay + "秒后执行重试");
bootstrap.config().group().schedule(() -> {
connect(bootstrap, IP, port, maxRetry - 1, finalRetryIndex[0] + 1);
}, delay, TimeUnit.SECONDS);
}
});
}
}
class Test_07_Server {
public static void start(int port) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new Test_07_ServerHandler());
}
});
bind(serverBootstrap, port);
}
private static void bind(ServerBootstrap serverBootstrap, int port) {
serverBootstrap
.bind(port)
.addListener(future -> {
if(future.isSuccess()) {
System.out.println("服务端:端口【"+port+"】绑定成功!");
}else {
System.out.println("服务端:端口【"+port+"】绑定失败,尝试绑定【"+(port+1)+"】!");
bind(serverBootstrap, port+1);
}
});
}
}
class Test_07_ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(new Date()+": 客户端读到数据 ->"+ byteBuf.toString(Charset.forName("UTF-8")));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String content = "你好,奥特曼!";
System.out.println(new Date() + " 客户端写出数据:"+content);
// 1. 获取数据
ByteBuf buffer = getByteBuf(ctx , content);
// 2. 写数据
ctx.channel().writeAndFlush(buffer);
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx , String content ) {
// 获取二进制抽象 ByteBuffer
ByteBuf buf = ctx.alloc().buffer();
// 准备数据
byte[] bs = content.getBytes(Charset.forName("UTF-8"));
// 把数据填充到 buf
buf.writeBytes(bs);
return buf;
}
}
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));
// 向客户端回复数据
String content = "你好,田先森!";
System.out.println(new Date() +":服务端写出数据-> "+content);
ByteBuf byteBuf = getByteBuf(ctx , content);
ctx.channel().writeAndFlush(byteBuf);
}
private static ByteBuf getByteBuf(ChannelHandlerContext cxt , String content) {
// 获取 二进制抽象 ByteBuf
ByteBuf byteBuf = cxt.alloc().buffer();
// 准备数据
byte[] bs = content.getBytes(Charset.forName("UTF-8"));
// 把数据填充到buf中
byteBuf.writeBytes(bs);
return byteBuf;
}
}
总结
思考: 如何实现在新连接介入的时候 , 服务端主动向客户端推送消息 , 客户端回复服务端消息?
解答: 在服务器端的逻辑处理其中也实现 channelActive() 在有新的连接接入时 会回调此方法
class Test_07_ServerHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String content = "是不是你连我了?";
System.out.println(new Date() +":服务端写出数据-> "+content);
ByteBuf byteBuf = getByteBuf(ctx , content);
ctx.channel().writeAndFlush(byteBuf);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(new Date() + ": 服务端读到数据->"+ buf.toString(Charset.forName("UTF-8")));
// 向客户端回复数据
String content = "你好,田先森!";
System.out.println(new Date() +":服务端写出数据-> "+content);
ByteBuf byteBuf = getByteBuf(ctx , content);
ctx.channel().writeAndFlush(byteBuf);
}
private static ByteBuf getByteBuf(ChannelHandlerContext cxt , String content) {
// 获取 二进制抽象 ByteBuf
ByteBuf byteBuf = cxt.alloc().buffer();
// 准备数据
byte[] bs = content.getBytes(Charset.forName("UTF-8"));
// 把数据填充到buf中
byteBuf.writeBytes(bs);
return byteBuf;
}
}