初始Netty —— 实现简单的C/S通信

写在前面:
Netty是Java的网络编程框架,既然是框架的学习,不免会碰到很多分支的知识和不熟悉的名词。这就需要不断的做“下潜”,耐心搜索,不求甚解,等到大致熟悉之后再去逐一深究。因此有些概念作者也不能做出详细解释,请参考贴出的相关文章或自行搜索以解决疑惑。

什么是Netty

网上很多文章都有作解释。以作者的使用体验来说,Netty是封装了 Java socket nio 来进行网络编程的工具。说到网络编程,大二软工的软件工程实训就有这个小课题,当时作者是用Java socket io来写,还没用到nio呢,就是参照网上的例子手动模拟通信过程,自己用最简单的 阻塞I/O 的模式写了一个Thread类来处理所有不同种类的请求,由于需求简单,尚能完成。 想要模拟效果更自然一点就要用 非阻塞I/O 模式,而nio就是用来写非阻塞I/O的api。但是nio的编写对java程序员是有比较高的要求的。Netty就可以简化这一系列操作。

预备知识

贴几个比较靠谱的博客,不求甚解,大致了解一下就好。
关于NIO:
https://www.jianshu.com/p/3cec590a122f (推荐,也包括I/O模型)
https://my.oschina.net/andylucc/blog/614295
关于I/O模型:
https://segmentfault.com/a/1190000003063859

开发环境

java JDK1.8 + IDEA + maven + Netty 4.1.6
maven依赖:

1
2
3
4
5
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>

实现功能

C/S通信:C是客户端,S是服务端。在IDEA控制台开启服务端接收客户端的信息String, 并返回一个“hi!”+String,客户端收到服务端的信息后在控制台上输出。

代码讲解

分为服务端和客户端两部分,各自又有一个处理连接逻辑的代码

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;


public class EchoServer {
private final int port; //1.设置服务端端口

public EchoServer(int port) {
this.port = port;
}

public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup(); //2.创建 EventLoopGroup
try{
/*
* 客户端的是Bootstrap,服务端的则是 ServerBootstrap。
**/
ServerBootstrap sbs = new ServerBootstrap(); //3.创建 ServerBootstrap
sbs.group(group)
.channel(NioServerSocketChannel.class) //4.指定使用 NIO 的传输 Channel
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline ph = ch.pipeline();
/*
* Netty中的编码/解码器,通过他你能完成字节与pojo、pojo与pojo的相互转换,
* 从而达到自定义协议的目的。
* 下面是以("\n")为结尾分割的 解码器
* */
ph.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
.addLast("decoder", new StringDecoder())
.addLast("encoder", new StringEncoder()) //解码和编码,应和客户端一致
.addLast("handler", new EchoServerHandler()); //5.添加 EchoServerHandler 到 Channel 的 ChannelPipeline

}
});

ChannelFuture cf = sbs.bind(this.port).sync(); //6.设置socket地址使用所选的端口 并且 绑定的服务器,sync 等待服务器关闭

System.out.println("服务端启动成功...");

cf.channel().closeFuture().sync(); //7.关闭 channel 和 块,直到它被关闭

}finally {
group.shutdownGracefully(); //8.关闭 EventLoopGroup,释放所有资源
}
}

public static void main(String[] args) throws Exception{
new EchoServer(65535).start();
}
}

注意这一段代码:

这是关键所在,其余代码基本上是套路代码,按部就班写就可以。
关键在于此处出现了:

  • 用ChannelPipeline引用了SocketChannel的pipeline,原因在于ChannelPipeline是用于存放ChannelHandler的容器,而接下来的解码编码操作和自定义的逻辑处理类都要涉及到ChannelHandler的子类
    它们之间的关系可以用下图表示:

  • Encoder(编码器)和Decoder(解码器),属于Codec框架的内容,大致意思是:此处描述了服务端和客户端之间传输了什么类型的数据,这里要传输String就用到了StringDecoder/Encode 当然也可以传输其他类型的数据,详情参考这篇博客:https://www.jianshu.com/p/fd815bd437cd

  • 注释5.处的EchoServerHandler是自定义的类,可以看作是一种“规则”,规定了服务端以什么方式处理客户端发来的数据。

服务端处理连接的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.net.InetAddress;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class EchoServerHandler extends SimpleChannelInboundHandler<String> {
/*
* 收到消息时,返回信息
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg)
throws Exception {
System.out.println("服务端接受的消息 : " + msg); // 收到消息直接打印输出
if("quit".equals(msg)){ //服务端断开的条件
ctx.close();
}
ctx.writeAndFlush("hi! "+msg+"\n"); // 返回客户端消息
}
/*
* 建立连接时,返回消息
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("连接的客户端地址:" + ctx.channel().remoteAddress());
ctx.writeAndFlush("客户端"+ InetAddress.getLocalHost().getHostName() + "成功与服务端建立连接! \n");
super.channelActive(ctx);
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;

public class EchoClient {
private final String host; //ip地址
private final int port; //端口

public EchoClient() {
this(0);
}

public EchoClient(int port) {
this("localhost", port);
}

public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}

public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup(); //1.创建 EventLoopGroup
try{
/**
* Netty创建全部都是实现自AbstractBootstrap。
* 客户端的是Bootstrap,服务端的则是 ServerBootstrap。
**/
Bootstrap bs = new Bootstrap(); //2.创建 Bootstrap

System.out.println("客户端成功启动...");

bs.group(group) //3.指定 NioEventLoopGroup 来处理客户端事件。
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() { //4.指定使用 NIO 的传输 Channel
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline ph = ch.pipeline();
ph.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()))
.addLast("decoder", new StringDecoder())
.addLast("encoder", new StringEncoder()) // 解码和编码,应和服务端一致
.addLast("handler", new EchoClientHandler()); //5.当建立一个连接和一个新的通道时,创建添加到 EchoClientHandler 实例 到 channel pipeline
}
});
Channel ch = bs.connect(this.host, this.port).sync().channel(); //6.设置服务器的ip和端口,并且连接到远程; 等待连接完成

Scanner in=new Scanner(System.in);

while(true){
System.out.println("请输入要发送的信息:");
String str=in.next();
//连接后发送数据
ch.writeAndFlush(str+ "\r\n");
System.out.println("客户端发送数据:"+str);
if (str.equals("quit"))break;
}

System.exit(0);

}finally {
group.shutdownGracefully(); //8.关闭线程池和释放所有资源
}
}

public static void main(String[] args) throws Exception{
new EchoClient("127.0.0.1", 65535).start();
}
}

客户端和服务端代码样式基本一致,有几个关键点都已注释

此处以不断向客户端发送信息,输入“quit”终止连接。

运行效果

先运行服务端再运行客户端
服务端:

客户端1:

客户端2:

参考文章

0%