3 發(fā)送和接收數(shù)據(jù)

3.1信息和編碼

通過套接字進(jìn)行發(fā)送和接收時(shí),只能處理字節(jié)和字節(jié)數(shù)組。作為一種強(qiáng)類型語言,Java需要把其他數(shù)據(jù)類型(int,String等)顯式轉(zhuǎn)換成字節(jié)數(shù)組。比如String類的getBytes()方法,是將一個(gè)Sring實(shí)例中的字符轉(zhuǎn)換成字節(jié)的標(biāo)準(zhǔn)方式。
注:語言有無類型,弱類型和強(qiáng)類型三種。其中,無類型不檢查,甚至不區(qū)分指令和數(shù)據(jù);弱類型的檢查很弱,僅能嚴(yán)格的區(qū)分指令和數(shù)據(jù);強(qiáng)類型的則嚴(yán)格的在編譯期進(jìn)行檢查。強(qiáng)類型語言在沒有強(qiáng)制類型轉(zhuǎn)化前,不允許兩種不同類型的變量相互操作。 如:double類型變量a,不經(jīng)過強(qiáng)制類型轉(zhuǎn)換那么程序int b = a是無法通過編譯。常用的強(qiáng)類型語言有Java、C# 、Apex和Python等。

3.1.1基本類型

計(jì)算機(jī)組成原理原碼補(bǔ)碼知識(shí)

3.1.2字符串和文本

我們可以將數(shù)字和boolean類型的數(shù)據(jù)表示成String類型,如“123478962”,“6.02e23”,“true”,“false”等。也可以通過調(diào)用getBytes()方法,將一個(gè)字符串轉(zhuǎn)換成字節(jié)數(shù)組。

3.2組合輸入輸出流

Java中與流相關(guān)的類可以組合起來從而提供強(qiáng)大的功能。例如,我們可以將一個(gè)Socket實(shí)例的OutputStream包裝在一個(gè)BufferedOutputStream實(shí)例中,這樣可以先將字節(jié)暫時(shí)緩存在一起,然后再一次全部發(fā)送到底層的通信信道中,以提高程序的性能。我們還能再將這個(gè)BufferedOutputStream實(shí)例包裹在一個(gè)DataOutputStream實(shí)例中,以實(shí)現(xiàn)發(fā)送基本數(shù)據(jù)類型的功能。


組合輸入輸出流圖解.png

在這個(gè)例子中,我們先將基本數(shù)據(jù)的值,一個(gè)一個(gè)寫入Data OutputStream中,DataOutputStream再將這些數(shù)據(jù)以二進(jìn)制的形式寫入BufferedOutput-Stream并將三次寫入的數(shù)據(jù)緩存起來,然后再由BufferedOutputStream一次性地將這些數(shù)據(jù)寫入套接字的OutputStream,最后由OutputStream將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)。在另一個(gè)終端,我們創(chuàng)建了相應(yīng)的組合InputStream,以有效地接收基本數(shù)據(jù)類型。


Java中的相關(guān)類.png

成幀與解析

這部分主要講的是流傳輸中對(duì)數(shù)據(jù)開始和結(jié)束邊界的處理,這也是為什么我們使用read()方法讀取-1,進(jìn)行判定讀到流結(jié)束的原因。(SOGa!)
成幀(framing)技術(shù)則解決了接收端如何定位消息的首尾位置的問題。無論信息是編碼成了文本、多字節(jié)二進(jìn)制數(shù)、或是兩者的結(jié)合,應(yīng)用程序協(xié)議必須指定消息的接收者如何確定何時(shí)消息已完整接收。
由于UDP套接字保留了消息的邊界信息,因此不需要進(jìn)行成幀處理(實(shí)際上,主要是DatagramPacket負(fù)載的數(shù)據(jù)有一個(gè)確定的長(zhǎng)度,接收者能夠準(zhǔn)確地知道消息的結(jié)束位置),而TCP協(xié)議中沒有消息邊界的概念,因此,在使用TCP套接字時(shí),成幀就是一個(gè)非常重要的考慮因素(在TCP連接中,接收者讀取完最后一條消息的最后一個(gè)字節(jié)后,將受到一個(gè)流結(jié)束標(biāo)記,即read()返回-1,該標(biāo)記指示出已經(jīng)讀取到了消息的末尾,非嚴(yán)格意義上來講,這也算是基于定界符方法的一種特殊情況)。
主要有兩種技術(shù)使接收者能夠準(zhǔn)確地找到消息的結(jié)束位置:

  • 1、基于定界符:消息的結(jié)束由一個(gè)唯一的標(biāo)記指出,即發(fā)送者在傳輸完數(shù)據(jù)后顯式添加的一個(gè)特定字節(jié)序列,這個(gè)特殊標(biāo)記不能在傳輸?shù)臄?shù)據(jù)中出現(xiàn)(這也不是絕對(duì)的,應(yīng)用填充技術(shù)能夠?qū)ο⒅谐霈F(xiàn)的定界符進(jìn)行修改,從而使接收者不將其識(shí)別為定界符)。該方法通常用在以文本方式編碼的消息中。
  • 2、顯式長(zhǎng)度:在變長(zhǎng)字段或消息前附加一個(gè)固定大小的字段,用來指示該字段或消息中包含了多少字節(jié)。該方法主要用在以二進(jìn)制字節(jié)方式編碼的消息中。
  • 基于定界符的方法
    通常用在以文本方式編碼的消息中:定義一個(gè)特殊的字符或字符串來標(biāo)識(shí)消息的結(jié)束。接收者只需要簡(jiǎn)單地掃描輸入信息(以字節(jié)的方式)來查找定界序列,并將定界符前面的字符串返回。這種方法的缺點(diǎn)是消息本身不能包含有定界字符,否則接收者將提前認(rèn)為消息已經(jīng)結(jié)束。在基于定界符的成幀方法中,發(fā)送者要保證滿足這個(gè)先決條件。幸運(yùn)的是,填充(stuffing)技術(shù)能夠?qū)ο⒅谐霈F(xiàn)的定界符進(jìn)行修改,從而使接收者不將其識(shí)別為定界符。在接收者掃描定界符時(shí),還能識(shí)別出修改過的數(shù)據(jù),并在輸出消息中對(duì)其進(jìn)行還原,從而使其與原始消息一致。這個(gè)技術(shù)的缺點(diǎn)是發(fā)送者和接收者雙方都必須掃描消息。
  • 基于長(zhǎng)度的方法
    更簡(jiǎn)單一些,不過要使用這種方法必須知道消息長(zhǎng)度的上限。發(fā)送者先要確定消息的長(zhǎng)度,將長(zhǎng)度信息存入一個(gè)整數(shù),作為消息的前綴。消息的長(zhǎng)度上限定義了用來編碼消息長(zhǎng)度所需要的字節(jié)數(shù):如果消息的長(zhǎng)度小于256字節(jié),則需要1個(gè)字節(jié);如果消息的長(zhǎng)度小于65 536字節(jié),則需要2個(gè)字節(jié)等。
    代碼實(shí)現(xiàn):
    定義的Framer接口。它有兩個(gè)方法:frameMsg()方法用來添加成幀信息并將指定消息輸出到指定流,nextMsg()方法則掃描指定的流,從中抽取出下一條消息。
    Frame.java
import java.io.IOException;
import java.io.OutputStream;
 
public interface Framer {
  void frameMsg(byte[] message, OutputStream out) throws IOException;
  byte[] nextMsg() throws IOException;
}

DelimFramer.java類實(shí)現(xiàn)了基于定界符的成幀方法,其定界符為“換行”符(“\n”,字節(jié)值為10)。frameMethod()方法并沒有實(shí)現(xiàn)填充,當(dāng)成幀的字節(jié)序列中包含有定界符時(shí),它只是簡(jiǎn)單地拋出異常。nextMsg()方法掃描流,直到讀取到了定界符,并返回定界符前面的所有字符,如果流為空則返回null。如果累積了一個(gè)消息的不少字符,但直到流結(jié)束也沒有找到定界符,程序?qū)伋鲆粋€(gè)異常來指示成幀錯(cuò)誤。
DelimFramer.java

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
 
public class DelimFramer implements Framer {
 
  private InputStream in;        // 數(shù)據(jù)來源
  private static final byte DELIMITER = '\n'; // 定界符
 
  public DelimFramer(InputStream in) {
    this.in = in;
  }
 
  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();
  }
 
  public byte[] nextMsg() throws IOException {
    ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
    int nextByte;
 
    while ((nextByte = in.read()) != DELIMITER) {
      //如果流已經(jīng)結(jié)束還沒有讀取到定界符
      if (nextByte == -1) { 
        //如果讀取到的流為空,則返回null
        if (messageBuffer.size() == 0) { 
          return null;
        } else { 
          //如果讀取到的流不為空,則拋出異常
          throw new EOFException("Non-empty message without delimiter");
        }
      }
      messageBuffer.write(nextByte); 
    }
 
    return messageBuffer.toByteArray();
  }
}

LengthFramer.java類實(shí)現(xiàn)了基于長(zhǎng)度的成幀方法,適用于長(zhǎng)度小于65 535(216-1)字節(jié)的消息。發(fā)送者首先給出指定消息的長(zhǎng)度,并將長(zhǎng)度信息以big-endian順序存入兩個(gè)字節(jié)的整數(shù)中,再將這兩個(gè)字節(jié)放在完整的消息內(nèi)容前,連同消息一起寫入輸出流。在接收端,我們使用DataInputStream以讀取整型的長(zhǎng)度信息;readFully()方法將阻塞等待,直到給定的數(shù)組完全填滿,這正是我們需要的。值得注意的是,使用這種成幀方法,發(fā)送者不需要檢查要成幀的消息內(nèi)容,而只需要檢查消息的長(zhǎng)度是否超出了限制。
LengthFramer.java

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
 
public class LengthFramer implements Framer {
  public static final int MAXMESSAGELENGTH = 65535;
  public static final int BYTEMASK = 0xff;
  public static final int SHORTMASK = 0xffff;
  public static final int BYTESHIFT = 8;
 
  private DataInputStream in;
 
  public LengthFramer(InputStream in) throws IOException {
    this.in = new DataInputStream(in);    //數(shù)據(jù)來源
  }
 
  //對(duì)字節(jié)流message添加成幀信息,并輸出到指定流 
  public void frameMsg(byte[] message, OutputStream out) throws IOException {
    //消息的長(zhǎng)度不能超過65535
    if (message.length > MAXMESSAGELENGTH) {
      throw new IOException("message too long");
    }
    out.write((message.length >> BYTESHIFT) & BYTEMASK);
    out.write(message.length & BYTEMASK);
    out.write(message);
    out.flush();
  }
 
  public byte[] nextMsg() throws IOException {
    int length;
    try { 
      //該方法讀取2個(gè)字節(jié),將它們作為big-endian整數(shù)進(jìn)行解釋,并以int型整數(shù)返回它們的值
      length = in.readUnsignedShort(); 
    } catch (EOFException e) { // no (or 1 byte) message
      return null;
    }
    // 0 <= length <= 65535
    byte[] msg = new byte[length];
    //該方法處阻塞等待,直到接收到足夠的字節(jié)來填滿指定的數(shù)組
    in.readFully(msg); //
    return msg;
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容