1. 程式人生 > >【Java TCP/IP Socket程式設計】----傳送和接收資料----訊息成幀與解析

【Java TCP/IP Socket程式設計】----傳送和接收資料----訊息成幀與解析

目錄

 

簡介

成幀與解析

成幀技術案例


簡介

在程式中使用套接字向其他程式提供資訊或者使用其他程式提供的資訊,這就需要任何需要交換資訊的程式間在資訊編碼方式上達成共識(包含了資訊交換的形式和意義),稱為協議,用來實現特定的應用程式的協議叫應用程式協議。大部分應用程式協議是根據欄位序列組成的離散資訊定義的,而每個欄位有包含了一段位序列編碼的特定資訊。應用程式協議中明確定義了資訊傳送者應該怎麼排列和解釋這些資訊,同時定義接收者應該怎樣解析。TCP/IP協議中資訊必須在塊(8位的倍數)中傳送和接收,所以TCP/IP協議傳輸的資訊是位元組序列或者陣列。

傳輸資訊時,對於一個TCP套接字而言,通過套接字將位元組資訊寫到一個與Socket關聯的OutputStream例項中。而UDP套接字會將資訊封裝到DatagramPacket例項中,然後通過DatagramSocket傳送。可見傳輸的資訊的資料是位元組和位元組陣列。Java是強型別語言,需要將其他資料型別轉換成位元組陣列。為了完整傳輸位元組資訊,需要傳送端和接收端達成一些共識:

1)傳輸的每個整數字節大小(size)。如Java中int資料型別由32位表示,所以使用4個位元組傳輸int型的變數或者常量。

2)對多位元組的整數,使用的是big-endian順序還是little-endian順序。big-endian從高位到低位傳送,而little-endian是低位到高位傳送。

3)傳送的數值是有符號的還是無符號的。Java中4種基本整型都是有符號的,編碼和解碼無符號數需要掩碼。掩碼是整數值,其中一位或者多位是1,其他為0,與掩碼進行“位操作”清空特定一位或者得到特定的一位。

4)明確符號與整數的對映方式(即編碼方式),才能使用文字資訊通訊。傳送者如果和接收者採用不同的字符集,可能會造成亂碼的情況,Java中傳輸文字資訊-->位元組/位元組陣列,可以呼叫getBytes()方法,並可以將字符集名稱傳遞getBytes()方法。

成幀與解析

傳送端按照規定格式傳輸資料,接收端必須將接收到的位元組序列還原成原始資訊,應用程式協議通常處理的是由一組欄位組成的離散資訊,無論資訊以文字、多位元組二進位制或者兩者結合傳輸,必須指定訊息的接收者如何確定何時訊息已完整接收。成幀技術解決了接收端如何定位訊息的首位位置問題。

1.使用UDP套接字傳送資料,訊息負載到DatagramPacket中傳送,由於DatagramPacket負載的資料有一個確定的長度,接收者能夠準確地知道訊息的結束位置。

2.對於TCP套接字,沒有訊息邊界的概念。如果每個訊息有固定數量欄位組成,所有欄位又有固定的長度,訊息的長度就能確定,接收端可將訊息長度對應的位元組數讀取到位元組緩衝區中。對於可變訊息長度,需要確定訊息以及欄位的邊界問題。

主要有兩個技術使接收者能夠準確地找到訊息的結束位置。

      1)基於定界符:訊息的結束由一個唯一的標記指出,即傳送者在傳輸完資料後顯式新增的一個特殊位元組序列。但要求訊息本身不能存在定界符。

      2)顯式長度:在變長欄位或訊息前附加一個固定大小的欄位。用來指示該欄位或訊息包含了多少位元組。

需要注意的一點是:基於定界符使用在TCP連線上傳輸的最後一個訊息上,傳送完這個訊息後,傳送者就簡單地關閉(使用shutdownOutput()或者close()方法)傳送端的TCP連線,接收者讀取萬這條訊息的最後一個位元組後,將接收到一個流結束標記(即read()方法返回-1),該編輯指示出已經讀取到達了訊息的末尾。

成幀技術案例

下面的案例分別使用兩中成幀技術來發送訊息,介面Framer定義了兩個方法,frameMsg()方法定義使用成幀技術將訊息新增到流中,而nextMsg()通過判斷成幀技術判斷訊息末尾,讀取下一條訊息。

public interface Framer {
  void frameMsg(byte[] message, OutputStream out) throws Exception;
  byte[] nextMsg() throws IOException;
}

1.基於定界符實現了成幀技術的類DelimerFramer。

      下面的類基於定界符的成幀方法,定界符為“換行”符('\n').frameMsg()方法並沒有實現填充,當成幀的位元組序列中包含了定界符時,簡單地丟擲了異常。nextMsg()方法掃描流,知道讀取到定界符,並返回定界符前面的所有的字元。如果流為空則返回null,如果累積了一個訊息的不少字元,但知道流結束也沒有知道定界符,程式將丟擲一個異常來只是成幀錯誤。

public class DelimerFramer implements Framer {

  private InputStream in;
  private static final byte DELIMITER = '\n';

  public DelimerFramer(InputStream in) {
    this.in = in;
  }

  @Override
  // 基於定界符的成幀技術
  public void frameMsg(byte[] message, OutputStream out) throws IOException {
    for (byte b : message) {
      // 訊息本身不能包含有定界符
      if (b == DELIMITER) {
        throw new IOException("message contains delimiter");
      }
    }
    // 寫入訊息以及定界符
    out.write(message);
    out.write(DELIMITER);
    out.flush();
  }

  @Override
  public byte[] nextMsg() throws IOException {
    ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
    int nextByte;
    while ((nextByte = in.read()) != DELIMITER) {
      // 流已經結束,沒有出現定界符
      if (nextByte == -1) {
        if (messageBuffer.size() == 0) {
          return null;
        } else {
          // 讀取到的訊息不為空,則丟擲無定界符非空訊息異常。
          throw new EOFException("Non-empty message without delimiter");
        }
      }
      messageBuffer.write(nextByte);
    }
    return messageBuffer.toByteArray();
  }
}

2.基於長度的成幀技術的LengthFramer類。

      基於長度的成幀方法,適用於長度小於65535位元組的訊息,傳送者首先給出指定訊息的長度小於65535位元組的訊息,傳送者首先給出指定訊息的長度,並將長資訊以big-endian順序存入兩個位元組的整數中,再將這兩個位元組放在完整的訊息內容前,連同訊息一起寫入輸出流。在接收端,我們使用DataInputStream讀取整型的長度資訊,readFully()方法將阻塞等待,直到給定陣列完全填滿。這種成幀方法,傳送者不需要檢查成幀的訊息內容,只需要檢查訊息的長度是否超出了限制。

public class LengthFramer implements Framer {

  private static final int MAXMESSSAGELENGTH = 65535;
  private static final int BYTEMASK = 0xff;
  private static final int SHORTMASK = 0xffff;
  private static final int BYTESHIFT = 8;

  private DataInputStream in;

  public LengthFramer(InputStream in) {
    this.in = new DataInputStream(in);
  }

  @Override
  public void frameMsg(byte[] message, OutputStream out) throws Exception {
    //訊息長度不能超過65535,即兩個位元組
    if (message.length > MAXMESSSAGELENGTH) {
      throw new IOException("message too long");
    }
    //基於顯式長度的幀訊息,前兩個位元組寫入訊息長度。
    out.write((message.length >> BYTESHIFT) & BYTEMASK);
    out.write(message.length & BYTEMASK);
    out.write(message);
    out.flush();
  }

  @Override
  public byte[] nextMsg() throws EOFException, IOException {
    int length;
    try {
      //讀取前兩個位元組是訊息的長度。
      length = in.readUnsignedShort();
    } catch (EOFException e) {
      return null;
    }
    byte[] bytes = new byte[length];
    in.readFully(bytes);
    return bytes;
  }
}