網路程式設計基礎之TCP程式設計
技術標籤:javajavahttp網路tcpipsocket
網路程式設計是開發中經常需要用到的一個點,也是面試中必考題,本節對網路程式設計中常用知識點進行簡要概述。
原文連結網路程式設計基礎(上)
網路程式設計基礎之UDP程式設計請看UDP
以下知識點均來源於廖雪峰官方網站https://www.liaoxuefeng.com/wiki/1252599548343744/1323711850348577
網路模型
計算機網路
計算機網路是指兩臺或更多的計算機組成的網路,在同一個網路中,任意兩臺計算機都可以直接通訊,因為所有計算機都需要遵循同一種網路協議。
那什麼是網際網路呢?網際網路是網路的網路(internet),即把很多計算機網路連線起來,形成一個全球統一的網際網路。
網路模型
由於計算機網路從底層的傳輸到高層的軟體設計十分複雜,要合理地設計計算機網路模型,必須採用分層模型,每一層負責處理自己的操作。OSI(Open System Interconnect) 網路模型是ISO組織定義的一個計算機互聯的標準模型,注意它只是一個定義,目的是為了簡化網路各層的操作,提供標準介面便於實現和維護。這個模型從上到下依次是:
- 應用層,提供應用程式之間的通訊;
- 表示層:處理資料格式,加解密等等;
- 會話層:負責建立和維護會話;
- 傳輸層:負責提供端到端的可靠傳輸;
- 網路層:負責根據目標地址選擇路由來傳輸資料;
- 鏈路層和物理層負責把資料進行分片並且真正通過物理網路傳輸,例如,無線網、光纖等。
TCP/IP 模型
網際網路實際使用的TCP/IP模型並不是對應到OSI的7層模型,而是大致對應OSI的5層模型:
OSI | TCP/IP |
---|---|
應用層 | 應用層 |
表示層 | |
會話層 | |
傳輸層 | 傳輸層 |
網路層 | IP層 |
鏈路層 | 網路介面 |
物理層 | 物理 |
小結
- 計算機網路:由兩臺或更多計算機組成的網路;
- 網際網路:連線網路的網路;
- IP地址:計算機的網路介面(通常是網絡卡)在網路中的唯一標識;
- 閘道器:負責連線多個網路,並在多個網路之間轉發資料的計算機,通常是路由器或交換機;
- 網路協議:網際網路使用TCP/IP協議,它泛指網際網路協議簇;
- IP協議:一種分組交換傳輸協議;
- TCP協議:一種面向連線,可靠傳輸的協議;
- UDP協議:一種無連線,不可靠傳輸的協議。
TCP 程式設計
Socket
在開發網路應用程式的時候,我們又會遇到Socket這個概念。Socket是一個抽象概念,一個應用程式通過一個Socket來建立一個遠端連線,而Socket內部通過TCP/IP協議把資料傳輸到網路:
Socket、TCP和部分IP的功能都是由作業系統提供的,不同的程式語言只是提供了對作業系統呼叫的簡單的封裝。例如,Java提供的幾個Socket相關的類就封裝了作業系統提供的介面。
為什麼需要Socket進行網路通訊?因為僅僅通過IP地址進行通訊是不夠的,同一臺計算機同一時間會執行多個網路應用程式,例如瀏覽器、QQ、郵件客戶端等。當作業系統接收到一個數據包的時候,如果只有IP地址,它沒法判斷應該發給哪個應用程式,所以,作業系統抽象出Socket介面,每個應用程式需要各自對應到不同的Socket,資料包才能根據Socket正確地發到對應的應用程式。
一個Socket就是由IP地址和埠號(範圍是0~65535)組成,可以把Socket簡單理解為IP地址加埠號。埠號總是由作業系統分配,它是一個0~65535之間的數字,其中,小於1024的埠屬於特權埠,需要管理員許可權,大於1024的埠可以由任意使用者的應用程式開啟。
使用Socket進行網路程式設計時,本質上就是兩個程序之間的網路通訊。其中一個程序必須充當伺服器端,它會主動監聽某個指定的埠,另一個程序必須充當客戶端,它必須主動連線伺服器的IP地址和指定埠,如果連線成功,伺服器端和客戶端就成功地建立了一個TCP連線,雙方後續就可以隨時傳送和接收資料。
因此,當Socket連線成功地在伺服器端和客戶端之間建立後:
- 對伺服器端來說,它的Socket是指定的IP地址和指定的埠號;
- 對客戶端來說,它的Socket是它所在計算機的IP地址和一個由作業系統分配的隨機埠號。
服務端實現
要使用Socket程式設計,我們首先要編寫伺服器端程式。Java標準庫提供了ServerSocket來實現對指定IP和指定埠的監聽。ServerSocket的典型實現程式碼如下:
/**
* @Auther Mario
* @Date 2020-12-30 12:38
* @Version 1.0
* Socket 通訊程式設計之TCP
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666);
System.out.println("服務埠正在監聽");
for(;;){
//處理每個新的請求
Socket sock = ss.accept();
System.out.println("已經接受客服端請求");
//每個請求分配一個執行緒處理
Thread t = new Handle(sock);
t.start();
}
}
}
class Handle extends Thread{
private Socket socket;
public Handle(Socket socket) {
this.socket = socket;
}
public void run(){
try(InputStream inputStream = this.socket.getInputStream()){
try(OutputStream outputStream = this.socket.getOutputStream()){
handle(inputStream,outputStream);
}
}catch (Exception e){
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
System.out.println("client disconnected");
}
}
private void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
bufferedWriter.write("hello\n");
bufferedWriter.flush();
for(;;){
//讀取請求的每一行資料,直到關鍵字'bye',傳送資料
String read = bufferedReader.readLine();
if(read.equals("bye\n")){
bufferedWriter.write("bye\n");
bufferedWriter.flush();
break;
}
bufferedWriter.write("ok: " + read + "bye\n");
bufferedWriter.flush();
}
}
}
伺服器端通過程式碼:
ServerSocket ss = new ServerSocket(6666);
在指定埠「6666」監聽。這裡我們沒有指定IP地址,表示在計算機的所有網路介面上進行監聽。
如果ServerSocket監聽成功,我們就使用一個無限迴圈來處理客戶端的連線:
for (;;) {
Socket sock = ss.accept();
Thread t = new Handler(sock);
t.start();
}
注意到程式碼ss.accept()表示每當有新的客戶端連線進來後,就返回一個Socket例項,這個Socket例項就是用來和剛連線的客戶端進行通訊的。由於客戶端很多,要實現併發處理,我們就必須為每個新的Socket建立一個新執行緒來處理,這樣,主執行緒的作用就是接收新的連線,每當收到新連線後,就建立一個新執行緒進行處理。
我們在多執行緒程式設計的章節中介紹過執行緒池,這裡也完全可以利用執行緒池來處理客戶端連線,能大大提高執行效率。
如果沒有客戶端連線進來,accept()方法會阻塞並一直等待。如果有多個客戶端同時連線進來,ServerSocket會把連線扔到佇列裡,然後一個一個處理。對於Java程式而言,只需要通過迴圈不斷呼叫accept()就可以獲取新的連線。
客服端實現
/**
* @Auther mashang
* @Date 2020-12-30 14:04
* @Version 1.0
* Socket 通訊程式設計 TCP
*/
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",6666);
// 用於讀取網路資料:
try(InputStream inputStream = socket.getInputStream()){
// 用於寫入網路資料:
try(OutputStream outputStream = socket.getOutputStream()){
handle(inputStream,outputStream);
}
}
socket.close();
System.out.println("disconnected");
}
private static void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server]: " + bufferedReader.readLine());
for(;;){
System.out.println(">>>");
String s = scanner.nextLine();
bufferedWriter.write(s);
bufferedWriter.newLine();
bufferedWriter.flush();
String resp = bufferedReader.readLine();
System.out.println("<<<" + resp);
if(resp.equals("bye")){
break;
}
}
}
}
客戶端程式通過:
Socket sock = new Socket("localhost", 6666);
連線到伺服器端,注意上述程式碼的伺服器地址是**“localhost”** ,表示本機地址,埠號是「6666」。如果連線成功,將返回一個Socket例項,用於後續通訊。
自我總結:通過以上對服務端和客服端簡要實現得知,服務端在6666埠一直處於監聽狀態,等待客服端連線,收到客戶端請求後新建一個Handle執行緒處理請求,該執行緒主要處理服務端通過socket流進行網路通訊,inputStream從客戶端接收的輸入流,outputStream發給客戶端的輸出流,handle()方法處理輸入輸出流,在通訊結束後關閉socket連線。handle()把inputStream和outputStream分別包裝為bufferedReader和bufferedWriter,建立連線後服務端首先發送"hello"給客戶端bufferedWriter.write(“hello\n”),flush()用於強制把緩衝區資料傳送出去,然後一直等待客戶端傳送資料,進而讀取資料,回覆資訊。客戶端則連線服務端埠建立通訊,通過socket傳遞資料,從輸入臺輸入資料,flush()傳送資料,通過bufferedReader.readLine()讀取資料,直到“bye”完成通訊,斷開連線。
Socket流
當Socket連線建立成功後,無論是伺服器端,還是客戶端,我們都使用Socket例項進行網路通訊。因為TCP是一種基於流的協議,因此,Java標準庫使用InputStream和OutputStream來封裝Socket的資料流,這樣我們使用Socket的流,和普通IO流類似:
// 用於讀取網路資料:
InputStream in = sock.getInputStream();
// 用於寫入網路資料:
OutputStream out = sock.getOutputStream();
最後我們重點來看看,為什麼寫入網路資料時,要呼叫flush()方法。
如果不呼叫flush(),我們很可能會發現,客戶端和伺服器都收不到資料,這並不是Java標準庫的設計問題,而是我們以流的形式寫入資料的時候,並不是一寫入就立刻傳送到網路,而是先寫入記憶體緩衝區,直到緩衝區滿了以後,才會一次性真正傳送到網路,這樣設計的目的是為了提高傳輸效率。如果緩衝區的資料很少,而我們又想強制把這些資料傳送到網路,就必須呼叫flush()強制把緩衝區資料傳送出去。
小結
使用Java進行TCP程式設計時,需要使用Socket模型:
- 伺服器端用ServerSocket監聽指定埠;
- 客戶端使用Socket(InetAddress, port)連線伺服器;
- 伺服器端用accept()接收連線並返回Socket;
- 雙方通過Socket開啟InputStream/OutputStream讀寫資料;
- 伺服器端通常使用多執行緒同時處理多個客戶端連線,利用執行緒池可大幅提升效率;
- flush()用於強制輸出緩衝區到網路。