首先咱們必須知道Tcp粘包和拆包的,TCP是個「流」協議,所謂流,就是沒有界限的一串數據,TCP底層並不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的實際數據進行包的劃分,一個完整的包可能會被拆分紅多個包進行發送,也有可能把多個小的包封裝成一個大的數據包進行發送。這裏引用Netty官網的User guide裏面的圖進行說明:html
In a stream-based transport such as TCP/IP, received data is stored into a socket receive buffer. Unfortunately, the buffer of a stream-based transport is not a queue of packets but a queue of bytes. It means, even if you sent two messages as two independent packets, an operating system will not treat them as two messages but as just a bunch of bytes. Therefore, there is no guarantee that what you read is exactly what your remote peer wrote. For example, let us assume that the TCP/IP stack of an operating system has received three packets:java
Because of this general property of a stream-based protocol, there's high chance of reading them in the following fragmented form in your application:json
Therefore, a receiving part, regardless it is server-side or client-side, should defrag the received data into one or more meaningful frames that could be easily understood by the application logic. In case of the example above, the received data should be framed like the following:app
那麼通常狀況下咱們是如何解決這種問題的呢?我所知道的有這幾種方案:less
>1.消息定長socket
>2.在包尾增長一個標識,經過這個標誌符進行分割ide
>3.將消息分爲兩部分,也就是消息頭和消息尾,消息頭中寫入要發送數據的總長度,一般是在消息頭的第一個字段使用int值來標識發送數據的長度。性能
這裏以第三種方式爲例,進行對象傳輸。Netty4自己自帶了ObjectDecoder,ObjectEncoder來實現自定義對象的序列化,可是用的是java內置的序列化,因爲java序列化的性能並非很好,因此不少時候咱們須要用其餘序列化方式,常見的有Kryo,Jackson,fastjson,protobuf等。這裏要寫的其實用什麼序列化不是重點,而是咱們怎麼設計咱們的Decoder和Encoder。ui
首先咱們寫一個Encoder,咱們繼承自MessageToByteEncoder<T> ,把對象轉換成byte,繼承這個對象,會要求咱們實現一個encode方法:this
@Override protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception { byte[] body = convertToBytes(msg); //將對象轉換爲byte,僞代碼,具體用什麼進行序列化,大家自行選擇。可使用我上面說的一些 int dataLength = body.length; //讀取消息的長度 out.writeInt(dataLength); //先將消息長度寫入,也就是消息頭 out.writeBytes(body); //消息體中包含咱們要發送的數據 }
那麼當咱們在Decode的時候,該怎麼處理髮送過來的數據呢?這裏咱們繼承ByteToMessageDecoder方法,繼承這個對象,會要求咱們實現一個decode方法
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { if (in.readableBytes() < HEAD_LENGTH) { //這個HEAD_LENGTH是咱們用於表示頭長度的字節數。 因爲上面咱們傳的是一個int類型的值,因此這裏HEAD_LENGTH的值爲4. return; } in.markReaderIndex(); //咱們標記一下當前的readIndex的位置 int dataLength = in.readInt(); // 讀取傳送過來的消息的長度。ByteBuf 的readInt()方法會讓他的readIndex增長4 if (dataLength < 0) { // 咱們讀到的消息體長度爲0,這是不該該出現的狀況,這裏出現這狀況,關閉鏈接。 ctx.close(); } if (in.readableBytes() < dataLength) { //讀到的消息體長度若是小於咱們傳送過來的消息長度,則resetReaderIndex. 這個配合markReaderIndex使用的。把readIndex重置到mark的地方 in.resetReaderIndex(); return; } byte[] body = new byte[dataLength]; // 嗯,這時候,咱們讀到的長度,知足咱們的要求了,把傳送過來的數據,取出來吧~~ in.readBytes(body); // Object o = convertToObject(body); //將byte數據轉化爲咱們須要的對象。僞代碼,用什麼序列化,自行選擇 out.add(o); }
固然咱們Netty也有自帶的LengthFieldBasedFrameDecoder,可是在使用自定義序列化的時候,我以爲仍是本身寫比較方便一點,反正總不是要寫代碼。
我走過的坑:用讀取ByteBuf的使用,必定要注意,其中的方法是否會增長readIndex,否則的話會形成沒法正常讀到咱們想要的數據。