TCP 和 UDP 的區別
在傳輸層,TCP 協議是有連接的,可靠傳輸,面向字節流,全雙工
而UDP 協議是無連接的,不可靠傳輸,面向數據報,全雙工
有連接和無連接的區別是在進行網絡通信的時候,通信雙方有沒有保存對端的地址信息,即假設 A 和 B 進行通信,A 保存了 B 的地址信息,B 也保存了 A 的地址信息,此時雙方都知道和誰建立了連接,這就是有連接的通信,在之前的 UDP 數據報套接字編程中就提到過 UDP 是無連接的,所以在發送數據報的時候要加上對端的信息,防止丟包。
可靠傳輸是通過各種手段來防止丟包的出現,而不可靠傳輸則沒有做任何處理直接把數據報傳輸過去,但是可靠傳輸不意味著能 100% 把數據報完整無誤地傳輸給對方,只是盡可能降低丟包發生的概率,并且可靠傳輸是要使用很多手段來保持的,所以付出的代價相比于不可靠傳輸要大。
面向字節流就是以字節為單位來進行數據的傳輸,面向數據報就是以數據報為單位進行數據的傳輸。
全雙工就是通信的雙發可以同時給對方發送數據,但是半雙工是指雙方只有一方可以發送數據。
TCP流套接字 API 介紹
ServerSocket
ServerSocket 是TCP服務端Socket 的API
構造方法:
方法名 | 說明 |
---|---|
ServerSocket(int port) | 創建一個TCP服務端流套接字Socket,并綁定端口號 |
ServerSocket 方法:
方法名 | 返回值 | 說明 |
---|---|---|
accept() | Socket | 開始監聽指定端口(創建時綁定的端口),有客戶端連接后,返回一個服務端Socket 對象,并基于該Socket 建立于客戶端的連接,否則阻塞等待 |
close() | void | 關閉此套接字 |
Socket
Socket 是客戶端Socket 或者是 服務端那邊收到客戶端建立連接的請求(通過 accept() 方法)返回的Socket 對象。
不管是客戶端還是服務端的Socket 對象,他們都保留了對端的地址信息,這也是TCP協議有連接的體現。
Socket 構造方法:
方法名 | 說明 |
---|---|
Socket(String host, int port) | 創建一個客戶端流套接字Socket,并于對應IP 的主機對應的端口的進程建立連接 |
Socket 方法:
方法名 | 返回值 | 說明 |
---|---|---|
getInetAddress() | InetAddress | 返回套接字所連接的地址 |
getInputStream() | InputStream | 返回此套接字的輸入流 |
getOutputStream() | OutputStream | 返回此套接字的輸出流 |
回顯服務器
首先在回顯服務器的構造方法里初始化我們的ServerSocket
public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}
然后就是服務器啟動運行的代碼了:在面對多個客戶端的時候,我們可以使用線程池來進行處理。
這里使用Executors.newCachedThreadPool()
是不固定線程的個數的線程池,這樣可以靈活地處理多個客戶端的請求。
public void start() throws IOException {System.out.println("服務器啟動...");ExecutorService executorService = Executors.newCachedThreadPool();while(true) {//與客戶端建立連接Socket clientSocket = serverSocket.accept();//處理客戶端發出的多個請求executorService.submit(() -> {try {processClient(clientSocket);} catch (IOException e) {throw new RuntimeException(e);}});}}
處理請求
我們通過了一個方法processClient
來封裝了處理請求的邏輯
如何進行數據的獲取和寫入操作?
可以通過輸入流和輸出流來處理getInputStream
和getOutputStream
try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream())
為了更加方便地使用這兩個流對象,我們進行了進一步的封裝:
//對輸入流和輸出流進行進一步的封裝,方便我們的使用
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
由于客戶端可能發來的不止一個請求,我們可以使用循環來處理一下,在循環體中,我們處理請求有三個步驟,首先獲取請求解析請求,然后計算響應,最后發送響應
while(true) {if(!scanner.hasNext()) {System.out.printf("[%s:%d] 客戶端下線\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}//解析請求String request = scanner.next();//計算響應String response = process(request);//發送響應writer.println(response);//因為此時的響應數據還在緩存區里,所以需要使用 flush 來將內存的數據發送出去writer.flush();System.out.printf("[%s:%d] request:%s response:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}
由于這里是回顯服務器,所以計算響應的代碼是直接返回字符串就可以了
private String process(String request) {return request;}
最后當客戶端沒有請求的時候,我們需要斷開此次連接,釋放資源,避免資源的泄漏
finally {//當請求處理完的時候記得關閉服務器與客戶端的連接,防止資源泄漏clientSocket.close();}
最終代碼
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TcpEchoServer {private ServerSocket serverSocket;public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服務器啟動...");ExecutorService executorService = Executors.newCachedThreadPool();while(true) {//與客戶端建立連接Socket clientSocket = serverSocket.accept();//處理客戶端發出的多個請求executorService.submit(() -> {try {processClient(clientSocket);} catch (IOException e) {throw new RuntimeException(e);}});}}private void processClient(Socket clientSocket) throws IOException {//獲取輸入流和輸出流try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {System.out.printf("[%s:%d] 客戶端上線\n",clientSocket.getInetAddress(),clientSocket.getPort());//對輸入流和輸出流進行進一步的封裝,方便我們的使用Scanner scanner = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);while(true) {if(!scanner.hasNext()) {System.out.printf("[%s:%d] 客戶端下線\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}//解析請求String request = scanner.next();//計算響應String response = process(request);//發送響應writer.println(response);//因為此時的響應數據還在緩存區里,所以需要使用 flush 來將內存的數據發送出去writer.flush();System.out.printf("[%s:%d] request:%s response:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}} catch (IOException e) {throw new RuntimeException(e);} finally {//當請求處理完的時候記得關閉服務器與客戶端的連接,防止資源泄漏clientSocket.close();}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(9090);server.start();}
}
客戶端
首先在客戶端構造方法建立于服務器的連接:
public TcpEchoClient(String serverIP, int port) throws IOException {//與服務器建立連接socket = new Socket(serverIP,port);}
運行邏輯
首先用戶從控制臺輸入數據,然后發送請求,接著等待服務器的響應并接收響應然后打印響應的內容即可。
public void start() {try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {//對輸入流和輸出流進行進一步的封裝Scanner scanner = new Scanner(System.in);Scanner scanner2 = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);while(true) {//發送多個請求和接收多個響應if(!scanner.hasNext()) {break;}//發送請求String request = scanner.next();writer.println(request);writer.flush();//接收響應String response = scanner2.next();System.out.println(response);}} catch (IOException e) {throw new RuntimeException(e);}}
這里要注意用戶通過控制臺輸入數據,我們要使用的是Scanner(System.in)
當我們要發送數據的時候是使用 Socket 的 getOutputStream 方法來獲取對應的輸出流對象,為了便于使用所以我們又使用 PrintWriter
來進一步封裝輸出流,來打印響應
在發送請求的時候我們需要使用 Socket 的 getInputStream 方法來獲得輸入流對象,為了方便使用,所以使用Scanner(inputStream)
進一步封裝。
最終代碼
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;public class TcpEchoClient {private Socket socket;public TcpEchoClient(String serverIP, int port) throws IOException {//與服務器建立連接socket = new Socket(serverIP,port);}public void start() {try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {//對輸入流和輸出流進行進一步的封裝Scanner scanner = new Scanner(System.in);Scanner scanner2 = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);while(true) {//發送多個請求和接收多個響應if(!scanner.hasNext()) {break;}//發送請求String request = scanner.next();writer.println(request);writer.flush();//接收響應String response = scanner2.next();System.out.println(response);}} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);client.start();}
}
細節說明
在我們使用PrintWriter 的 writer.println(xxx)
之后,我們的數據其實還保留在緩存區中,也就是還沒發出去,我們需要通過flush()
方法來刷新緩存區的數據,才能將數據真正發送到對端去。
我們不可以使用writer.print
這種沒有自動添加換行符的方法,因為我們在接收數據的時候,使用的是Scanner 的 next()
方法,next() 是要接收到空白符(包括換行符,制表符,翻頁符…)才停止接收的,如果你使用 print 來發送數據,這時候的數據是沒有帶任何空白符的,那么就不會停止接收數據而是繼續等待空白符的到來,這時候服務器就無法處理客戶端的請求:如下圖:
服務器就阻塞在 下圖標紅的代碼里:
客戶端被阻塞在接收響應的代碼里:
你在客戶端的控制臺輸入的回車不算進數據的換行符里,控制臺輸入的回車時,只是將數據交給了客戶端程序,并不會自動將這些數據轉換為網絡流中的換行符。
換一句話說,控制臺的回車只是結束你在控制臺的輸入,并不會自動在數據末尾加上換行符
效果展示