netty系列之:一口多用,使用同一埠執行不同協議

flydean發表於2022-01-12

簡介

在之前的文章中,我們介紹了在同一個netty程式中支援多個不同的服務,它的邏輯很簡單,就是在一個主程式中啟動多個子程式,每個子程式通過一個BootStrap來繫結不同的埠,從而達到訪問不同埠就訪問了不同服務的目的。

但是多個埠雖然區分度夠高,但是使用起來還是有諸多不便,那麼有沒有可能只用一個埠來統一不同的協議服務呢?

今天給大家介紹一下在netty中使用同一埠執行不同協議的方法,這種方法叫做port unification。

SocksPortUnificationServerHandler

在講解自定義port unification之前,我們來看下netty自帶的port unification,比如SocksPortUnificationServerHandler。

我們知道SOCKS的主要協議有3中,分別是SOCKS4、SOCKS4a和SOCKS5,他們屬於同一種協議的不同版本,所以肯定不能使用不同的埠,需要在同一個埠中進行版本的判斷。

具體而言,SocksPortUnificationServerHandler繼承自ByteToMessageDecoder,表示是將ByteBuf轉換成為對應的Socks物件。

那他是怎麼區分不同版本的呢?

在decode方法中,傳入了要解碼的ByteBuf in,首先獲得它的readerIndex:

int readerIndex = in.readerIndex();

我們知道SOCKS協議的第一個位元組表示的是版本,所以從in ByteBuf中讀取第一個位元組作為版本號:

byte versionVal = in.getByte(readerIndex);

有了版本號就可以通過不同的版本號進行處理,具體而言,對於SOCKS4a,需要新增Socks4ServerEncoder和Socks4ServerDecoder:

case SOCKS4a:
            logKnownVersion(ctx, version);
            p.addAfter(ctx.name(), null, Socks4ServerEncoder.INSTANCE);
            p.addAfter(ctx.name(), null, new Socks4ServerDecoder());
            break;

對於SOCKS5來說,需要新增Socks5ServerEncoder和Socks5InitialRequestDecoder兩個編碼和解碼器:

case SOCKS5:
            logKnownVersion(ctx, version);
            p.addAfter(ctx.name(), null, socks5encoder);
            p.addAfter(ctx.name(), null, new Socks5InitialRequestDecoder());
            break;

這樣,一個port unification就完成了,其思路就是通過傳入的同一個埠的ByteBuf的首位元組,來判斷對應的SOCKS的版本號,從而針對不同的SOCKS版本進行處理。

自定義PortUnificationServerHandler

在本例中,我們將會建立一個自定義的Port Unification,用來同時接收HTTP請求和gzip請求。

在這之前,我們先看一下兩個協議的magic word,也就是說我們拿到一個ByteBuf,怎麼能夠知道這個是一個HTTP協議,還是傳輸的一個gzip檔案呢?

先看下HTTP協議,這裡我們預設是HTTP1.1,對於HTTP1.1的請求協議,下面是一個例子:

GET / HTTP/1.1
Host: www.flydean.com

HTTP請求的第一個單詞就是HTTP請求的方法名,具體而言有八種方法,分別是:

OPTIONS
返回伺服器針對特定資源所支援的HTTP請求方法。也可以利用向Web伺服器傳送'*'的請求來測試伺服器的功能性。
HEAD
向伺服器索要與GET請求相一致的響應,只不過響應體將不會被返回。這一方法可以在不必傳輸整個響應內容的情況下,就可以獲取包含在響應訊息頭中的元資訊。
GET
向特定的資源發出請求。注意:GET方法不應當被用於產生“副作用”的操作中,例如在Web Application中。其中一個原因是GET可能會被網路蜘蛛等隨意訪問。
POST
向指定資源提交資料進行處理請求(例如提交表單或者上傳檔案)。資料被包含在請求體中。POST請求可能會導致新的資源的建立和/或已有資源的修改。
PUT
向指定資源位置上傳其最新內容。
DELETE
請求伺服器刪除Request-URI所標識的資源。
TRACE
回顯伺服器收到的請求,主要用於測試或診斷。
CONNECT
HTTP/1.1協議中預留給能夠將連線改為管道方式的代理伺服器。

那麼需要幾個位元組來區分這八個方法呢?可以看到一個位元組是不夠的,因為我們有POST和PUT,他們的第一個位元組都是P。所以應該使用2個位元組來作為magic word。

對於gzip協議來說,它也有特殊的格式,其中gzip的前10個位元組是header,其中第一個位元組是0x1f,第二個位元組是0x8b。

這樣我們用兩個位元組也能區分gzip協議。

這樣,我們的handler邏輯就出來了。首先從byteBuf中取出前兩個位元組,然後對其進行判斷,區分出是HTTP請求還是gzip請求:

    private boolean isGzip(int magic1, int magic2) {
        return magic1 == 31 && magic2 == 139;
    }

    private static boolean isHttp(int magic1, int magic2) {
        return
                magic1 == 'G' && magic2 == 'E' || // GET
                        magic1 == 'P' && magic2 == 'O' || // POST
                        magic1 == 'P' && magic2 == 'U' || // PUT
                        magic1 == 'H' && magic2 == 'E' || // HEAD
                        magic1 == 'O' && magic2 == 'P' || // OPTIONS
                        magic1 == 'P' && magic2 == 'A' || // PATCH
                        magic1 == 'D' && magic2 == 'E' || // DELETE
                        magic1 == 'T' && magic2 == 'R' || // TRACE
                        magic1 == 'C' && magic2 == 'O';   // CONNECT
    }

對應的,我們還需要對其新增相應的編碼和解碼器,對於gzip來說,netty提供了ZlibCodecFactory:

p.addLast("gzipEncoder", ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP));
p.addLast("gzipDecoder", ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP));

對於HTTP來說,netty也提供了HttpRequestDecoder和HttpResponseEncoder還有HttpContentCompressor來對HTTP訊息進行編碼解碼和壓縮。

p.addLast("decoder", new HttpRequestDecoder());
p.addLast("encoder", new HttpResponseEncoder());
p.addLast("compressor", new HttpContentCompressor());

總結

新增了編碼和解碼器之後,如果你想自定義一些操作,只需要再新增自定義的對應的訊息handler即可,非常的方便。

本文的例子可以參考:learn-netty4

本文已收錄於 http://www.flydean.com/38-netty-cust-port-unification/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章