本文主要内容:
设置开发环境
编写Echo服务器和客户端
构建并测试应用程序
1.设置开发环境
准备好JDK和Maven
2.Netty客户端/服务器概览
在客户端建立一个连接之后,它会向服务器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。
3.编写Echo服务器
首先明确的一点是,所有的Netty服务器都需要以下两部分:
- 至少一个ChannelHandler:用户实现服务器对从客户端接收的数据的处理,即业务逻辑。
- 引导:配置服务器的启动代码,如将服务器绑定到它需要监听连接请求的端口上。
3.1ChannelHandler和业务逻辑
ChannelHandler是一个父接口,它的实现负责接收并响应事件通知。
这里我们的Echo服务器需要响应传入的消息,所以需要实现ChannelInboundHandler接口,用来定义响应入站事件的方法,这里我们继承ChannelInboundHandlerAdapter类。
主要有如下方法可以调用:
channelRead(): 对于每个传入的消息都要调用
channelReadComplete():表明了本次从 Socket 读了数据,但是否是完整的数据它其实并不知道
exceptionCaught():在读取操作期间 ,有异常抛出时会调用
/** Sharable标识一个ChannelHandler可以被多个Channel安全地共享 **/
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf)msg;
System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));
//将接收到的消息写给发送者
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//将消息冲刷到客户端,并且关闭该Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//打印异常栈跟踪
cause.printStackTrace();
//关闭该Channel
ctx.close();
}
}
3.2 引导服务器
主要内容如下:
- 绑定服务器将在其上监听并接收传入连接请求的端口;
- 配置Channel,以将有关的入站消息通知给EchoServerHandler实例
编写EchoServer类:
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
//设置端口值不正确
if(args.length != 1){
System.err.print("Usage: " + EchoServer.class.getSimpleName() + "<port>");
}
int port = Integer.parseInt(args[0]);
//调用服务器的start()方法
new EchoServer(port).start();
}
private void start() throws InterruptedException {
final EchoServerHandler serverHandler = new EchoServerHandler();
//创建EventLoopGroup,因为我们使用的是NIO传输,所以要指定NioEventLoopGroup来接收和处理新的连接
NioEventLoopGroup group = new NioEventLoopGroup();
try {
//创建ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
b.group(group)
//指定所使用的NIO传输Channel
.channel(NioServerSocketChannel.class)
//使用指定的端口设置套接字地址,服务器将绑定到这个地址以监听新的连接请求
.localAddress(new InetSocketAddress(port))
//当一个新的连接被接收时,一个新的子Channel将会被创建,ChannelInitializer会把EchoServerHandler的实例添加到Channel的ChannelPipeline中,这个ChannelHandler会接收入站消息的通知。
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//由于EchoServerHandler被标注位@Shareable,所以我们使用的是同一个EchoServerHandler
socketChannel.pipeline().addLast(serverHandler);
}
});
//异步的绑定服务器,调用sync()方法阻塞等待直到绑定完成
ChannelFuture f = b.bind().sync();
//获取Channel的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
}finally {
//关闭EventLoopGroup,并且释放所有的资源
group.shutdownGracefully().sync();
}
}
}
主要步骤:
- EchoServerHandler实现业务逻辑;
- main()方法引导了服务器
在引导过程中的步骤:
使用一个EchoServerHandler实例来初始化每一个新的Channel
创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接收新连接以及读/写数据;
创建一个ServerBootstrap的实例以引导和绑定服务器;
指定服务器绑定的本地InetSocketAddress
调用ServerBootstrap.bind()方法来绑定服务器
4. 编写Echo客户端
Echo客户端主要任务:
- 连接到服务端
- 发送一个或多个消息;
- 对于每个消息,等待并接收从服务器发回的消息;
- 关闭连接
客户端所涉及到的两个主要代码部分也是业务逻辑和引导
4.1 通过ChannelHandler实现客户端逻辑
客户端也拥有一个用来处理数据的ChannelInboundHandler,这里我们扩展SimpleChannelInboundHandler类来处理所有必须的任务,重写以下方法:
channelActive():在到服务器的连接已经建立之后将被调用
channelRead0():当从服务器接收到一条消息时被调用
exceptionCaught():在处理过程中引发异常时被调用
代码如下:
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
/** 连接服务器后调用该方法 **/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
}
/** 从服务器接收到消息后调用该方法 **/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("Client received: "+ byteBuf.toString(CharsetUtil.UTF_8));
}
/** 发生异常时,记录错误并关闭Channel **/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
注意: 从服务器发送的消息可能会被分块接收,channelRead0()方法因此有可能被调用多次。
还有一点值得注意的是Echo 服务端使用的ChannelHandler是 ChannelInboundHandlerAdapter,而 Echo 客户端使用的却是 SimpleChannelInboundHandler,其实它们是继承关系。
既然是继承关系,也就是说,”你有的我也有,你没有的我还有。” 那么 SimpleChannelInboundHandler 里面肯定重写或者新增了 ChannelInboundHandlerAdapter 里面的方法功能 - channelRead0 和 channelRead()。
protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
至于为什么会这样设计,原因是在客户端,当 channelRead0() 方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。而在服务端,你仍然需要将传入消息回送给发送者,而 write() 操作是异步的,直到 channelRead() 方法返回后可能仍然没有完成。为此,EchoServerHandler扩展了 ChannelInboundHandlerAdapter ,其在这个时间点上不会释放消息。
4.2 引导客户端
客户端使用主机和端口号来连接远程地址
代码如下:
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
if(args.length != 2){
System.err.println("Usage: "+ EchoClient.class.getSimpleName()+"<host> <port>");
return;
}
String host = args[0];
int port = Integer.parseInt(args[1]);
new EchoClient(host,port).start();
}
private void start() throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host,port))
//在创建Channel时向ChannelPipeline中添加一个EchoClientHandler实例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture f = b.connect().sync();
f.channel().closeFuture().sync();
}finally {
group.shutdownGracefully().sync();
}
}
}
5. 构建和运行Echo服务器和客户端
目录如下:
注意要在pom.xml文件中引入netty依赖以及编译maven模板需要的插件。
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<!--指定main文件,不指定会报错,如果是client就要变成EchoClient-->
<mainClass>EchoServer</mainClass>
</configuration>
</plugin>
</plugins>
</build>
先执行mvn clean package
来清除指定的包,然后cd server
,执行exec:java -Dexec.args="1"
,接下来点击右下角的加号新建一个终端,然后cd client
,执行exec:java -Dexec.args="0 1"
,即可看到效果。一定一定要先开启服务器然后再开客户端,否则会报错。
效果:
补充一个遇到的坑:
如果在执行mvn exec:java -Dexec.args="0 1"
的时候出现上面错误,首先去自己的maven的目录下查看配置文件setting.xml
<localRepository>D:\Software\apache-maven-3.6.1\repository</localRepository>
看路径是否正确,其次检查IDEA中的MAVEN配置。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!