Netty協議之 TCP (一)

前言

本章將會介紹如何使用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章