Socket 套接字
Socket套接字,是由系統提供用于網絡通信的技術,是基于 TCP/IP 協議的網絡通信的基本單元。基于 Socket 套接字的網絡程序開發就是網絡編程。
應用層會調用操作系統提供的一組 api ,這組 api 就是 socket api(傳輸層給應用層提供)
socket => 插槽 -> 主板上的一些接口
傳輸層有兩個核心協議:
TCP UDP 由于這兩個協議差別非常大,編寫代碼的時候,也是不同的風格。所以,socket api 提供了兩套~
TCP的特點:有鏈接 , 可靠傳輸 ,面向字節流 , 全雙工
UDP的特點:無連接 ,不可靠傳輸 , 面向數據報 , 全雙工
有鏈接/無連接:
有鏈接/無連接 是抽象的概念,虛擬的/邏輯上的鏈接。
要進行網絡通信,物理上的鏈接(網線什么的)
對于 TCP 來說,TCP 協議中,就保存了對端的信息(A和B 通信,A和B先建立鏈接,讓A保存B的信息,B保存A的信息(彼此之間知道,誰是和它建立連接的那個))
對于 UDP 來說, UDP 協議本身,不保存對方的信息 ——就是無連接 (當然可以在自己的代碼中寫變量,保存對方的信息,但這不是 UDP 的行為)
可靠傳輸 VS 不可靠傳輸
網絡上,數據是非常容易出現丟失的情況(丟包)。像光信號/電信號,都可能受到外界的干擾。(比如本來是傳輸 0101,其中有些 bit 為就被修改了,這樣亂了的數據就會被識別出來,把這樣的數據給丟掉)
并且網絡世界 是通過路由器/交換機交織起來了,路由器/交換機就類似于“十字路口”,因此就會發生“堵車”,在某個時間點,實際需要轉發的數據超過了設備轉發的上限。
所以,我們不能指望,一個數據包發送之后,100%到達對方。
可靠傳輸的意思,不是保證數據包100%到達,而是盡可能的提高傳輸成功的概率,如果出現丟包了,是能夠感知到的。
不可傳輸的意思,只是把數據發了,就不管了。
可能我們只管感覺,可靠傳輸更好,但是可靠傳輸也是要付出代價的 —— 他的效率是遠不如不可靠傳輸的。所以也是要分情況使用的。
面向字節流 VS 面向數據報
面向字節流,讀寫數據的時候,是以字節為單位 => 支持任意長度,但會導致粘包問題。
面向數據報,讀寫數據的時候,是以一個數據報為單位(不是字符) => 一次必須讀寫一個 UDP 數據報,不能是半個,所以不存在粘包,但長度受到限制
全雙工 VS 半雙工
全雙工:一個通信鏈路,支持 雙向通信 (類似能讀,也能寫)
半雙工: 一個通信鏈路,只支持單向通信(要么讀,要么寫)
計算機中的“文件”通常是一個“廣義的概念”,文件還能代指一些硬件設備(操作系統管理硬件設備,也是抽象成文件,統一管理的)。
網卡 => socket文件
操作網卡的時候,流程和操作普通文件差不多,也是 打開(也會在文件描述符表中分配一個表項) -> 讀寫 -> 關閉
操作網卡,直接操作不好操作,所以就把網卡轉換成操作 socket 文件,socket 文件就相當于“網卡的遙控器”。
那么接下來我們就要進行 socket api 進行網絡編程了,本身也是操作系統的功能。
UDP數據報套接字編程
API介紹
DatagramSocket
DatagramSocket 是 UDP Socket,用與發送和接收UDP數據報。
DatagramSocket的構造方法:(相當于打開文件)
port:端口號
創建Socket的時候,就會關聯上一個 端口號,使用端口號來區分主機上不同的應用程序。
DatagramSocket的一些關鍵方法:
DatagramPacket
DatagramPacket 是UDP Socket 發送和接受的數據報。
DatagramPacket的構造方法:
DatagramPacket的關鍵方法:
構造UDP發送的數據報時,需要傳入 SocketAddress,該對象可以使用 InetSocketAddress 來創建。
下來我們來創建一個回顯服務器和客戶端(簡易):
Echo:回聲
回顯服務器:客戶端給服務器發一個數據(請求),服務器返回一個數據(響應),請求是什么,響應就是什么。
不過真實的服務器,請求和響應是不一樣的。
回顯服務器,處理請求的關鍵步驟:
- 1.接收請求并解析
- 2.根據請求,計算響應(最關鍵的步驟)
- 3.發送響應給客戶端
- 4.創建日志
此處有一個問題是 socket 不用close 嗎?
文件要關閉,要考慮清除這個文件對象的生命周期是怎么樣的,此處的 socket 對象,伴隨整個 Udp 服務器,自始至終。
如果服務器關閉(進程結束),進程結束時就會自動釋放PCB的文件描述表中的所有資源,也不需要手動調用 close 了。
還有一個問題:當前服務器啟動之后,客戶端還沒有,當然也沒有請求發送,那么在客戶端請求過來之前, 服務器里面的邏輯都在干什么呢?
receive 會觸發阻塞行為。客戶端請求發來了,receive 才會返回,客戶端的請求沒來,receive 就一直阻塞了。
創建客戶端的重要步驟:
- 1.從控制臺讀取用戶輸入的信息
- 2.將請求發送給服務器
- 3.接收服務器的響應
- 4.從服務器讀取的數據進行解析,并打印
不過有一個不同的是,客戶端的構造方法需要指定訪問的服務器的地址和端口號
在此基礎上,我們可以編寫一個漢譯英服務器。
我們只需要重寫 process 方法。
TCP套接字編程
TCP的一個核心特點,面向字節流。讀寫數據的基本單位就是字節byte。
API介紹
ServerSocket:
ServerSocket是創建 TCP服務端Socket 的API。(專門給服務端用的)
構造方法:
服務器啟動,需要先綁定一個端口號。
關鍵方法:
TCP是“有連接”,這里的 accept 聯通連接的關鍵操作。
Socket:
Socket 是客戶端Socket,或服務器中接收到客戶端建立連接(accept方法)的請求后,返回的服務端Socket。
不管是客戶端還是服務端Socket,都是雙方建立連接以后,保存的對端信息,以及用來與對方收發數據的。
構造方法:
host 和 port 是服務器的 IP 和服務器的 端口,不是客戶端自己的。
關鍵方法:(注意返回值)
那接下來我們就通過 TCP 來實現回顯服務器和客戶端的簡單通信吧~~
大致思路是一樣的,但還是有一些不同的。
服務器:
客戶端:
這里面的 Scanner 和 PrintWriter 是針對 InputStream 和 OutputStream 套了一層。上述,我們其實已經完成輸入輸出?作,但總是有所不?便,所以將InputStream 和 OutputStream 處理下,使?Scanner 和 PrintWriter 類來完成輸入輸出,因為PrintWriter 類中提供了我們熟悉的 print/println/printf ?法。
Scanner 這里的構造方法:
PrintWriter這里的構造方法:
上述代碼是我們根據 UDP 的思路來寫出來的。雖然看起來沒啥問題,但是運行的時候,我們會發現,雖然服務器日志這邊顯示出來客戶端已經建立連接了,但是在客戶端發送請求之后,沒有響應答打印出來。這是為什么呢?
這是因為這個println方法只是把數據放到“發送緩沖區(內存空間)”中,還沒有真正寫入到網卡中。
解決辦法就是 使用 flush 方法來“沖刷緩沖區”。這是PrintWriter 的行為,如果不套殼,是可以直接發送的,但是在實際開發中,廣泛使用緩沖區這樣的概念,flush 這個操作是很關鍵的。
加了flush就可以了。
但是還有一個問題是,一個服務器可能要同時給多個客戶端提供服務,在服務器建立連接的代碼上我們寫了個while循環,代表的意思是可以建立多個連接。在IDEA上面默認的是一個程序只能啟動一邊,再次啟動還是該程序。通過設置,我們可以連續創建多個客戶端。
通過在不同的客戶端發送請求,我們會發現只有第一個客戶端的請求能得到響應,但是其他的客戶端沒有得到響應。
但是,把當前得到響應的客戶端關閉之后,下一個客戶端就會得到響應了,這是為什么呢?
在服務器中
如果只是單個線程,無法同時響應多個客戶端。此處應該給每個客戶端都分配一個線程,所以多線程的誕生,就是為了這個場景服務的。
除了引入多線程之外,為了避免頻繁創建銷毀線程,也可以引入線程池。
如果客戶端進一步增加,此時多線程/線程池,就會產生出大量的線程。操作系統中內置了IO多路復用,其本質上是一個線程同時負責多個客戶端的請求。
舉個例子:
比如我和朋友要去小吃街吃飯,他想吃煎餅果子,我想吃肉夾饃。此時有三個方案:
方案一:
我先買煎餅果子,再去買肉夾饃。(單線程)
方案二:
我和朋友一起出發,他買煎餅果子,我買肉夾饃。(多線程)
方案三:
我先到煎餅果子這邊,給老板說:“老板,來個煎餅果子,我一會過來拿”,然后又到肉夾饃那邊,給老板說:“老板來個肉夾饃,我一會過來拿”,然后我就開始等,在這等的過程中,這兩個小攤的老板在同時工作。等他們做好的時候,就會過來叫我。所以我只是用了一份等待的時間,同時等待兩個任務的完成。
(IO多路復用)
多個客戶端,對應著多個老板,每個客戶端,絕大部分時間是沉默的,工作線程只需要等待,等到客戶端發來數據的時候,線程再來處理就可以了,多個客戶端同時發數據的概率比較小。
IO多路復用是當前開發服務器的主流的核心技術,操作系統內置的,只需要調用api即可,Java 通過 NIO 來封裝了 IO 多路復用。