目錄
一. TCP API?
二. TCP回顯服務器-客戶端
1. 服務器
2. 客戶端
3. 服務端-客戶端工作流程
4. 服務器優化
TCP數據流套接字編程是一種基于有連接協議的網絡通信方式
一. TCP API?
在TCP編程中,主要使用兩個核心類ServerSocket?和 Socket
ServerSocket
ServerSocket類只有服務器會使用,用于接受連接
ServerSocket構造方法:
方法簽名 | 方法說明 |
---|---|
ServerSocket(int port) | 創建一個服務端流套接字Socket,并綁定到指定端口 |
?ServerSocket方法:
方法簽名 | 方法說明 |
---|---|
Socket accept() | 監聽端口,如果有客戶端連接后,則會返回一個服務端 Socket 對象 如果沒有客戶端連接,則會進入阻塞等待 |
void close() | 關閉此套接字 |
- ?accept()方法用于接收客戶端連接請求并建立正式通信通道
- ?accept()方法是接受連接并返回Socket,真正和客戶端進行交互的是Socket
?Socket
?Socket類負責具體的數據傳輸?
- 客戶端一開始就使用Socket進行通信(請求由客戶端發起)
- 服務器在接受客戶端建立請求后,返回服務端Socket
- 在雙方建立連接之后,都會使用Socket進行通信?
Socket 構造方法:
方法簽名 | 方法說明 |
---|---|
Socket(String host, int port) | 創建一個客戶端流套接字Socket,并與對應IP的主機上,對應端口的進程建立連接 |
?Socket 方法:
方法簽名 | 方法說明 |
---|---|
InetAddress getInetAddress() | 返回的地址(IP和端口) |
InputStream getInputStream() | 返回輸入流 |
OutputStream getOutputStream() | 返回輸出流 |
- TCP面向字節流,基本傳輸單位是字節
二. TCP回顯服務器-客戶端
回顯服務器
回顯服務器:不進行任何的業務邏輯,只是將收到的數據顯示出來
1. 服務器
??接收連接請求?
- TCP是有連接的可靠通信
- 真正建立連接的過程在內核中被實現,應用層只是調用相應API同意建立連接
- 類比打電話,客戶端撥號,服務器這邊在響鈴,通過調用accept接聽
?代碼實現:
Socket clientSocket = serverSocket.accept();
- accept()方法具有阻塞功能
- accept()方法一次只能返回一個Socket對象,接收一次請求
- 如果沒有客戶端發起連接請求,則會進入阻塞等待
- 如果有一個客戶端發起連接請求,則執行一次,如果有多個客戶端發起連接請求,則執行多次
處理請求
private void processConnection(Socket clientSocket) {}
- ?使用方法專門處理一次連接,在一次連接中可能會涉及多次請求響應交互
如何處理請求和返回響應??
由于TCP面向字節流,我們可以字節流傳輸的類 InputStream和OutputStream
try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){} catch (IOException e) {throw new RuntimeException(e);}
- ?使用try-with-resources管理InputStream和OutputStream,確保流自動關閉。?
- InputStream從網卡中讀取數據
- OutputStream從網卡中寫入數據
1)接收請求并解析
Scanner scanner = new Scanner(inputStream);if(!scanner.hasNext()){System.out.printf("[客戶端ip:%s,端口號:%d],客戶端下線\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}String request = scanner.next();
- 客戶端和服務器雙方都有自己的緩沖區
- 客戶端發送數據,會先將數據放入服務器緩沖區中
- 如果服務器緩沖區中沒有數據,hasNext()則會陷入阻塞等待中
- 如果客戶端退出,則會觸發四次揮手斷開連接,服務器會感知到,就會在hasNext()返回false。
2)根據請求計算響應
?回顯服務器:不會處理數據,輸入什么就會返回什么
? ? ? ? ? ? String response = process(request);
使用process方法來實現回顯功能
? ? public ?String process(String request) {return request;}
?如果想要實現特定的功能,直接在process中實現即可?
3)返回響應
?Scanner的寫操作無法自己完成,只能進行讀取操作,寫操作需要依靠其他的類(PrintWriter)?
PrintWriter printWriter = new PrintWriter(outputStream);
// 將數據寫入數據的緩沖區中printWriter.println(respond);
沖刷緩沖區
?由于緩沖區的特殊機制,緩沖區只有滿的時候,才會被發送出去
printWriter.flush();
- 我們這里要保證實時性,客戶端每發送一次請求,服務器都要第一時間響應
- IO操作比較低效,如果每進行一次IO,就要沖刷一次,效率很低,為了讓這種低效的操作少一點,等緩沖區滿了,才會沖刷
服務器總代碼:
import java.io.*;
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 = null;TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服務器啟動");while(true){//從緩沖區內取出并同意鏈接//將取出的數據使用clientSocket另外保存起來,//每有一個客戶端,就會出現一個clientSocket對象,所有使用完,必須關閉Socket clientSocket = serverSocket.accept();//進行數據分析/* Thread t = new Thread(()->{processConnection(clientSocket);});t.start();*/
// 這樣寫開銷大,會有很多次的創建和銷毀,改進使用線程池ExecutorService service = Executors.newFixedThreadPool(3);service.submit(()->{processConnection(clientSocket);});}}//使用這個方法專門處理一次連接,在一次連接中可能會涉及多次請求交互private void processConnection(Socket clientSocket) {System.out.printf("[客戶端ip:%s,端口號:%d],客戶端上線\n",clientSocket.getInetAddress(),clientSocket.getPort());//循環處理請求并返回響應(請求可能不止一次)//從網卡中讀數據和寫數據try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){while (true){
// byte[] buffer = new byte[1024];
// int n = inputStream.read(buffer);
// //將字節數組轉換為字符串
// if(n==-1){
// System.out.printf("[客戶端ip:%s,端口號:%d],客戶端下線",clientSocket.getInetAddress(),clientSocket.getPort());
// break;
// }
// String request = new String(buffer,0,n);Scanner scanner = new Scanner(inputStream);if(!scanner.hasNext()){System.out.printf("[客戶端ip:%s,端口號:%d],客戶端下線\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}
// 1.接受請求并解析//客戶端必須有一個空格或者換行符String request = scanner.next();
// 2.根據請求計算響應String respond = process(request);
// 3.返回響應//返回的是字節數組類型
// outputStream.write(request.getBytes(),0,request.getBytes().length);//返回字符串類型(各種類型)PrintWriter printWriter = new PrintWriter(outputStream);
// 將數據寫入數據的緩沖區中printWriter.println(respond);//沖刷緩沖區printWriter.flush();//打印日志System.out.printf("[客戶端ip:%s,端口號:%d],req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,respond);}} catch (IOException e) {throw new RuntimeException(e);}finally {try {//必須進行close,clientSocket.close();} catch (IOException e) {throw new RuntimeException(e);}}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(9090);server.start();}
}
注意:?
- 在服務器中,ServerSocket對象不需要被消耗,整個程序中只有一個ServerSocket對象,它的生命周期要伴隨整個程序,不能提前關閉,只有程序退出了,才會被釋放
- 方法中的Socket必須要釋放,每出現一個客戶端,就會隨之出現一個Socket對象,如果不釋放,Socket對象會越來越多,將文件描述符表占滿(內存泄露問題)
2. 客戶端
?構造方法
TcpEchoClient(String serverIp,int serverPort) throws IOException {socket = new Socket(serverIp,serverPort);}
- 這里不需要將serverIP和serverPort在類中保存
- 因為tcp有鏈接,socket會保存好這兩個值
客戶端如何發送請求和接收響應?
客戶端同樣使用字節流進行傳輸
try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){} catch (IOException e) {throw new RuntimeException(e);}
- ?使用try-with-resources管理InputStream和OutputStream,確保流自動關閉。?
- InputStream從網卡中讀取數據
- OutputStream從網卡中寫入數據
1)從控制臺讀取請求
//客戶端輸入的數據Scanner scannerConsole = new Scanner(System.in);while(true){System.out.print("->");//客戶端沒有輸入if(!scannerConsole.hasNext()){break;}
// 從控制臺讀取請求String request = scannerConsole.next();}
- 使用Scanner進行輸入,如果沒有輸入數據,hasNext()會進入阻塞等待
2)將請求發送給服務器
?Scanner只會讀取數據,發送使用類PrintWriter
PrintWriter writer = new PrintWriter(outputStream);writer.println(request);
- ?向服務器發送數據
沖刷緩沖區?
//沖刷緩沖區writer.flush();
將發送的數據,先放入緩沖區中,等待緩沖區滿了,才會發送緩沖區中的內容?
3)接收服務器返回的響應
Scanner scannerNetwork = new Scanner(inputStream);String respond = scannerNetwork.next();
- 服務器發送的數據,先到達客戶端的緩沖區,客戶端要從緩沖區讀出數據
- 這里使用Scanner進行讀出數據,也可以使用read()方法讀取?
4)將響應數據顯示在控制臺
System.out.println(respond);
將接收到的字符串響應,直接打印出來即可?
客戶端總代碼:
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 = null;TcpEchoClient(String serverIp,int serverPort) throws IOException {
// 這里不需要將serverIP和serverPort在類中保存
// 因為tcp有鏈接,socket會保存好這兩個值socket = new Socket(serverIp,serverPort);}public void start(){System.out.println("客戶端啟動");try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){//客戶端輸入的數據Scanner scannerConsole = new Scanner(System.in);//通過網絡讀取Scanner scannerNetwork = new Scanner(inputStream);//像服務器發送請求PrintWriter writer = new PrintWriter(outputStream);while(true){System.out.print("->");//客戶端沒有輸入if(!scannerConsole.hasNext()){break;}
// 1. 從控制臺讀取請求String request = scannerConsole.next();
// 2.將請求發送給服務端writer.println(request);//沖刷緩沖區writer.flush();
// 3.接受服務端返回的響應//從數據緩沖區中讀取出內容String respond = scannerNetwork.next();
// 4.將響應顯示在控制臺System.out.println(respond);}} 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();}
}
3. 服務端-客戶端工作流程
?無論是TCP還是UDP都是服務端先啟動?
創建連接過程
- 服務器啟動,由于沒有客戶端建立連接,accept()進入阻塞,等待客戶端創建連接
- 客戶端啟動,客戶端申請和服務器建立連接
- 服務器從accept()阻塞中返回,調用processConnection()方法進行交互
?雙方交互過程
- 服務器進入processConnection()方法,執行到hasNext(),由于客戶端沒有發送數據,服務器讀取不到數據,進入阻塞狀態
- 客戶端在hasNext()這里進入阻塞,等待用戶在控制臺中輸入數據
- 用戶輸入數據,客戶端從hasNext()中退出阻塞,將數據發送給服務器,next()阻塞等待服務器返回數據
- 服務器從hasNext()阻塞中返回,讀取請求并處理,構造響應,發送給客戶端
- 客戶端讀取響應并打印
4. 服務器優化
Thread t = new Thread(()->{processConnection(clientSocket);});t.start();
- 每來一個客戶端,服務器就需要創建出一個新的線程
- 每次客戶端結束,服務器就需要銷毀這個線程
如果客戶端比較多,那么服務器就需要頻繁的創建和銷毀 ,開銷大
?(1)可以通過引入線程池來避免頻繁的創建和銷毀
ExecutorService service = Executors.newFixedThreadPool(3);service.submit(()->{processConnection(clientSocket);});
?如果有的客戶端處理的過程很短(網站),也有可能客戶端處理的時間會很長
處理時間很短的客戶端,分配一個專門的線程,有點浪費,所有引入了IO多路復用技術
(2)IO多路復用技術
IO多路復用技術是操作系統提供的機制。?
讓一個線程去同時去負責處理多個Socket對象
本質在于這些Socket對象不是同一時刻都需要處理?
雖然有多個Socket對象,但是同一時間活躍的Socket對象只是少數(大部分的Socket對象都是在等數據),我們可以在等的過程中,去處理活躍的Socket對象?
點贊的寶子今晚自動觸發「躺贏錦鯉」buff!?