3.1信息和編碼
通過(guò)套接字進(jìn)行發(fā)送和接收時(shí)篷牌,只能處理字節(jié)和字節(jié)數(shù)組恶迈。作為一種強(qiáng)類型語(yǔ)言臣咖,Java需要把其他數(shù)據(jù)類型(int跃捣,String等)顯式轉(zhuǎn)換成字節(jié)數(shù)組。比如String類的getBytes()方法夺蛇,是將一個(gè)Sring實(shí)例中的字符轉(zhuǎn)換成字節(jié)的標(biāo)準(zhǔn)方式疚漆。
注:語(yǔ)言有無(wú)類型,弱類型和強(qiáng)類型三種刁赦。其中娶聘,無(wú)類型不檢查,甚至不區(qū)分指令和數(shù)據(jù)甚脉;弱類型的檢查很弱丸升,僅能嚴(yán)格的區(qū)分指令和數(shù)據(jù);強(qiáng)類型的則嚴(yán)格的在編譯期進(jìn)行檢查牺氨。強(qiáng)類型語(yǔ)言在沒(méi)有強(qiáng)制類型轉(zhuǎn)化前发钝,不允許兩種不同類型的變量相互操作顿涣。 如:double類型變量a,不經(jīng)過(guò)強(qiáng)制類型轉(zhuǎn)換那么程序int b = a是無(wú)法通過(guò)編譯酝豪。常用的強(qiáng)類型語(yǔ)言有Java涛碑、C# 、Apex和Python等孵淘。
3.1.1基本類型
計(jì)算機(jī)組成原理原碼補(bǔ)碼知識(shí)
3.1.2字符串和文本
我們可以將數(shù)字和boolean類型的數(shù)據(jù)表示成String類型蒲障,如“123478962”,“6.02e23”瘫证,“true”揉阎,“false”等。也可以通過(guò)調(diào)用getBytes()方法背捌,將一個(gè)字符串轉(zhuǎn)換成字節(jié)數(shù)組毙籽。
3.2組合輸入輸出流
Java中與流相關(guān)的類可以組合起來(lái)從而提供強(qiáng)大的功能。例如毡庆,我們可以將一個(gè)Socket實(shí)例的OutputStream包裝在一個(gè)BufferedOutputStream實(shí)例中坑赡,這樣可以先將字節(jié)暫時(shí)緩存在一起,然后再一次全部發(fā)送到底層的通信信道中么抗,以提高程序的性能毅否。我們還能再將這個(gè)BufferedOutputStream實(shí)例包裹在一個(gè)DataOutputStream實(shí)例中,以實(shí)現(xiàn)發(fā)送基本數(shù)據(jù)類型的功能蝇刀。
在這個(gè)例子中螟加,我們先將基本數(shù)據(jù)的值,一個(gè)一個(gè)寫入Data OutputStream中吞琐,DataOutputStream再將這些數(shù)據(jù)以二進(jìn)制的形式寫入BufferedOutput-Stream并將三次寫入的數(shù)據(jù)緩存起來(lái)捆探,然后再由BufferedOutputStream一次性地將這些數(shù)據(jù)寫入套接字的OutputStream,最后由OutputStream將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)站粟。在另一個(gè)終端徐许,我們創(chuàng)建了相應(yīng)的組合InputStream,以有效地接收基本數(shù)據(jù)類型卒蘸。
成幀與解析
這部分主要講的是流傳輸中對(duì)數(shù)據(jù)開始和結(jié)束邊界的處理雌隅,這也是為什么我們使用read()方法讀取-1,進(jìn)行判定讀到流結(jié)束的原因缸沃。(SOGaG∑稹)
成幀(framing)技術(shù)則解決了接收端如何定位消息的首尾位置的問(wèn)題。無(wú)論信息是編碼成了文本趾牧、多字節(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é)議中沒(méi)有消息邊界的概念柬唯,因此,在使用TCP套接字時(shí)圃庭,成幀就是一個(gè)非常重要的考慮因素(在TCP連接中锄奢,接收者讀取完最后一條消息的最后一個(gè)字節(jié)后,將受到一個(gè)流結(jié)束標(biāo)記剧腻,即read()返回-1拘央,該標(biāo)記指示出已經(jīng)讀取到了消息的末尾,非嚴(yán)格意義上來(lái)講书在,這也算是基于定界符方法的一種特殊情況)灰伟。
主要有兩種技術(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è)固定大小的字段,用來(lái)指示該字段或消息中包含了多少字節(jié)捻激。該方法主要用在以二進(jìn)制字節(jié)方式編碼的消息中制轰。
-
基于定界符的方法
通常用在以文本方式編碼的消息中:定義一個(gè)特殊的字符或字符串來(lái)標(biāo)識(shí)消息的結(jié)束。接收者只需要簡(jiǎn)單地掃描輸入信息(以字節(jié)的方式)來(lái)查找定界序列胞谭,并將定界符前面的字符串返回垃杖。這種方法的缺點(diǎn)是消息本身不能包含有定界字符,否則接收者將提前認(rèn)為消息已經(jīng)結(jié)束丈屹。在基于定界符的成幀方法中调俘,發(fā)送者要保證滿足這個(gè)先決條件。幸運(yùn)的是旺垒,填充(stuffing)技術(shù)能夠?qū)ο⒅谐霈F(xiàn)的定界符進(jìn)行修改彩库,從而使接收者不將其識(shí)別為定界符。在接收者掃描定界符時(shí)先蒋,還能識(shí)別出修改過(guò)的數(shù)據(jù)骇钦,并在輸出消息中對(duì)其進(jìn)行還原,從而使其與原始消息一致竞漾。這個(gè)技術(shù)的缺點(diǎn)是發(fā)送者和接收者雙方都必須掃描消息眯搭。 -
基于長(zhǎng)度的方法
更簡(jiǎn)單一些窥翩,不過(guò)要使用這種方法必須知道消息長(zhǎng)度的上限。發(fā)送者先要確定消息的長(zhǎng)度鳞仙,將長(zhǎng)度信息存入一個(gè)整數(shù)寇蚊,作為消息的前綴。消息的長(zhǎng)度上限定義了用來(lái)編碼消息長(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()方法用來(lái)添加成幀信息并將指定消息輸出到指定流,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()方法并沒(méi)有實(shí)現(xiàn)填充,當(dāng)成幀的字節(jié)序列中包含有定界符時(shí)谦纱,它只是簡(jiǎn)單地拋出異常看成。nextMsg()方法掃描流,直到讀取到了定界符跨嘉,并返回定界符前面的所有字符川慌,如果流為空則返回null。如果累積了一個(gè)消息的不少字符祠乃,但直到流結(jié)束也沒(méi)有找到定界符梦重,程序?qū)伋鲆粋€(gè)異常來(lái)指示成幀錯(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ù)來(lái)源
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é)束還沒(méi)有讀取到定界符
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ù)來(lái)源
}
//對(duì)字節(jié)流message添加成幀信息,并輸出到指定流
public void frameMsg(byte[] message, OutputStream out) throws IOException {
//消息的長(zhǎng)度不能超過(guò)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é)來(lái)填滿指定的數(shù)組
in.readFully(msg); //
return msg;
}
}