目錄
一、Socket套接字
(一)概念
(二)分類
1.流套接字:
2.數據報套接字
3.原始套接字
二、TCP協議VSUDP協議
(一)有連接VS無連接
(二)可靠傳輸VS不可靠傳輸
(三)面向字節流VS面向數據報
(四)全雙工VS半雙工
三、UDP協議中的socket api
(一)DatagramSocket類
(二)DatagramPacket類
(三)InetSocketAddress類
四、UDP協議的回顯服務器
(一)UdpEchoServer回顯服務器
(二)UdpEchoClient客戶端?
(三)拓展:英譯漢服務器?
五、TCP協議中的socket api
(一)ServerSocket類
(二)Socket類
六、TCP協議的回顯服務器
(一)TcpEchoServer回顯服務器
(二)TcpEchoClient客戶端
一、Socket套接字
(一)概念
Socket套接字,是有系統提供用于網絡通信的技術,是基于TCP/IP協議的網絡通信的基本操作單元。基于Socket套接字的網絡程序開發就是網絡編程。
(二)分類
1.流套接字:
使用傳輸層TCP協議,是以字節流的格式來通信的。對于字節流來說,可以簡單的理解為傳輸數據是基于IO流,流失數據的特征就是在IO流沒有關閉的情況下,是無邊界的數據,可以多次發送,也可以分開多次發送。
2.數據報套接字
使用傳輸層UDP協議,是以數據報的格式來通信的。對于數據報來說,可以簡單的理解為,傳輸數據是一塊一塊的,發送一塊數據假如100個字節,必須一次發送,接收也必須一次接收100個字節,而不能分100次,每次接收1個字節。
3.原始套接字
原始套接字用于自定義傳輸層協議,用于讀寫內核沒有處理的IP協議數據。
二、TCP協議VSUDP協議
TCP的特點:
- 有連接
- 可靠傳輸
- 面向字節流
- 全雙工
UDP的特點:
- 無連接
- 不可靠傳輸
- 面向數據報
- 全雙工
(一)有連接VS無連接
這是抽象的概念,指的是虛擬的/邏輯上的連接。
- 對于TCP來說,TCP協議中,就保存了對端的信息:?A和B通信,A和B先建立連接,讓A保存B的信息,B保存A的信息(彼此之間知道要連接的是哪個)。
- 對于UDP來說,UDP協議本身,不保存對方的信息,就是無連接。
(二)可靠傳輸VS不可靠傳輸
在網絡上,數據是非常容易出現丟失的情況的(丟包),光信號/電信號都可能受到外界的干擾。
在進行通信時,不能指望一個數據包100%地到達對方。
- 可靠傳輸指的是,雖然不能保證數據包100%到達,但是能盡可能提高傳輸成功的概率。
- 不可靠傳輸只是把數據發了,就不管了。
(三)面向字節流VS面向數據報
- 面向字節流指的是在讀寫數據時,以字節為單位。
- 面向數據報指的是讀寫數據時,以數據報為單位。
(四)全雙工VS半雙工
- 全雙工指的是 一個通信鏈路中,支持雙向通信(能讀也能寫)。
- 半雙工指的是 一個通信鏈路中,只支持單向通信(要么讀,要么寫)。
三、UDP協議中的socket api
計算機中的“文件”,是一個廣義的概念,文件還能代指一些硬件設備(操作系統管理硬件設備,也是抽象成文件,統一管理的)。
UDP協議是用來操作網卡的,將網卡抽象成socket文件,操作網卡的時候,流程和操作普通文件差不多。
(一)DatagramSocket類
DatagramSocket類是用來操作socket文件,發送和接收數據報的。
構造方法:
方法簽名 | 方法說明 |
DatagramSocket() | 創建一個UDP數據報套接字的Socket,綁定到主機的任意一個隨機端口號(一般用于客戶端)。 |
DatagramSocket(int port) | 創建一個UDP數據報套接字的Socket,綁定到主機的一個指定的端口號(一般用于服務端)。 |
成員方法:?
方法簽名 | 方法說明 |
void receive(DatagramPacket p) | 從此套接字接收數據報(如果沒有接受到數據報,該方法會阻塞等待)。 |
void send(DatagramPacket p) | 從此套接字發送數據報(不會阻塞等待,直接發送)。 |
void close() | 關閉此數據報套接字。 |
(二)DatagramPacket類
DatagramPacket就是UDP發送和接收的數據報。
構造方法:
方法簽名 | 方法說明 |
DatagramPacket(byte[]buf,int length) | 構造一個DatagramPacket用來接收數據報,接收的數據報保存在字節數組中(第一個參數buf),接收的指定長度(第二個參數length)。 |
DatagramPacket(byte[]buf,int offset,int length,SocketAddress address) | 構造一個DatagramPacket用來接收數據報,接收的數據報保存在字節數組中(第一個參數buf),指定起點(第二個參數offset),接收的指定長度(第三個參數length)。address指定目的主機的IP和端口號。 |
成員方法:
方法簽名 | 方法說明 |
InetAddress getAddress() | 從接收的數據報中,獲取發送端的主機IP地址;或從發送的數據報中,獲取接收端的主機IP地址。 |
int getPort() | 從接收的數據報中,獲取發送端的主機的端口號;或從發送的數據報中,獲取接收端的主機的端口號。 |
byte[] getData() | 獲取數據報中的數據。 |
(三)InetSocketAddress類
構造UDP發送的數據報時,需要傳入SocketAddress(父類),該對象可以使用InetSocketAddress(子類)來創建。
InetSocketAddress的構造方法:
方法簽名 | 方法說明 |
InetSocketAddress(InetAddress addr,int port) | 創建一個Socket地址,包含IP地址和端口號 |
四、UDP協議的回顯服務器
Java數據報套接字通信模型:
(一)UdpEchoServer回顯服務器
package NetWork;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;//UDP協議的回顯服務器
//服務器端
public class UdpEchoServer {private DatagramSocket socket=null;//指定了一個固定端口號, 讓服務器來使用.public UdpEchoServer(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);//開始接收,并更新數據報socket.receive(RequestPacket);//2.根據請求, 計算響應. (服務器最關鍵的邏輯)//把讀取到的二進制數據, 轉成字符串. 只是構造有效的部分.String request=new String(RequestPacket.getData(),0, RequestPacket.getLength());String response=process(request);//3.把響應返回給客戶端//根據 response 構造 DatagramPacket, 發送給客戶端.//此處不能使用 response.length(),因為這是String的長度而不是byte數組的長度DatagramPacket ResponsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,RequestPacket.getSocketAddress());//發送構建好的數據報socket.send(ResponsePacket);//4.打印日志System.out.printf("[%s:%d] req: %s, resp: %s\n", RequestPacket.getAddress().toString(), RequestPacket.getPort(), request, response);}}//服務器根據請求,處理業務public String process(String request){return request;}public static void main(String[] args) throws IOException {UdpEchoServer server=new UdpEchoServer(9090);server.start();}}
(二)UdpEchoClient客戶端?
package NetWork;import java.io.IOException;
import java.net.*;
import java.util.Scanner;//UDP協議的回顯服務器
//客戶端
public class UdpEchoClient {DatagramSocket socket=null;// 客戶端要給服務器發送數據報,首先得知道服務器的IP和端口號private String ServerIp;//目的IPprivate int ServerPort;//目的端口號// 和服務器不同, 此處的構造方法是要指定訪問的服務器的地址.public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.ServerIp = serverIp;this.ServerPort = serverPort;socket = new DatagramSocket();}public void start() throws IOException {Scanner sc=new Scanner(System.in);while(true){// 1.讀取用戶輸入的內容System.out.println("請輸入要發送的內容:");if(!sc.hasNext()){break;}String request=sc.next();// 2. 把請求發送給服務器, 需要構造 DatagramPacket 對象.// 構造過程中, 不光要構造載荷, 還要設置服務器的 IP 和端口號DatagramPacket RequestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(ServerIp),ServerPort);// 3. 發送數據報socket.send(RequestPacket);// 4. 接收服務器的響應DatagramPacket ResponsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(ResponsePacket);// 5. 從服務器讀取的數據進行解析, 打印出來.String response = new String(ResponsePacket.getData(), 0, ResponsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);client.start();}
}
(三)拓展:英譯漢服務器?
當我們需要實現另外一個簡單的服務器時,例如英譯漢服務器,只需要繼承然后重寫process方法就可以了。
package NetWork;import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;//英譯漢服務器
public class UdpDictServer extends UdpEchoServer{private HashMap<String,String> dict=new HashMap<>();//要在子類的構造方法中調用父類的構造方法//構造方法初始化字典public UdpDictServer(int port) throws SocketException {super(port);dict.put("apple","蘋果");dict.put("boy","男孩");dict.put("cat","小貓");dict.put("dog","小狗");}//重寫process方法public String process(String request){return dict.getOrDefault(request,"沒有找到該詞匯");}public static void main(String[] args) throws IOException {UdpDictServer DictServer=new UdpDictServer(9090);DictServer.start();}
}
五、TCP協議中的socket api
(一)ServerSocket類
ServerSocket是創建TCP服務器端Socket的API。
構造方法:
方法簽名 | 方法說明 |
ServerSocket(int port) | 創建一個服務器端套接字Socket,并綁定到指定端口。 |
成員方法:
方法簽名 | 方法說明 |
Socket accept() | 開始監聽指定端口(創建時綁定的端口),有客戶端連接后,返回一個服務器端Socket對象,并基于該Socket建立與客戶端的連接,否則阻塞等待。 |
void close() | 關閉此套接字 |
(二)Socket類
Socket類是客戶端socket,或服務器端中接收到客戶端建立連接(accept方法)的請求后,返回的服務端Socket。
不管是客戶端還是服務器端Socket,都是雙方建立連接以后,保存的對端信息,及用來與對方收發數據的。
構造方法:
方法簽名 | 方法說明 |
Socket(String host,int port) | 創建一個客戶端套接字Socket,并對應IP的主機上對應端口的進程進行連接。 |
成員方法:
方法簽名 | 方法說明 |
InetAddress getInetAddress() | 返回套接字所連接的地址 |
InputStream getInputStream() | 返回此套接字的輸入流 |
OutputStream getOutPutStream() | 返回此套接字的輸出流 |
六、TCP協議的回顯服務器
Java流套接字通信模型:
(一)TcpEchoServer回顯服務器
package NetWork;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=null;public TcpEchoServer(int port) throws IOException {serverSocket=new ServerSocket(port);}public void start() throws IOException {System.out.println("啟動服務器");// 這種情況一般不會使用 fixedThreadPool, 意味著同時處理的客戶端連接數目就固定了.ExecutorService executorService = Executors.newCachedThreadPool();while (true) {// tcp 來說, 需要先處理客戶端發來的連接.// 通過讀寫 clientSocket, 和客戶端進行通信.// 如果沒有客戶端發起連接, 此時 accept 就會阻塞.// 主線程負責進行 accept, 每次 accept 到一個客戶端, 就創建一個線程, 由新線程負責處理客戶端的請求.Socket clientSocket = serverSocket.accept();// 使用多線程的方式來調整// Thread t = new Thread(() -> {// processConnection(clientSocket);// });// t.start();// 使用線程池來調整executorService.submit(() -> {processConnection(clientSocket);});}}private void processConnection(Socket clientSocket){//對clientSocket進行讀寫操作System.out.printf("[%s:%d] 客戶端上線!\n", clientSocket.getInetAddress(), clientSocket.getPort());try(InputStream inputStream=clientSocket.getInputStream();OutputStream outputStream=clientSocket.getOutputStream()){// 針對 InputStream 套了一層Scanner scanner = new Scanner(inputStream);// 針對 OutputStream 套了一層PrintWriter writer = new PrintWriter(outputStream);while(true){//因為輸入流中的數據是持續讀取的,要加上循環// 1. 讀取請求并解析. 可以直接 read, 也可以借助 Scanner 來輔助完成.if (!scanner.hasNext()) {//scanner.hasNext():判斷輸入流中是否還有 “下一個令牌”(默認以空白字符分割,如空格、換行等)。// 連接斷開了System.out.printf("[%s:%d] 客戶端下線!\n", clientSocket.getInetAddress(), clientSocket.getPort());break;}// 2. 根據請求計算響應String request=scanner.next();String response=process(request);// 3. 返回響應到客戶端// outputStream.write(response.getBytes());writer.println(response);//將緩存區中的數據都發送出去,避免殘留writer.flush();// 打印日志System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),request, response);}} catch (IOException e) {throw new RuntimeException(e);} finally {try {//服務器連接一個客戶端就要創建一個clientSocket,使用完就要關閉.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 tcpEchoServer=new TcpEchoServer(9090);tcpEchoServer.start();}
}
(二)TcpEchoClient客戶端
package NetWork;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;public TcpEchoClient(String serverIp, int serverPort) throws IOException {// 直接把字符串的 IP 地址, 設置進來.// 127.0.0.1 這種字符串socket = new Socket(serverIp, serverPort);}public void start()throws IOException{Scanner scanner=new Scanner(System.in);try(InputStream inputStream=socket.getInputStream();OutputStream outputStream=socket.getOutputStream()){//給inPutStream套一層Scanner scannerNet= new Scanner(inputStream);//給outPutStream套一層PrintWriter writer=new PrintWriter(outputStream);while (true){//1.讀取用戶輸入String request=scanner.next();//2.發送請求并刷新緩存區數據writer.println(request);writer.flush();//3.接收服務器的響應String response=scannerNet.next();//4.打印出響應System.out.println(response);}}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);client.start();}
}
注意點:
- 在服務器中,采用多線程的方式來處理客戶端的請求(使用線程池)。因為如果是單線程有多個客戶端連接,當程序處理processConnection請求時,就可能阻塞在processConnection,而不能accpet。
- 因為服務器中有scanner.hasNext來判斷發來的請求,所以客戶端發送的請求要以換行符/空白符號結束,因此發送時用writer.println。
- 發送請求后記得使用flush刷新緩沖區的數據。