前言
本文主要介紹UDP和TCP相關的API,并且基于這兩套API實現回顯服務器
UDP和TCP
UDP和TCP屬于網絡五層模型中傳輸層的協議
特點:
UDP:無連接,不可靠,面向數據包,全雙工
TCP:有連接,可靠,面向字節流,全雙工
1.無連接和有連接
這里所說的連接不是指物理意義上的通過實物來進行綁定,而是虛擬的連接。舉個例子,你打電話給對方,對方接通之后你才能進行后續通信,這就是有連接;無連接就相當于QQ發消息,無論對方是否同意,消息都能發過去
2.可靠和不可靠
不論是哪種通信方式,實際上都無法保證數據一定能傳輸成功。所以,這里的可靠指的是,能獲取到數據的傳輸情況。即使傳輸失敗了我能知道它傳輸失敗了,重傳就是了;不可靠指的是信息傳輸之后就不管了,傳輸成功與否都和我無關,更不會重傳
3.全雙工和半雙工
全雙工通信允許通信的雙方可以同時發送和接收數據,半雙工通信同一時間內只能在同一方向上傳輸
什么是回顯服務器
回顯的意思是無論客戶端給服務器發送什么請求,服務器會把客戶端的請求原樣返回
1.UCP回顯服務器
1.1API介紹
Java中UDP協議的API有兩個,一個是DatagramSocket,一個是DatagramPacket
1.1.1DatagramSocket類
作用:用于應用程序之間發送和接收UDP數據報
構造方法:
//不指定端口號,由系統隨機分配
DatagramSocket()
//指定端口號
DatagramSocket(int port)
其他方法:
//接收一個數據報
receive(DatagramPacket p)
//發送一個數據報
send(DatagramPacket p)
1.1.2DatagramPacket類
作用:封裝UDP數據報的數據和目標地址信息。它包含要發送的數據,目的主機的端口號和IP地址
構造方法:
//字節數組,字節數組長度,服務器IP,服務器端口號
//這是傳入的IP地址和端口號是固定值,因為服務器的IP和端口號一般不變
//所以作為DatagramSocket類的send方法的參數,用于客戶端向服務器發送請求
DatagramPacket(byte buf[], int length,InetAddress address, int port)//字節數組,字節數組長度
//這個和上面有所不同,主要用于服務器向客戶端返回請求
//一會代碼再具體講解
DatagramPacket(byte buf[], int length, SocketAddress address)//字節數組,字節數組長度
//作為DatagramSocket類的receive方法的參數,用于客戶端接收服務器的響應 或者 用于服務器接收客戶端的請求
DatagramPacket(byte buf[], int length)
1.2UDP回顯服務器代碼
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class EchoSever {private final DatagramSocket socket;//傳入端口號public EchoSever(int port) throws SocketException {socket = new DatagramSocket(port);}//啟動服務器public void start() throws IOException {System.out.println("服務器啟動");//服務器死循環就行了,想要關閉服務器直接殺進程while (true) {//1.讀取請求并解析DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//receive從網卡讀取到UDP數據報,載荷部分放到byte數組里//UDP報頭存放在requestPacket其他屬性里//還能通過requestPacket知道源IP/端口//receive具有阻塞功能socket.receive(requestPacket);//將接收到的字節數組轉換為字符串//requestPacket.getLength()得到的長度是有效長度,不一定是4096String request = new String(requestPacket.getData(),0,requestPacket.getLength());//2.根據請求計算相應(回顯服務器啥都不用做)String response = process(request);//3.返回響應到客戶端//response.getBytes()得到String內部的字節數組//當String里面全都是英文字符的時候,response.length()是可以的//因為一個英文字母對于一個字節,但是一個漢字對應多個字節//requestPacket.getSocketAddress()找到對應客戶端的IP和端口號DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);//打印日志//IP,端口號,請求,響應System.out.printf("[%s:%d] request:%s,response:%s\n",requestPacket.getAddress().toString(),responsePacket.getPort(),request,response);}}//根據請求計算相應public String process(String request){return request;}//public static void main(String[] args) throws IOException {EchoSever sever = new EchoSever(9090);sever.start();}
}
注意1:DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,requestPacket.getSocketAddress());
因為requestPacket數據報保存了客戶端的IP地址和端口號,所以通過getSocketAddress()能獲取到客戶端的IP地址和端口號
//
其次,客戶端的IP地址和端口號是系統隨機分配的,而且服務器會同時處理多個客戶端請求,這些客戶端的IP和端口號都不一樣,通過getSocketAddress()才能正確的找到對應客戶端,而不是輸入固定的IP和端口號
//
這就是為什么該方法用于服務器返回客戶端的響應,而DatagramPacket(byte buf[], int length,InetAddress address, int port)用于客戶端向服務器發送請求
//
對比兩個方法不難發現,前者獲取到的IP和端口號是不固定的,而后者是指定IP和端口號
注意2:為什么服務器要指定端口號?而客戶端的端口號是隨機分配?
講服務器比作餐廳,客戶端比作顧客。顧客來餐廳吃飯,坐的桌子就相當于端口號,顧客今天坐001號桌,改天坐002號桌,人家樂意坐哪就坐那;但是餐廳的位置肯定是固定的,不可能今天餐廳在河邊,明天餐廳就跑到半山腰去了嗎,餐廳的位置不能改變,否則顧客找不到餐廳的位置
1.2UDP客戶端代碼
import java.io.IOException;
import java.net.*;
import java.util.Scanner;public class EchoClient {private final DatagramSocket socket;//這里的IP是Stringprivate final String severIP;private final int severPort;//傳入服務器IP和端口public EchoClient(String severIP,int severPort) throws SocketException {this.socket = new DatagramSocket();this.severIP = severIP;this.severPort = severPort;}//啟動客戶端public void start() throws IOException {System.out.println("客戶端啟動");Scanner in = new Scanner(System.in);while (true){//提示用戶要輸入請求了System.out.print("-> ");//1.從控制臺讀取要發送的請求數據//在用戶輸入之前有阻塞效果if (!in.hasNext()){break;}String request = in.next();//2.請求并發送//字節數組,字節數組長度,服務器IP,服務器端口號DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(severIP),severPort);socket.send(requestPacket);//3.讀取服務器返回的響應DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);socket.receive(responsePacket);//4.將響應打印到控制臺String response = new String(responsePacket.getData(),0,responsePacket.getLength());System.out.println(response);}}//public static void main(String[] args) throws IOException {EchoClient client = new EchoClient("127.0.0.1",9090);client.start();}
}
2.TCP回顯服務器
2.1API介紹
Java中TCP協議的API有兩個,一個是SeverSocket,一個是Socket
2.1.1SeverSocket類
作用:用于服務器監聽來自客戶端的TCP連接請求
構造方法:
//不指定端口號,由系統隨機分配
ServerSocket()
//指定端口號
ServerSocket(int port)
其他方法:
//監聽并接受客戶端傳入的連接請求。此方法有阻塞效果,直到有客戶端連接
Socket accept():
2.1.2Socket類
作用:主要用于客戶端和服務器之間建立TCP連接
構造方法:
//通過傳入IP和端口號連接到指定的主機(服務器)
Socket(String host, int port)
其他方法:
//返回此套接字(實例)的輸入流,用于接收數據
getInputStream()
//返回此套接字(實例)的輸出流,用于發送數據
getOutputStream()
2.1TCP回顯服務器代碼
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;public class TCPEchoSever {private final ServerSocket socket;//public TCPEchoSever(int port) throws IOException {socket = new ServerSocket(port);}//啟動服務器private void start() throws IOException {System.out.println("服務器啟動");while (true){//將服務器和客戶端連接//accept()有阻塞效果,等待客戶端建立聯系Socket clientSocket = socket.accept();//每與一個客戶端建立連接,都創建一個線程來執行客戶端的請求Thread thread = new Thread(()->{//服務器和客戶端交互try {processConnection(clientSocket);} catch (IOException e) {throw new RuntimeException(e);}});thread.start();}}//public void processConnection(Socket clientSocket) throws IOException {System.out.printf("[%s:%d] 服務器上線\n",clientSocket.getInetAddress(),clientSocket.getPort());//inputStream從網卡讀數據try(InputStream inputStream = clientSocket.getInputStream();//OutputStream往網卡寫數據OutputStream outputStream = clientSocket.getOutputStream()) {//從網卡讀數據//byte[] array = new byte[1024];int ret = inputStream.read(array);PrintWriter printWriter = new PrintWriter(outputStream);Scanner scanner = new Scanner(inputStream);while (true){//讀取完畢,當客戶端下線的時候產生//在用戶輸入之前,hasNext()有阻塞效果//當客戶端斷開連接時,scanner.hasNext()返回false并中斷循環if (!scanner.hasNext()){System.out.printf("[%s:%d] 客戶端下線\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}//1.讀取請求并解析//用戶傳過來的請求必須帶有空白符,沒有的話就會阻塞String request = scanner.next();//2.計算響應String response = process(request);//3.返回響應//outputStream.write(response.getBytes(),0,response.getBytes().length);//這個方式不方便添加空白符//通過PrintWriter來封裝outputStream//添加\nprintWriter.println(response);//刷新緩沖區printWriter.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 {TCPEchoSever sever = new TCPEchoSever(9090);sever.start();}
}
注意:為什么要調用clientSocket.close()?
因為每和一個客戶端連接都會創建一個clientSocket套接字,它負責和客戶端交互,即便客戶端進程終止了,客戶端的socket會被操作系統回收,但服務器中的clientSocket仍然會占用文件描述符和內存資源。當文件資源耗盡時,就無法與新的客戶端建立連接
2.2TCP客戶端代碼
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 final Socket socket;public TCPEchoClient(String severIp,int port) throws IOException {//與服務器建立聯系socket = new Socket(severIp,port);}//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 printWriter = new PrintWriter(outputStream);while (true){System.out.print("->");//在用戶輸入之前,hasNext()有阻塞效果if (!scannerConsole.hasNext()){break;}//1.從控制臺輸入請求String request = scannerConsole.next();//2.發送請求//讓請求的結尾有\nprintWriter.println(request);//刷新緩沖區printWriter.flush();//3.從服務器讀取響應String response = scannerNetWork.next();//4.將響應打印到控制臺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();}
}