关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
上一节分享Websocket独立部署的一个设计思路,我们今天接着聊一下基于Netty的TCP协议的Socket服务端如何搭建。这个对于熟悉的人可能很简单,但是对于新手或者不常用的开发者来说,可能一头雾水。
小编在初次使用Socket的时候,都是度娘一大堆,然后抄抄抄,完成自己的任务。至于为什么这么做,完全不知道。这一节将自己的理解分享并记录下来,以备不时之需。
02 服务端案例
2.1 代码展示
java
public void start() {
// 创建线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup(5);
try {
// 服务端类
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 添加组
serverBootstrap.group(bossGroup, workGroup);
// 设置NioServerSocketChannel通道
serverBootstrap.channel(NioServerSocketChannel.class);
// 连接队列大小
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
// 保持连接
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 设置处理器链,依次执行
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(2048, Unpooled.copiedBuffer("_".getBytes())));
pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
// 自定义的handler,处理业务逻辑
pipeline.addLast(new BusinessHandler<>());
}
});
// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = serverBootstrap.bind(9091).sync();
log.info("Server started and listen on:{}",channelFuture.channel().localAddress());
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.error("信息异常:", e);
}finally {
bossGroup.close();
workGroup.close();
}
}
2.2 创建线程组
java
// 创建线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup(5);
EventLoopGroup就是一个线程池。在Netty的Socket中需要两个不同的线程池,分别处理不同的任务。其中bossGroup用来接收客户端连接,通常设置1个线程,而workGroup用来处理I/O操作和业务逻辑,可以根据CPU的核心数指定。
2.3 创建服务端
java
// 服务端类
ServerBootstrap bootstrap = new ServerBootstrap();
// 添加组
serverBootstrap.group(bossGroup, workGroup);
// 设置NioServerSocketChannel通道
serverBootstrap.channel(NioServerSocketChannel.class);
// 连接队列大小
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
// 保持连接
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
服务端引导类创建完成之后,需要设置参数:
- group():添加线程池组,第一个参数为
bossGroup,第二个是workGroup - channel():设置
NioServerSocketChannel通道 - option():配置服务端监听的
ServerSocketChannel - childOption():配置客户端连接的
SocketChannel
所以这里的ChannelOption.SO_BACKLOG只能设置在option中,用来指定服务端接收的任务最大队列。ChannelOption.SO_KEEPALIVE用来TCP保活机制,检测死连接,是针对客户端的连接,所以需要设置在childOption上。
ChannelOption.SO_KEEPALIVE也可以不同设置,采用心跳机制来保活。其使用有一定的局限性,通常都会通过心跳机制来代替。
2.4 设置处理链
java
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 设置处理器链,依次执行
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("...");
pipeline.addLast("...");
}
});
采用责任链模式,每个处理器处理特定任务,依次执行。里面具体的Handler单独说明。
2.5 绑定端口,同步阻塞
java
// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = serverBootstrap.bind(9091).sync();
// 对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
当前服务端绑定一个端口,客户端就可以通过当前端口连接,并同步阻塞,等待端口绑定成功。最后同步阻塞等待通过关闭。
03 消息处理
消息的处理是接受和推送消息重要部分。Netty框架提供了丰富的处理器,我们可以选择适合自己的处理器。处理器都是实现io.netty.channel.ChannelInboundHandler接口

3.1 框架自带编解码器
Netty框架下的Socket数据传输,默认都是ByteBuf(字节缓冲)。我们使用的时候自然想通过常用的字符串传输,而Netty自然帮我们提供了字符串相关的编解码处理器。
io.netty.handler.codec.string.StringDecoderio.netty.handler.codec.string.StringEncoder
通过源码我们可以看到注释:

StringDecoder是将ByteBuf转成字符串的解码器,但是在处理之前必须使用ByteToMessageDecoder先解码,子类包括:
io.netty.handler.codec.DelimiterBasedFrameDecoder:分隔符分割io.netty.handler.codec.FixedLengthFrameDecoder:固定长度分割io.netty.handler.codec.LengthFieldBasedFrameDecoder:按照字段长度分割io.netty.handler.codec.LineBasedFrameDecoder:按行分割
这几种方式都是有效防止拆包、粘包的方法。
按照注释的案例,我们就可以配置。而StringEncoder是用来发送消息的解码器,用来将字符串转成ByteBuf。
我们这里采用分隔符的方式分割:
java
pipeline.addLast(new DelimiterBasedFrameDecoder(2048, Unpooled.copiedBuffer("_".getBytes())));
pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
而其中DelimiterBasedFrameDecoder的maxFrameLength参数用来控制接收消息的最大字节大小,超过就会异常。
3.2 自定义业务处理器
自定义业务处理器是用来处理客户端连接以及消息的。
java
@Slf4j
public class BusinessHandler extends SimpleChannelInboundHandler {
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 建立客户端
Channel channel = ctx.channel();
log.info("Socket客户端建立连接:channelId={}", channel.id());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 断开链接
Channel channel = ctx.channel();
log.info("Socket客户端断开连接:channelId={}", channel.id());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接受消息
Channel channel = ctx.channel();
log.info("Socket收到来自通道channelId[{}]发送的消息:{}", channel.id(), msg);
// 通过WebSocket将方法发送给客户端
channel.writeAndFlush(msg + "789_000");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("异常:", cause);
}
}
handlerAdded()
客户端建立连接之后会触发该方法。可以通过ctx.channel()获取来连接的通道(客户端)。连接的通常可以通过io.netty.channel.group.ChannelGroup收集。
java
ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE)
channelGroup.add(channel);
向客户端发送消息时,可以通过channelGroup.writeAndFlush()统一给客户端发送消息。
handlerRemoved()
客户端断开连接的时触发,可以通过channelGroup.remove(channel)移除已经关闭的客户端通道
channelRead0()
接收客户端消息的重要方法,通过channel.writeAndFlush()可以直接向客户端发送消息
exceptionCaught()
处理异常的方法
3.3 客户端测试

从图上可以看出,介绍的方法都被触发了。

从图可以看出客户端的也接收到服务端的消息了。
注意
客户端发送的消息:foo test..._
服务端发送的消息:foo test...789_000
客户端接受的消息:foo test...789
客户端和服务端接收到的消息都通过_截断的
04 小结
简单的服务端搭建就已经好了,但是实际应用的时候,还需要考虑心跳机制、以及无效客户端的清理等。TCP协议服务端的介绍就到这里,客户端我们下一期介绍。