天天看點

Java進階06-網絡程式設計,編碼格式

網絡程式設計

計算機網絡

什麼是IP位址

IP位址一般泛指IPv4,長32比特,以點分十進制表示,範圍為0.0.0.0~255.255.255.255,IP位址是唯一辨別網際網路計算機的邏輯位址。也就是說,每台計算機都有唯一的IP位址,反之,可以通過一個IP位址鎖定一台計算機。

IP位址不是唯一,MAC位址才是唯一

IP位址分公網 和私網,靜态IP和動态IP(一般的電腦都是動态配置設定IP,是私網IP,因為目前公有的IP位址不夠用了)

什麼是子網路遮罩

子網路遮罩(subnet mask)又叫網絡掩碼、位址掩碼、子網絡遮罩,它是一種用來指明一個IP位址的哪些位辨別的是主機所在的子網以及哪些位辨別的是主機的位掩碼。子網路遮罩不能單獨存在,它必須結合IP位址一起使用。子網路遮罩隻有一個作用,就是将某個IP位址劃分成網絡位址(對外)和主機位址(對内)兩部分。

是什麼網關

網關(Gateway)又稱網間連接配接器、協定轉換器。網關在傳輸層上以實作網絡互連,是最複雜的網絡互連裝置,僅用于兩個高層協定不同的網絡互連。網關的結構也和路由器類似,不同的是互連層。網關既可以用于廣域網互連,也可以用于區域網路互連。 網關是一種充當轉換重任的計算機系統或裝置。在使用不同的通信協定、資料格式或語言,甚至體系結構完全不同的兩種系統之間,網關是一個翻譯器。與網橋隻是簡單地傳達資訊不同,網關對收到的資訊要重新打包,以适應目的系統的需求。網關實質上是一個網絡通向其他網絡的IP位址。

有網關協定- 可以把A網關的資訊發給B網關。同時網絡中有一個網關表。

什麼是路由

TCP/IP網絡是由網關(Gateways)或路由器(Routers)連接配接的。當IP準備發送一個包的時候,它把本地(源)IP位址和包的目的位址插入IP頭,并且檢查目的地網絡ID是否和源主機的網絡ID一緻,如果一緻,包就被直接發送到本地網的目的計算機,如果不一緻,就檢查路由表中的靜态路由,如果沒有發現路由資訊,包就被轉送到預設網關。

預設網關連接配接到本地子網和其它網絡的計算機,它知道網際網上其它網絡的網絡ID,也知道如何到達那裡,是以它能把包轉發到别的網關,直到最終轉發到直接和限定的目的地相連的網關,這一過程稱為路由。

什麼DNS

DNS 是域名系統 (Domain Name System) 的縮寫,是網際網路的一項核心服務,它作為可以将域名和IP位址互相映射的一個分布式資料庫,能夠使人更友善的通路網際網路,而不用去記住能夠被機器直接讀取的IP數串。

TCP/IP模型

Java進階06-網絡程式設計,編碼格式
Java進階06-網絡程式設計,編碼格式
  1. 應用層:任務是通過應用程序間的互動來完成特定的網絡應用。
  2. 運輸層:任務是負責向兩個主機中程序之間的通信提供通用的資料傳輸服務。 應用層主要有兩種協定:

    *傳輸控制協定TCP——提供面向連接配接的、可靠地資料傳輸服務,其資料傳輸的機關是封包段。

    *使用者資料報協定UDP——-提供無連接配接的、盡最大努力傳遞的資料傳輸服務(不保證資料傳輸的可靠性),其資料傳輸的機關是使用者資料報。

  3. 網絡層:負責為分組交換網上的不同主機提供通信服務。在發送資料的時候,網絡層把運輸層産生的封包段或使用者資料報封裝成分組或包進行傳送。分組也叫IP資料包或者資料報。是以,網絡層也是把資料封裝成資料報。
  4. 資料鍊路層:兩台主機之間的資料傳輸,總是在一段一段的鍊路層上傳送的,這就需要專門的資料鍊路層協定。當兩個相鄰節點之間傳送資料時,資料鍊路層将網絡層上交下來的IP資料報組裝成幀。
  5. 實體層:也就是最底層,傳輸的資料是比特。

TCP/IP,即Transmission Control Protocol/Internet Protocol的簡寫,中譯名為傳輸控制協定/網際網路互聯協定,是Internet最基本的協定、Internet國際網際網路絡的基礎。

IP協定

TCP協定

Java進階06-網絡程式設計,編碼格式
  1. 源端口和目的端口,各占2個位元組,分别寫入源端口和目的端口;
  2. 序号,占4個位元組,TCP連接配接中傳送的位元組流中的每個位元組都按順序編号。例如,一段封包的序号字段值是 301 ,而攜帶的資料共有100字段,顯然下一個封包段(如果還有的話)的資料序号應該從401開始;
  3. 确認号,占4個位元組,是期望收到對方下一個封包的第一個資料位元組的序号。例如,B收到了A發送過來的封包,其序列号字段是501,而資料長度是200位元組,這表明B正确的收到了A發送的到序号700為止的資料。是以,B期望收到A的下一個資料序号是701,于是B在發送給A的确認封包段中把确認号置為701;
  4. 資料偏移,占4位,它指出TCP封包的資料距離TCP封包段的起始處有多遠;
  5. 保留,占6位,保留今後使用,但目前應都位0;
  6. 緊急URG,當URG=1,表明緊急指針字段有效。告訴系統此封包段中有緊急資料;
  7. 确認ACK,僅當ACK=1時,确認号字段才有效。TCP規定,在連接配接建立後所有封包的傳輸都必須把ACK置1;
  8. 推送PSH,當兩個應用程序進行互動式通信時,有時在一端的應用程序希望在鍵入一個指令後立即就能收到對方的響應,這時候就将PSH=1;
  9. 複位RST,當RST=1,表明TCP連接配接中出現嚴重差錯,必須釋放連接配接,然後再重建立立連接配接;
  10. 同步SYN,在連接配接建立時用來同步序号。當SYN=1,ACK=0,表明是連接配接請求封包,若同意連接配接,則響應封包中應該使SYN=1,ACK=1;
  11. 終止FIN,用來釋放連接配接。當FIN=1,表明此封包的發送方的資料已經發送完畢,并且要求釋放;
  12. 視窗,占2位元組,指的是通知接收方,發送本封包你需要有多大的空間來接受;
  13. 檢驗和,占2位元組,校驗首部和資料這兩部分;
  14. 緊急指針,占2位元組,指出本封包段中的緊急資料的位元組數;
  15. 選項,長度可變,定義一些其他的可選的參數。

三次握手

Java進階06-網絡程式設計,編碼格式
  1. TCP伺服器程序先建立傳輸控制塊TCB,時刻準備接受客戶程序的連接配接請求,此時伺服器就進入了LISTEN(監聽)狀态;
  2. TCP客戶程序也是先建立傳輸控制塊TCB,然後向伺服器發出連接配接請求封包,這是封包首部中的同部位SYN=1,同時選擇一個初始序列号 seq=x ,此時,TCP用戶端程序進入了 SYN-SENT(同步已發送狀态)狀态。TCP規定,SYN封包段(SYN=1的封包段)不能攜帶資料,但需要消耗掉一個序号。
  3. TCP伺服器收到請求封包後,如果同意連接配接,則發出确認封包。确認封包中應該 ACK=1,SYN=1,确認号是ack=x+1,同時也要為自己初始化一個序列号 seq=y,此時,TCP伺服器程序進入了SYN-RCVD(同步收到)狀态。這個封包也不能攜帶資料,但是同樣要消耗一個序号。
  4. TCP客戶程序收到确認後,還要向伺服器給出确認。确認封包的ACK=1,ack=y+1,自己的序列号seq=x+1,此時,TCP連接配接建立,用戶端進入ESTABLISHED(已建立連接配接)狀态。TCP規定,ACK封包段可以攜帶資料,但是如果不攜帶資料則不消耗序号。
  5. 當伺服器收到用戶端的确認後也進入ESTABLISHED狀态,此後雙方就可以開始通信了。

四次揮手

Java進階06-網絡程式設計,編碼格式
  1. 用戶端程序發出連接配接釋放封包,并且停止發送資料。釋放資料封包首部,FIN=1,其序列号為seq=u(等于前面已經傳送過來的資料的最後一個位元組的序号加1),此時,用戶端進入FIN-WAIT-1(終止等待1)狀态。 TCP規定,FIN封包段即使不攜帶資料,也要消耗一個序号。
  2. 伺服器收到連接配接釋放封包,發出确認封包,ACK=1,ack=u+1,并且帶上自己的序列号seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀态。TCP伺服器通知高層的應用程序,用戶端向伺服器的方向就釋放了,這時候處于半關閉狀态,即用戶端已經沒有資料要發送了,但是伺服器若發送資料,用戶端依然要接受。這個狀态還要持續一段時間,也就是整個CLOSE-WAIT狀态持續的時間。
  3. 用戶端收到伺服器的确認請求後,此時,用戶端就進入FIN-WAIT-2(終止等待2)狀态,等待伺服器發送連接配接釋放封包(在這之前還需要接受伺服器發送的最後的資料)。
  4. 伺服器将最後的資料發送完畢後,就向用戶端發送連接配接釋放封包,FIN=1,ack=u+1,由于在半關閉狀态,伺服器很可能又發送了一些資料,假定此時的序列号為seq=w,此時,伺服器就進入了LAST-ACK(最後确認)狀态,等待用戶端的确認。
  5. 用戶端收到伺服器的連接配接釋放封包後,必須發出确認,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此時,用戶端就進入了TIME-WAIT(時間等待)狀态。注意此時TCP連接配接還沒有釋放,必須經過2∗*∗MSL(最長封包段壽命)的時間後,當用戶端撤銷相應的TCB後,才進入CLOSED狀态。
  6. 伺服器隻要收到了用戶端發出的确認,立即進入CLOSED狀态。同樣,撤銷TCB後,就結束了這次的TCP連接配接。可以看到,伺服器結束TCP連接配接的時間要比用戶端早一些。

為什麼用戶端最後還要等待2MSL?

MSL(Maximum Segment Lifetime),TCP允許不同的實作可以設定不同的MSL值。

第一,保證用戶端發送的最後一個ACK封包能夠到達伺服器,因為這個ACK封包可能丢失,站在伺服器的角度看來,我已經發送了FIN+ACK封包請求斷開了,用戶端還沒有給我回應,應該是我發送的請求斷開封包它沒有收到,于是伺服器又會重新發送一次,而用戶端就能在這個2MSL時間段内收到這個重傳的封包,接着給出回應封包,并且會重新開機2MSL計時器。

第二,防止類似與“三次握手”中提到了的“已經失效的連接配接請求封包段”出現在本連接配接中。用戶端發送完最後一個确認封包後,在這個2MSL時間中,就可以使本連接配接持續的時間内所産生的所有封包段都從網絡中消失。這樣新的連接配接中不會出現舊連接配接的請求封包。

為什麼建立連接配接是三次握手,關閉連接配接确是四次揮手呢?

建立連接配接的時候, 伺服器在LISTEN狀态下,收到建立連接配接請求的SYN封包後,把ACK和SYN放在一個封包裡發送給用戶端。

而關閉連接配接時,伺服器收到對方的FIN封包時,僅僅表示對方不再發送資料了但是還能接收資料,而自己也未必全部資料都發送給對方了,是以己方可以立即關閉,也可以發送一些資料給對方後,再發送FIN封包給對方來表示同意現在關閉連接配接,是以,己方ACK和FIN一般都會分開發送,進而導緻多了一次。

如果已經建立了連接配接,但是用戶端突然出現故障了怎麼辦?

TCP還設有一個保活計時器,顯然,用戶端如果出現故障,伺服器不能一直等下去,白白浪費資源。伺服器每收到一次用戶端的請求後都會重新複位這個計時器,時間通常是設定為2小時,若兩小時還沒有收到用戶端的任何資料,伺服器就會發送一個探測封包段,以後每隔75秒發送一次。若一連發送10個探測封包仍然沒反應,伺服器就認為用戶端出了故障,接着就關閉連接配接。

Http協定

HTTP,超文本傳輸協定,英文全稱是Hypertext Transfer Protocol,它是網際網路上應用最為廣泛的一種網絡協定。HTTP是一種應用層協定,它是基于TCP協定之上的請求/響應式的協定,即一個用戶端與伺服器建立連接配接後,向伺服器發送一個請求;伺服器接到請求後,給予相應的響應資訊。HTTP協定預設的端口号為80.

總結

  1. IP位址不是唯一,MAC位址才是唯一
  2. 網關和路由也是協定,主要是尋址
  3. IP位址分公網 和私網

Java網絡程式設計-Socket

定義解釋

麼什麼是Socket呢?簡單地說,Socket,套接字,就是兩台主機之間邏輯連接配接的端點。TPC/IP協定是傳輸層協定,主要解決資料如何在網絡中傳輸,而HTTP是應用層協定,主要解決如何包裝資料。Socket,本質上就是一組接口,是對TCP/IP協定的封裝和應用(程式員層面上)。

我們經常把socket翻譯為套接字,socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操作抽象為幾個簡單的接口供應用層調用已實作程序在網絡中通信。

Java進階06-網絡程式設計,編碼格式

如何使用

使用比較簡單,總共也沒幾個API,可能設計IO的還多一些,我們寫個Demo測試socket連接配接 收發資訊。

首先是服務端:

public void init() {
		try {
			System.out.println("服務端啟動");
			int port=4567;
			server = new ServerSocket(port);
			// server将一直等待連接配接的到來
			System.out.println("server将一直等待連接配接的到來");
			Socket socket = server.accept();
			// 建立好連接配接後,從socket中擷取輸入流,并建立緩沖區進行讀取
			System.out.println("有用戶端來連接配接");
			InputStream inputStream = socket.getInputStream();
			byte[] bytes = new byte[1024];
			int len;
			StringBuilder sb = new StringBuilder();
			while ((len = inputStream.read(bytes)) != -1) {
				// 注意指定編碼格式,發送方和接收方一定要統一,建議使用UTF-8
				sb.append(new String(bytes, 0, len, "UTF-8"));
			}
			System.out.println("get message from client: " + sb);
			inputStream.close();
			socket.close();
			server.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
           

然後就是用戶端:

public static void main(String[] args) {
		try {
			System.out.println("用戶端啟動");
			// 要連接配接的服務端IP位址和端口
			String host = "192.168.0.26";
			int port = 4567;
			// 與服務端建立連接配接
			Socket socket;
			socket = new Socket(host, port);
			System.out.println("連接配接上服務端");
			// 建立連接配接後獲得輸出流
			OutputStream outputStream = socket.getOutputStream();
			String message = "你好  我是用戶端";
			outputStream.write(message.getBytes("UTF-8"));
			outputStream.close();
			socket.close();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
           

先運作服務端,然後運作用戶端,運作結果:

Java進階06-網絡程式設計,編碼格式

可以說socket的使用還是很簡單的。就是ServiceSocket和Socket 2個類。而且方法也沒多少。

上面需要注意的是host這個是服務端的IP位址,檢視自己電腦的IP位址 使用ipconfig

Java進階06-網絡程式設計,編碼格式

然後就是字元串編碼問題,這個隻要統一就好了。否則會出現亂碼,具體的原因。如果篇幅還夠就寫在後面,否則就另開一篇。

socket也可以進行長連接配接,就是用個無限循環等待資料,這樣雙方就可以進行互動了。比如聊天。

當然ServiceSocket并不隻會為一個socket服務,需要無限循環調用Socket socket = server.accept(); 并且開啟新的線程處理。

Java進階06-網絡程式設計,編碼格式

有何利弊

原理源碼

主要是通過SocketImpl實作。

Java進階06-網絡程式設計,編碼格式

使用場景

網絡請求

總結

  1. read() 方法會導緻堵塞
  2. accept() 方法會導緻堵塞
  3. shutdownInput() shutdownOutput()指結束寫入/讀出
  4. Java C++ C等等都有socket

Java網絡API-HttpURLConnection

定義解釋

一種多用途、輕量極的HTTP用戶端,使用它來進行HTTP操作可以适用于大多數的應用程式。 雖然HttpURLConnection的API提供的比較簡單,但是同時這也使得我們可以更加容易地去使 用和擴充它。繼承至URLConnection,抽象類,無法直接執行個體化對象。通過調用openCollection() 方法獲得對象執行個體,預設是帶gzip壓縮的;

如何使用

使用HttpURLConnection的步驟如下:

  1. 建立一個URL對象: URL url = new URL(https://www.baidu.com);
  2. 調用URL對象的openConnection( )來擷取HttpURLConnection對象執行個體: HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  3. 設定HTTP請求使用的方法:GET或者POST,或者其他請求方式比如:PUT conn.setRequestMethod(“GET”);
  4. 設定連接配接逾時,讀取逾時的毫秒數,以及伺服器希望得到的一些消息頭 conn.setConnectTimeout(6*1000); conn.setReadTimeout(6 * 1000);
  5. 調用getInputStream()方法獲得伺服器傳回的輸入流,然後輸入流進行讀取了 InputStream in = conn.getInputStream();
  6. 最後調用disconnect()方法将HTTP連接配接關掉 conn.disconnect();

例子 以請求百度為例:

public static byte[] read(InputStream inStream) {
		try {
			ByteArrayOutputStream outStream = new ByteArrayOutputStream();
			byte[] buffer = new byte[1024];
			int len = 0;

			while ((len = inStream.read(buffer)) != -1) {
				outStream.write(buffer, 0, len);
			}
			inStream.close();
			return outStream.toByteArray();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	public static void main(String[] args) {
		try {
			URL url = new URL("https://www.baidu.com");
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			// 設定請求類型為Get類型
			conn.setRequestMethod("GET");
			// 判斷請求Url是否成功
			if (conn.getResponseCode() != 200) {
				throw new RuntimeException("請求url失敗");
			}
			InputStream inStream = conn.getInputStream();
			byte[] bt = read(inStream);
			println(new String(bt));
			inStream.close();
			conn.disconnect();

		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
           

運作結果:

Java進階06-網絡程式設計,編碼格式

當然也可以用Post請求,這裡就不詳細說了。

Java進階06-網絡程式設計,編碼格式

有何利弊

原理源碼

通過socket實作

使用場景

總結

  1. 一般不會直接使用,因為如果請求複雜就會很麻煩
  2. 網上的網絡架構底層原理還是這些,是以基礎要很熟悉才行
  3. 目前最新的是okhttp

什麼是編碼?

因為計算機隻能識别和存儲0101,最小的存儲機關是byte就是8個010101。是以當我們向計算輸入英文或者漢字時 就需要把它們轉化為byte 010101,然後計算機輸出的時候又把byte 根據 一個表轉為原來的英文和漢字。這個轉化的過程就叫做編碼。

比如 05 就代表字母 B

目前有以下幾個主要的表:

  • ASCII 碼學過計算機的人都知道 ASCII 碼,總共有 128 個,用一個位元組的低 7 位表示,0~31 是控制字元如換行回車删除等;32~126 是列印字元,可以通過鍵盤輸入并且能夠顯示出來。 

    ISO-8859-1(擴充ASCII編碼)

    128 個字元顯然是不夠用的,于是 ISO 組織在 ASCII 碼基礎上又制定了一些列标準用來擴充 ASCII 編碼,它們是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵蓋了大多數西歐語言字元,所有應用的最廣泛。ISO-8859-1 仍然是單位元組編碼,它總共能表示 256 個字元。

  • GB2312

    它的全稱是《資訊交換用漢字編碼字元集 基本集》,它是雙位元組編碼,總的編碼範圍是 A1-F7,其中從 A1-A9 是符号區,總共包含 682 個符号,從 B0-F7 是漢字區,包含 6763 個漢字。

  • GBK(擴充GB2312)

    全稱叫《漢字内碼擴充規範》,是國家技術監督局為 windows95 所制定的新的漢字内碼規範,它的出現是為了擴充 GB2312,加入更多的漢字,它的編碼範圍是 8140~FEFE(去掉 XX7F)總共有 23940 個碼位,它能表示 21003 個漢字,它的編碼是和 GB2312 相容的,也就是說用 GB2312 編碼的漢字可以用 GBK 來解碼,并且不會有亂碼。

  • GB18030(相容GB2312)

    全稱是《資訊交換用漢字編碼字元集》,是我國的強制标準,它可能是單位元組、雙位元組或者四位元組編碼,它的編碼與 GB2312 編碼相容,這個雖然是國家标準,但是實際應用系統中使用的并不廣泛。

  • Unicode編碼集

    ISO 試圖想建立一個全新的超語言字典,世界上所有的語言都可以通過這本字典來互相翻譯。可想而知這個字典是多麼的複雜,關于 Unicode 的詳細規範可以參考相應文檔。Unicode 是 Java 和 XML 的基礎,下面詳細介紹 Unicode 在計算機中的存儲形式。

    1. UTF-16

      UTF-16 具體定義了 Unicode 字元在計算機中存取方法。UTF-16 用兩個位元組來表示 Unicode 轉化格式,這個是定長的表示方法,不論什麼字元都可以用兩個位元組表示,兩個位元組是 16 個 bit,是以叫 UTF-16。UTF-16 表示字元非常友善,每兩個位元組表示一個字元,這個在字元串操作時就大大簡化了操作,這也是 Java 以 UTF-16 作為記憶體的字元存儲格式的一個很重要的原因。

    2. UTF-8

      UTF-16 統一采用兩個位元組表示一個字元,雖然在表示上非常簡單友善,但是也有其缺點,有很大一部分字元用一個位元組就可以表示的現在要兩個位元組表示,存儲空間放大了一倍,在現在的網絡帶寬還非常有限的今天,這樣會增大網絡傳輸的流量,而且也沒必要。而 UTF-8 采用了一種變長技術,每個編碼區域有不同的字碼長度。不同類型的字元可以是由 1~6 個位元組組成。

      UTF-8 有以下編碼規則:

    • 如果一個位元組,最高位(第 8 位)為 0,表示這是一個 ASCII 字元(00 - 7F)。可見,所有 ASCII 編碼已經是 UTF-8 了。
    • 如果一個位元組,以 11 開頭,連續的 1 的個數暗示這個字元的位元組數,例如:110xxxxx 代表它是雙位元組 UTF-8 字元的首位元組。
    • 如果一個位元組,以 10 開始,表示它不是首位元組,需要向前查找才能得到目前字元的首位元組

字元編碼:

是一套規則,定義了在計算機記憶體中如何表示字元,是字元集中的每個字元與計算機記憶體中位元組之間的轉換關系,也可以認為是把字元數字化,規定每個“字元”分别用一個位元組還是多個位元組存儲,用哪些位元組來存儲。例如ASCII編碼[你沒看錯,它既是一種字元集合,也是一種字元編碼],定義了英文字母和符号在計算機中的表示方式,是用一個位元組來表示。Unicode字元集合,有好幾種字元編碼方式,例如變長度編碼的UTF8,UTF16等。中文字元集也有很多字元編碼,例如上文提到的GB2312編碼,GBK編碼等。

char、unicode、string和UTF8、UTF16之間的關系描述:Java語言内部使用的就是16位的Unicode編碼,從概念上講java字元串就是Unicode字元序列,Unicode字元集合的碼點(碼點:指與一個編碼表中的某個字元對應的代碼值)可以分成17個代碼級别,第一個代碼級别稱為基本的多語言級别,碼點從U+0000到U+FFFF,即65536個碼點,隻有第一級别的碼點可以用一個char值表示;其餘的16個級别碼點從U+10000到U+10FFFF,其中包含一些輔助字元,需要兩個char值才能表示一個碼點;Unicode隻是一個符号集,它隻規定了符号的二進制代碼,卻沒有規定這個二進制代碼應該如何存儲,二進制與16進制等可以靈活轉換,隻是數值的表示方式變化而已,其值的大小不變。

UTF8、UTF16即為采用什麼樣的規則表示所有的碼點,在UTF16的編碼規則中,UTF16采用不同長度的編碼表示所有的Unicode碼點,在基本的多語言級别,每個字元用16位表示,通常被稱為代碼單元;而輔助字元采用一對連續的代碼單元進行編碼。U+D800U+DFFF為空閑的2048個替代區域,U+D800U+DBFF用于第一個代碼單元,U+DC00~U+DFFF用于第二個代碼單元,這樣的設計可以迅速的知道一個代碼單元是一個字元的編碼還是一個輔助字元的第一或第二部分。

Unicode詳細介紹

1.容易産生後歧義的兩位元組

unicode的第一個版本是用兩個位元組(16bit)來表示所有字元

,實際上這麼說容易讓人産生歧義,我們總覺得兩個位元組就代表儲存在計算機中時是兩個位元組.于是任何字元如果用unicode表示的話儲存下來都占兩個位元組.其實這種說法是錯誤的.

其實Unicode涉及到兩個步驟,首先是定義一個規範,給所有的字元指定一個唯一對應的數字,這完全是數學問題,可以跟計算機沒半毛錢關系.第二步才是怎麼把字元對應的數字儲存在計算機中,這才涉及到實際在計算機中占多少位元組空間.

是以我們也可以這樣了解,Unicode是用0至65535之間的數字來表示所有字元.其中0至127這128個數字表示的字元仍然跟ASCII完全一樣.65536是2的16次方.這是第一步.第二步就是怎麼把0至65535這些數字轉化成01串儲存到計算機中.這肯定就有不同的儲存方式了.于是出現了UTF(unicode transformation format),有UTF-8,UTF-16.

2.UTF-8 與UTF-16的差別

UTF-16比較好了解,就是任何字元對應的數字都用兩個位元組來儲存.我們通常對Unicode的誤解就是把Unicode與UTF-16等同了.但是很顯然如果都是英文字母這做有點浪費.明明用一個位元組能表示一個字元為啥整兩個啊.

于是又有個UTF-8,這裡的8非常容易誤導人,8不是指一個位元組,難道一個位元組表示一個字元?實際上不是.當用UTF-8時表示一個字元是可變的,有可能是用一個位元組表示一個字元,也可能是兩個,三個…反正是根據字元對應的數字大小來确定.

于是UTF-8和UTF-16的優劣很容易就看出來了.如果全部英文或英文與其他文字混合,但英文占絕大部分,用UTF-8就比UTF-16節省了很多空間.而如果全部是中文這樣類似的字元或者混合字元中中文占絕大多數.UTF-16就占優勢了,可以節省很多空間.另外還有個容錯問題,等會再講

看的有點暈了吧,舉個例子.假如中文字"漢"對應的unicode是6C49(這是用十六進制表示,用十進制表示是27721為啥不用十進制表示呢?很明顯用十六進制表示要短點.其實都是等價的沒啥不一樣.就跟你說60分鐘和1小時一樣.).你可能會問當用程式打開一個檔案時我們怎麼知道那是用的UTF-8還是UTF-16啊.自然會有點啥标志,在檔案的開頭幾個位元組就是标志.

EF BB BF 表示UTF-8

FE FF 表示UTF-16.

用UTF-16表示"漢"

假如用UTF-16表示的話就是01101100 01001001(共16 bit,兩個位元組).程式解析的時候知道是UTF-16就把兩個位元組當成一個單元來解析.這個很簡單.

用UTF-8表示"漢"

用UTF-8就有複雜點.因為此時程式是把一個位元組一個位元組的來讀取,然後再根據位元組中開頭的bit标志來識别是該把1個還是兩個或三個位元組做為一個單元來處理.

0xxxxxxx,如果是這樣的01串,也就是以0開頭後面是啥就不用管了XX代表任意bit.就表示把一個位元組做為一個單元.就跟ASCII完全一樣.

110xxxxx 10xxxxxx.如果是這樣的格式,則把兩個位元組當一個單元

1110xxxx 10xxxxxx 10xxxxxx 如果是這種格式則是三個位元組當一個單元.

這是約定的規則.你用UTF-8來表示時必須遵守這樣的規則.我們知道UTF-16不需要用啥字元來做标志,是以兩位元組也就是2的16次能表示65536個字元.

而UTF-8由于裡面有額外的标志資訊,所有一個位元組隻能表示2的7次方128個字元,兩個位元組隻能表示2的11次方2048個字元.而三個位元組能表示2的16次方,65536個字元.

由于"漢"的編碼27721大于2048了所有兩個位元組還不夠,隻能用三個位元組來表示.

所有要用1110xxxx 10xxxxxx 10xxxxxx這種格式.把27721對應的二進制從左到右填充XXX符号(實際上不一定從左到右,也可以從右到左,這是涉及到另外一個問題.等會說.

剛說到填充方式可以不一樣,于是就出現了Big-Endian,Little-Endian的術語.Big-Endian就是從左到右,Little-Endian是從右到左.

由上面我們可以看出UTF-8在局部的位元組錯誤(丢失、增加、改變)不會導緻連鎖性的錯誤,因為 UTF-8 的字元邊界很容易檢測出來,是以容錯性較高。

Unicode版本2

前面說的都是unicode的第一個版本.但65536顯然不算太多的數字,用它來表示常用的字元是沒一點問題.足夠了,但如果加上很多特殊的就也不夠了.于是從1996年開始又來了第二個版本.用四個位元組表示所有字元.這樣就出現了UTF-8,UTF16,UTF-32.原理和之前肯定是完全一樣的,UTF-32就是把所有的字元都用32bit也就是4個位元組來表示.然後UTF-8,UTF-16就視情況而定了.UTF-8可以選擇1至8個位元組中的任一個來表示.而UTF-16隻能是選兩位元組或四位元組…由于unicode版本2的原理完全是一樣的,就不多說了.

前面說了要知道具體是哪種編碼方式,需要判斷文本開頭的标志,下面是所有編碼對應的開頭标志

EF BB BF    UTF-8

FE FF     UTF-16/UCS-2, little endian

FF FE     UTF-16/UCS-2, big endian

FF FE 00 00  UTF-32/UCS-4, little endian.

00 00 FE FF  UTF-32/UCS-4, big-endian.

其中的UCS就是前面說的ISO制定的标準,和Unicode是完全一樣的,隻不過名字不一樣.ucs-2對應utf-16,ucs-4對應UTF-32.UTF-8是沒有對應的UCS

UTF-16 并不是一個完美的選擇,它存在幾個方面的問題:

UTF-16 能表示的字元數有 6 萬多,看起來很多,但是實際上目前 Unicode 5.0 收錄的字元已經達到 99024 個字元,早已超過 UTF-16 的存儲範圍;這直接導緻 UTF-16 地位頗為尴尬——如果誰還在想着隻要使用 UTF-16 就可以高枕無憂的話,恐怕要失望了

UTF-16 存在大小端位元組序問題,這個問題在進行資訊交換時特别突出——如果位元組序未協商好,将導緻亂碼;如果協商好,但是雙方一個采用大端一個采用小端,則必然有一方要進行大小端轉換,性能損失不可避免(大小端問題其實不像看起來那麼簡單,有時會涉及硬體、作業系統、上層軟體多個層次,可能會進行多次轉換)

另外,容錯性低有時候也是一大問題——局部的位元組錯誤,特别是丢失或增加可能導緻所有後續字元全部錯亂,錯亂後要想恢複,可能很簡單,也可能會非常困難。(這一點在日常生活裡大家感覺似乎無關緊要,但是在很多特殊環境下卻是巨大的缺陷)

目前支撐我們繼續使用 UTF-16 的理由主要是考慮到它是雙位元組的,在計算字元串長度、執行索引操作時速度很快。當然這些優點 UTF-32 都具有,但很多人畢竟還是覺得 UTF-32 太占空間了。

反過來 UTF-8 也不完美,也存在一些問題:

文化上的不平衡——對于歐美地區一些以英語為母語的國家 UTF-8 簡直是太棒了,因為它和 ASCII 一樣,一個字元隻占一個位元組,沒有任何額外的存儲負擔;但是對于中日韓等國家來說,UTF-8 實在是太備援,一個字元竟然要占用 3 個位元組,存儲和傳輸的效率不但沒有提升,反而下降了。是以歐美人民常常毫不猶豫的采用 UTF-8,而我們卻老是要猶豫一會兒

變長位元組表示帶來的效率問題——大家對 UTF-8 疑慮重重的一個問題就是在于其因為是變長位元組表示,是以無論是計算字元數,還是執行索引操作效率都不高。為了解決這個問題,常常會考慮把 UTF-8 先轉換為 UTF-16 或者 UTF-32 後再操作,操作完畢後再轉換回去。而這顯然是一種性能負擔。

當然,UTF-8 的優點也不能忘了:

字元空間足夠大,未來 Unicode 新标準收錄更多字元,UTF-8 也能妥妥的相容,是以不會再出現 UTF-16 那樣的尴尬

不存在大小端位元組序問題,資訊交換時非常便捷

容錯性高,局部的位元組錯誤(丢失、增加、改變)不會導緻連鎖性的錯誤,因為 UTF-8 的字元邊界很容易檢測出來,這是一個巨大的優點(正是為了實作這一點,咱們中日韓人民不得不忍受 3 位元組 1 個字元的苦日子)

那麼到底該如何選擇呢?

因為無論是 UTF-8 和 UTF-16/32 都各有優缺點,是以選擇的時候應當立足于實際的應用場景。例如在我的習慣中,存儲在磁盤上或進行網絡交換時都會采用 UTF-8,而在程式内部進行處理時則轉換為 UTF-16/32。對于大多數簡單的程式來說,這樣做既可以保證資訊交換時容易實作互相相容,同時在内部處理時會比較簡單,性能也還算不錯。(基本上隻要你的程式不是 I/O 密集型的都可以這麼幹,當然這隻是我粗淺的認識範圍内的經驗,很可能會被無情的反駁)

稍微再展開那麼一點點……

在一些特殊的領域,字元編碼的選擇會成為一個很關鍵的問題。特别是一些高性能網絡處理程式裡更是如此。這時采用一些特殊的設計技巧,可以緩解性能和字元集選擇之間的沖突。例如對于内容檢測/過濾系統,需要面對任何可能的字元編碼,這時如果還采用把各種不同的編碼都轉換為同一種編碼後再處理的方案,那麼性能下降将會很顯著。而如果采用多字元編碼支援的有限狀态機方案,則既能夠無需轉換編碼,同時又能夠以極高的性能進行處理。當然如何從規則清單生成有限狀态機,如何使得有限狀态機支援多編碼,以及這将帶來哪些限制,已經又成了另外的問題了。

具體執行個體 中 在不同的編碼格式下占的位元組

public static void println16(String tag, byte[] data) {

		int length = data == null ? 0 : data.length;

		if (length == 0) {
			println("Emprt data");
		} else {
			StringBuffer buffer = new StringBuffer();

			for (int i = 0; i < length; i++) {
				buffer.append(String.format("%02x", data[i]) + ",");
			}

			println(tag + ":" + buffer);
		}

	}

	public static void main(String[] args) {
		// new SocketMyService().init();
		String message = "中";
		try {
			println16("zx",message.getBytes("UTF-8"));
			println16("zx",message.getBytes("UTF-16"));
			println16("zx",message.getBytes());
			println16("zx",message.getBytes("GBK"));
			println16("zx",message.getBytes("unicode"));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

	}
           
Java進階06-網絡程式設計,編碼格式

看到Utf-8是3個位元組,utf-16是4個位元組,這個和上面說的不一樣。上面不是說u-utf16是2個位元組碼。經過搜尋了解到java的位元組碼檔案(.class)檔案采用的是UTF-8編碼,但是在java 運作時會使用UTF-16編碼。在轉碼的時候會在前面加上表示位元組順序的字元,這個字元稱為”零寬度非換行空格”(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。FEFF占用兩個位元組,是以就解釋了為什麼java環境下英文字母a在UTF-16編碼占3個位元組。