netty(十五)Netty提升 - 協議設計以及常見協議介紹 一、什麼是協議? 二、協議的必要性 三、常見協議示例

一、什麼是協議?

在計算機網絡與信息通信領域裏,人們經常提及“協議”一詞。互聯網中常用的具有代表性的協議有IP、TCP、HTTP等。

簡單來說,協議就是計算機與計算機之間通過網絡實現通信時事先達成的一種“約定”。這種“約定”使那些由不同廠商的設備、不同的CPU以及不同的操作系統組成的計算機之間,只要遵循相同的協議就能夠實現通信。

二、協議的必要性

通常,我們發送一封電子郵件、訪問某個主頁獲取信息時察覺不到協議的存在,只有在我們重新配置計算機的網絡連接、修改網絡設置時纔有可能涉及協議。

這就好比兩個人使用不同國家的語言說話,怎麼也無法相互理解。也可以比喻成沒有斷句的話,有多種不同的解釋,雙方可能會產生分歧。協議可以分爲很多種,每一種協議都明確地界定了它的行爲規範。兩臺計算機之間必須能夠支持相同的協議,並遵循相同協議進行處理,這樣才能實現相互通信。

TCP/IP 中消息傳輸基於流的方式,沒有邊界。

協議的目的就是劃定消息的邊界,制定通信雙方要共同遵守的通信規則。

三、常見協議示例

3.1 redis協議

比如我們要發送消息給redis,需要遵從其協議。
比如我們要發送set key value 形式的命令,則我們首先要發送整個命令的長度,然後分別發送命令每個位置的長度,如下所示:

set name Tom

則需要按照如下的協議發送:

*3 //命令長度
$3 //set長度
set //命令內容
$4 //key的長度
name //key的內容
$3 //value的長度
Tom //value的內容

我們使用如下代碼連接redis併發送上面的內容:

創建一個客戶端,連接本地redis:

public class RedisProtocol {
    // 回車換行
    static final byte[] LINE = {13, 10};

    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ChannelFuture channelFuture = new Bootstrap().group(worker)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                //連接建立後觸發以下方法,發送內容
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) {
                                    ByteBuf buf = ctx.alloc().buffer();
                                    buf.writeBytes("*3".getBytes());
                                    buf.writeBytes(LINE);
                                    buf.writeBytes("$3".getBytes());
                                    buf.writeBytes(LINE);
                                    buf.writeBytes("set".getBytes());
                                    buf.writeBytes(LINE);
                                    buf.writeBytes("$4".getBytes());
                                    buf.writeBytes(LINE);
                                    buf.writeBytes("name".getBytes());
                                    buf.writeBytes(LINE);
                                    buf.writeBytes("$3".getBytes());
                                    buf.writeBytes(LINE);
                                    buf.writeBytes("Tom".getBytes());
                                    buf.writeBytes(LINE);
                                    ctx.writeAndFlush(buf);
                                }
                                //接收redis返回值
                                @Override
                                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                    ByteBuf byteBuf = (ByteBuf) msg;
                                    System.out.println(byteBuf.toString(StandardCharsets.UTF_8));
                                }
                            });
                        }
                    }).connect("127.0.0.1", 6379);

            channelFuture.sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
        }
    }
}

驗證:

127.0.0.1:0>get name
"Tom"

3.2 http協議

下面我們演示下netty當中解析http請求的示例代碼。因爲http協議我們自己實現太過複雜,所以我們直接使用netty提供的http編解碼器:HttpServerCodec

public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder> implements SourceCodec

如上圖所示,我們發現這個類繼承自CombinedChannelDuplexHandler,同時實現了編碼和解碼的功能。

下面我們提供個一個服務端代碼,通過瀏覽器訪問,看看最終能收到瀏覽器請求內容是什麼樣的:

public class HttpProtocolServer {

    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ChannelFuture channelFuture = new ServerBootstrap()
                    .group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel nioSocketChannel) {
                            nioSocketChannel.pipeline().addLast(new HttpServerCodec());
                            nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                @Override
                                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                    System.out.println(msg);
                                    System.out.println("****************");
                                    System.out.println(msg.getClass());
                                    super.channelRead(ctx, msg);
                                }
                            });
                        }
                    }).bind(8080);
            channelFuture.sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

瀏覽器訪問http://localhost:8080/index.html,得到如下結果:

DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
****************
class io.netty.handler.codec.http.DefaultHttpRequest
EmptyLastHttpContent
****************
class io.netty.handler.codec.http.LastHttpContent$1

分析如上內容,發現得到了很多http請求的參數內容,以及共接收到兩個類型的類,分別是:DefaultHttpRequest 和 LastHttpContent,也就是說,http請求會給我們兩部分的內容,分別是請求和請求體,這裏無論是get還是post,都是如此。

所以,當我們想要處理一個http請求的話,需要對這兩個請求進行判斷去分別處理:

if (msg instanceof HttpRequest){
     // TODO do something...
}else if(msg instanceof HttpContent){
   // TODO do something...
}

也可以使用netty提供的SimpleChannelInboundHandler的針對單一類型的入站處理器去處理請求:

 nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
      @Override
       protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) {

       }
});

處理處理請求,一個正常的http請求還需要返回響應,我們用DefaultFullHttpResponse最爲響應。這個類需要需要指定請求協議的version,以及請求的狀態。

除此之外,我們給請求增加上響應內容:Hello World!

完整代碼如下所示:

public class HttpProtocolServer {

    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ChannelFuture channelFuture = new ServerBootstrap()
                    .group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel nioSocketChannel) {
                            nioSocketChannel.pipeline().addLast(new HttpServerCodec());
                            nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
                                @Override
                                protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest) {
                                    System.out.println(httpRequest.uri());

                                    //創建響應
                                    DefaultFullHttpResponse response =
                                            new DefaultFullHttpResponse(httpRequest.protocolVersion(), HttpResponseStatus.OK);

                                    byte[] bytes = "<h1>Hello World!<h1>".getBytes(StandardCharsets.UTF_8);
                                    //添加響應內容長度,否則會瀏覽器會一直處於加載狀態
                                    response.headers().setInt(CONTENT_LENGTH, bytes.length);
                                    //添加響應內容
                                    response.content().writeBytes(bytes);

                                    //寫入響應
                                    channelHandlerContext.channel().writeAndFlush(response);
                                }
                            });
//
//                            nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//                                @Override
//                                public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//                                    System.out.println(msg);
//                                    System.out.println("****************");
//                                    System.out.println(msg.getClass());
//
//                                    if (msg instanceof HttpRequest) {
//                                        // TODO do something...
//                                    } else if (msg instanceof HttpContent) {
//                                        // TODO do something...
//                                    }
//                                    super.channelRead(ctx, msg);
//                                }
//                            });
                        }
                    }).bind(8080);
            channelFuture.sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

瀏覽器請求http://localhost:8080/index.html,查看結果:

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