前言

本章将会介绍如何使用Netty搭建一个TCP服务器,本系列不会详细介绍Netty本身框架。

TCP 协议

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

Netty 支持

导入依赖包

// gradle 
compile group: 'io.netty', name: 'netty-all', version: '4.1.50.Final'
// maven
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.50.Final</version>
</dependency>

Server 服务端

创建事件线程组

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(6);

bossGroup 里面初始化了一条线程(这里就是NioEventLoop,包含一个NIO SELECTOR,一个队列,一条线程),其作用就是负责处理所有客户端连接的请求,创建新连接(channel),然后注册到workerGroup 的其中一个NioEventLoop。之后该channel的所有读/写操作都由与Channel绑定的NioEventLoop来处理。

workerGroup 如上所说,主要是负责处理读/写操作,这里初始化了6条线程(NioEventLoop)去执行。

Channel的整个生命周期都由同一个NioEventLoop处理。

服务启动器

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 1024)
        .childOption(ChannelOption.SO_KEEPALIVE, true)
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                //添加自定义处理器
                socketChannel.pipeline()
                        .addLast(new StringEncoder())
                        .addLast(new StringDecoder())
                        .addLast(new TcpNettyHandler());
            }
        });
  • serverBootstrap:为服务启动引导器,可以设置处理器,启动端口,绑定IP以及Channel配置等等。
  • group:绑定线程组。
  • NioServerSocketChannel:NIO模式。
  • option(ChannelOption.SO_BACKLOG, 1024):标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。
  • childOption(ChannelOption.SO_KEEPALIVE, true):启用心跳保活机制

这里说明下option和childOption的区别(下面childHandler同理):

  • option主要是作用于boss线程,用于处理新连接。
  • childOption主要作用与worker线程,也就是已创建的channel。

handler处理器

以上都是对Channel的配置,到这里,才是处理数据的关键。

编解码器

上面有提到,TCP是基于字节流的传输协议,因此我们必须要用到编解码器来处理字节流。本文我们当其是字符串处理。
Netty提供了很多协议编解码器,无需我们自己去编写。

StringDecoder:字符串解码器,它会把输入的字节流解码成字符串。
StringEncoder:字符串编码器,它会把输出的String编码为字节流。

源码可见,它们其实也是属于handler。

TcpServerNettyHandler
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * Created by chenws on 2020/3/24.
 */
@ChannelHandler.Sharable
public class TcpServerNettyHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg);
        //把消息写到缓冲区
        ctx.write("I get the message " + msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        //刷新缓冲区,把消息发出去
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //异常处理,如果该handler不处理,将会传递到下一个handler
        cause.printStackTrace();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush("connect success!");
    }
}

该handler继承了SimpleChannelInboundHandler,它是继承了ChannelInboundHandlerAdapter,其实就是帮我们转化了数据的类型,体现在泛型上。这里我们指定了< String >。

  • channelRead0:当channel接收到数据时,该方法就会被执行。
  • channelReadComplete:本次数据传输接收完后,该方法就会被执行。
  • exceptionCaught:在接收数据过程中发生异常,该方法会被执行,如果不实现,则异常会传递到下一个handler。
  • channelActive:channel建立成功后,该方法就会被执行。

那这里有个问题,channelReadComplete什么时候触发呢?换种说法也就是Netty是怎么知道本次传输已经读完呢?有两个条件,符合其一即会执行:

  • read 返回0字节
  • read 传递给它的缓冲区有1024个字节要填充,但最终少于1024个字节被填充。
Bind
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();

最后一步就是绑定端口,默认绑定本机的IP 地址。

至此,TCP Netty的服务端已经完成,下面来看看客户端。

Client 客户端

大部分代码的逻辑都跟server服务端差不多,这也是netty的强大之处,下面我就直接贴

EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
try{
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(eventLoopGroup)
	    .channel(NioServerSocketChannel.class)
	    .option(ChannelOption.SO_BACKLOG, 1024)
	    .handler(new ChannelInitializer<SocketChannel>() {
	        @Override
	        protected void initChannel(SocketChannel socketChannel) throws Exception {
	            socketChannel.pipeline()
	                    .addLast(new StringEncoder())
	                    .addLast(new StringDecoder())
	                    .addLast(new TcpClientNettyHandler());
	        }
	    });
    ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    eventLoopGroup.shutdownGracefully();
}
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TcpClientNettyHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        log.info("接受来自服务器的消息:{}", msg);
        ctx.write(msg);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //刷出通道
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}

主要区别是不再用ServerBootstrap,改用Bootstrap来启动客户端。代码没有什么难度,这里就不展开来说了。

总结

到此为止,Netty结合TCP已经介绍完,总的来说,Netty已经帮我们做了很多事情,如果对Netty感兴趣的,可以随时关注我的博客。

源码可以见:Netty-Learn