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模型
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZNUHg1We-1576226044889)(534D6FC6B67D46D59E708EDC5839995A)]


  1. 應用層:任務是通過應用進程間的交互來完成特定的網絡應用。
  2. 運輸層:任務是負責向兩個主機中進程之間的通信提供通用的數據傳輸服務。 應用層主要有兩種協議:
    *傳輸控制協議TCP——提供面向連接的、可靠地數據傳輸服務,其數據傳輸的單位是報文段。
    *用戶數據報協議UDP——-提供無連接的、盡最大努力交付的數據傳輸服務(不保證數據傳輸的可靠性),其數據傳輸的單位是用戶數據報。
  3. 網絡層:負責爲分組交換網上的不同主機提供通信服務。在發送數據的時候,網絡層把運輸層產生的報文段或用戶數據報封裝成分組或包進行傳送。分組也叫IP數據包或者數據報。所以,網絡層也是把數據封裝成數據報。
  4. 數據鏈路層:兩臺主機之間的數據傳輸,總是在一段一段的鏈路層上傳送的,這就需要專門的數據鏈路層協議。當兩個相鄰節點之間傳送數據時,數據鏈路層將網絡層上交下來的IP數據報組裝成幀。
  5. 物理層:也就是最底層,傳輸的數據是比特。

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

IP協議

TCP協議

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-whXidAH4-1576226044892)(4FBD0D83B32444B493EDE639730912B9)]

  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. 選項,長度可變,定義一些其他的可選的參數。

三次握手

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kaW4IQKZ-1576226044892)(5E7EAC04FFBF4ED6BB32893C0A862D51)]

  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狀態,此後雙方就可以開始通信了。

四次揮手
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-k5cfCSyy-1576226044893)(FECC1E89F134455DAD580D674C2C7EE0)]

  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層複雜的操作抽象爲幾個簡單的接口供應用層調用已實現進程在網絡中通信。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VoMg6Wo0-1576226044894)(04822B9DC38E4EE0B0294F7F10545149)]

如何使用
使用比較簡單,總共也沒幾個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();
		}
	}

先運行服務端,然後運行客戶端,運行結果:

可以說socket的使用還是很簡單的。就是ServiceSocket和Socket 2個類。而且方法也沒多少。
上面需要注意的是host這個是服務端的IP地址,查看自己電腦的IP地址 使用ipconfig
在這裏插入圖片描述

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

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

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

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5yc64vPY-1576226044895)(856734544907480AB0102D1E36C215C3)]

有何利弊

原理源碼

主要是通過SocketImpl實現。

使用場景

網絡請求

總結

  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();
		}

運行結果:
在這裏插入圖片描述

當然也可以用Post請求,這裏就不詳細說了。
在這裏插入圖片描述

有何利弊

原理源碼

通過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();
		}

	}

在這裏插入圖片描述

看到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個字節。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章