Netty-1-服务端启动详解

上文中通过一个简单的例子,对netty有了一个大致的了解,本文来详细看一下Netty服务端的启动.

NettyServer解析

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
}
});

serverBootstrap.bind(8000);
}
}

和上篇文章中的NettyServer基本是一样的,详细看一下这段代码

首先创建了两个NioEventLoopGroup对象,这两个对象可以看做是传统IO编程模型的两大线程组,bossGroup表示监听端口,accept新连接的线程组,workerGroup表示处理每一条连接的数据读写的线程组,不理解的同学可以看一下上一小节.

用生活中的例子来讲就是,一个工厂要运作,必然要有一个老板负责从外面接活,然后有很多员工,负责具体干活,老板就是bossGroup,员工们就是workerGroup,bossGroup接收完连接,扔给workerGroup去处理.

接下来,我们创建了一个引导类ServerBootstrap,这个类将引导我们进行服务端的启动工作,直接new出来开搞

我们通过.group(bossGroup, workerGroup)给引导类配置两大线程组,这个引导类的线程模型也就定型了

然后,我们指定我们服务端的IO模型为NIO,我们通过.channel(NioServerSocketChannel.class)来指定 IO 模型,当然,这里也有其他的选择,如果你想指定 IO 模型为BIO,那么这里配置上OioServerSocketChannel.class类型即可,当然通常我们也不会这么做,因为Netty的优势就在于NIO.

接着,我们调用childHandler()方法,给这个引导类创建一个ChannelInitializer,这里主要就是定义后续每条连接的数据读写,业务处理逻辑,不理解没关系,在后面我们会详细分析.ChannelInitializer这个类中,我们注意到有一个泛型参数NioSocketChannel,这个类呢,就是 Netty 对 NIO 类型的连接的抽象,而我们前面NioServerSocketChannel也是对 NIO 类型的连接的抽象,NioServerSocketChannelNioSocketChannel的概念可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上

总结

启动一个Netty服务端,必须要指定三类属性,分别是线程模型、IO 模型、连接读写处理逻辑,有了这三者,之后在调用bind(8000),我们就可以在本地绑定一个 8000 端口启动起来

image
图片来源,侵删

自动绑定递增端口

首先看下代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void bind(final ServerBootstrap serverBootstrap, final int port) {
serverBootstrap.bind(port)
.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()) {
log.info("端口:{}>绑定成功", port);
} else {
log.info("端口:{}>绑定失败", port);
bind(serverBootstrap, port + 1);
}
}
});
}

就是把上面绑定端口的代码抽出来,单独的一个方法,上面的代码中serverBootstrap.bind(8000);是一个异步方法,调用之后立即返回一个ChannelFuture,然后我们给这个ChannelFuture加了一个监听器GenericFutureListener,然后里面重写了operationComplete()方法,里面判断端口是否绑定成功,绑定失败的话,就又调用自身,把端口加1再去尝试绑定

比如说我传的port参数是1000,然后端口1000绑定失败的话,就又去绑定1001,1002…直到绑定成功

服务端启动时的一些其他功能

NettyServer代码如下:

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
77
78
/**
* @author 周泽
* @date Create in 14:14 2019/5/29
* @Description netty服务端
*/
@Slf4j
public class NettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();

NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();

serverBootstrap
.group(boos, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
log.info("取出childAttr属性:{}", ch.attr(CommonConfig.CLIENT_KEY).get());

ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info(msg);
}
});
}
});


serverBootstrap
// handler() 服务端启动过程中的一些逻辑处理,通常情况用不到这个方法
.handler(new ChannelInitializer<ServerSocketChannel>() {
@Override
protected void initChannel(ServerSocketChannel serverSocketChannel) throws Exception {
log.info("取出attr属性:{}", serverSocketChannel.attr(CommonConfig.SERVER_NAME_KEY).get());
log.info("服务端启动中...............");
}
})
// attr() 给服务端的channel即NioServerSocketChannel指定一些自定义属性,通过channel.attr()取出该属性,给NioServerSocketChannel维护一个map
.attr(CommonConfig.SERVER_NAME_KEY, CommonConfig.SERVER_NAME_VALUE)

// childAttr() 给每一条连接指定自定义属性,通过channel.attr()取出该属性,对应的是childHandler
.childAttr(CommonConfig.CLIENT_KEY, CommonConfig.CLIENT_VALUE)

// childOption() 可以给每条连接设置一些TCP底层相关的属性
// ChannelOption.SO_KEEPALIVE 表示是否开启TCP底层心跳机制,true为开启
.childOption(ChannelOption.SO_KEEPALIVE, true)
// ChannelOption.TCP_NODELAY 表示是否开启Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启.
.childOption(ChannelOption.TCP_NODELAY, true)
// ChannelOption.SO_REUSEADDR 表示端口释放后立即就可以被再次使用,因为一般来说,一个端口释放后会等待两分钟之后才能再被使用
.childOption(ChannelOption.SO_REUSEADDR, true)
// option() 给服务端channel设置一些属性
// ChannelOption.SO_BACKLOG 表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,适当调大该参数
.option(ChannelOption.SO_BACKLOG, 1024);


// 绑定端口
bind(serverBootstrap, CommonConfig.NETTY_PORT);
}

public static void bind(final ServerBootstrap serverBootstrap, final int port) {
serverBootstrap.bind(port)
.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()) {
log.info("端口:{}>绑定成功", port);
} else {
log.info("端口:{}>绑定失败", port);
bind(serverBootstrap, port + 1);
}
}
});
}
}

额外的一些方法及其功能都在代码的注释里了.从方法名上来看比如说上面的attr(),childAttr(),option(),childOption()…等等这些,可以看出一类是去设置服务端的,还有一类是针对连接的设置

上面的代码中还用到了一个类CommonConfig,用来放一些常量,如下:

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
public class CommonConfig {

/**
* 服务端ip
*/
public static final String NETTY_HOST = "127.0.0.1";

/**
* 绑定端口
*/
public static final int NETTY_PORT = 8000;

/**
* 尝试重新连接次数
*/
public static final int MAX_RETRY = 5;

/**
* serverNameKey
*/
public static final AttributeKey<Object> SERVER_NAME_KEY = AttributeKey.newInstance("serverName");

/**
* saerverNameValue
*/
public static final String SERVER_NAME_VALUE = "nettyServer";

/**
* clientKey
*/
public static final AttributeKey<Object> CLIENT_KEY = AttributeKey.newInstance("clientKey");

/**
* clientValue
*/
public static final String CLIENT_VALUE = "clientValue";

}