目錄
前言:
基礎了解
傳輸層協定
UDP
TCP
Socket API
DatagramSocket API
DatagramPacket API
UDP實作回顯伺服器
完整代碼展現(有詳細注釋)
UDP實作回顯用戶端
完整代碼展現(有詳細注釋)
小結:
前言:
通過套接字Socket就可以實作用戶端發送請求,服務起接收請求,處理完成後就可以響應給用戶端。這樣的一套流程就實作了資料在網絡上的傳輸。
基礎了解
網絡程式設計中,在硬體上使用網卡發送和接收資料。在java中使用Socket直接操作網卡,而對于作業系統來說一切皆檔案,那麼這個Socket對象在作業系統中是被當作檔案處理的。Socket就是作業系統給應用程式提供的接口。
Socket所提供的api和傳輸層密切相關,應用層首先接觸的就是傳輸層。使用Socket所提供的api就可以實作應用層的代碼并且和傳輸層進行互動。
用戶端發起請求 --> 伺服器接收請求 --> 伺服器處理請求并響應給用戶端 --> 用戶端接收響應
傳輸層協定
UDP
特點:無連接配接,不可靠傳輸,面向資料報,全雙工,大小首先(一次最多64k),有接收緩沖區無發送緩沖區。
TCP
特點:有連接配接。可靠傳輸,面向位元組流,全雙工,大小不限,有接收緩沖區和發送緩沖區。
了解:
1)無連接配接:不需要建立用戶端和伺服器之間的連接配接,就可以發送資料。(例如微信發消息)
2)有連接配接:需要建立用戶端和伺服器之間的連接配接,才可發送資料。(例如打電話,需要接聽)
3)不可靠傳輸:發送方不知道資料是發過去了,還是丢包了。
4)可靠傳輸:發送方知道自己的消息是否發送過去。
注意:可靠性就是針對發送方是否清楚資料是否發送過去。
5)面向資料報:資料傳輸以“資料報”為基本機關,一塊一塊的發資料。
6)面向位元組流:資料傳輸和讀檔案類似,“流式”的。一次發送部分資料,也可以發送全部資料。
7)全雙工:可以同時發送和接收資料,那麼半雙工就不支援。
Socket API
java中使用UDP協定通信,主要基于 DatagramSocket 類來建立資料報套接字,并使用
DatagramPacket 作為發送或接收的UDP資料報。
DatagramSocket API
DatagramSocket構造方法
注意:
建立一個UDP資料報套接字的Socket,綁定本機任意一個随機端口(一般用于用戶端)
注意:
建立一個UDP資料報套接字的Socket,綁定指定端口(一般用于服務端)
DatagramSocket方法
注意:
從網卡接收資料報。這個參數需要一個空的DatagramPacket對象,當從網卡接收到資料報就會填充好這個空的對象,以便供我們處理資料。
如果沒有接收資料報,這個方法會阻塞等待。
注意:
将已經構造好的資料報發送到網卡。不會阻塞等待直接發送。
注意:
在作業系統中Socket對象是被當作檔案處理的,那麼就需要釋放pcb中檔案描述符表中的資源。
DatagramPacket API
DatagramPacket構造方法
注意:
構造一個DatagramPacket以用來接收資料報,接收的資料儲存在buf緩沖數組中,接收的指定長度。
注意:
構造一個DatagramPacket以用來接收資料報,資料填充為位元組數組,從0起始位置到指定長度(offset,length),address指定目的主機IP和端口号。(一般處理完請求後,構造成資料報來發送)
DatagramPacket方法
注意:
從接收的資料報中,擷取發送端主機IP位址;或從發送的資料報中,擷取接收端主機IP位址。
注意:
從接收的資料報中,擷取發送端主機的端口号;或從發送的資料報中,擷取接收端主機端口号。
注意:
擷取資料報中的資料,就是緩沖數組。
UDP實作回顯伺服器
伺服器是被動的一方,需要接收用戶端發起的請求。那麼用戶端就必須明确伺服器的ip和具體程序的端口号。是以在實作伺服器時就必須指定端口号,這裡實作的是本機到本機的資料發送,ip就使用環回ip即可。
由于不清楚用戶端什麼時候發起請求,那麼伺服器不能休息(随時待命)。這裡使用死循環的方式,但它不會一直循環,因為receive()方法當沒有接收到請求時會阻塞等待。
我們首先需要明确伺服器的工作流程。接收用戶端的請求 --> 處理請求 --> 将響應發送給用戶端。
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
注意:
首先構造一個空的DatagramPacket對象,傳入緩沖數組,和指定長度。當下面receive()方法從網卡接收到用戶端請求時就會填充這個空對象。(資料是寫入了緩沖數組)
當receive()方法當沒有接收到請求時會阻塞等待。(随時待命)
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
注意:
由于接收的資料構造成了資料報,這樣不利于我們處理資料。我們将資料報中的資料取出來構造成字元串。
String response = process(request);
public String process(String request) {
return request;
}
注意:
伺服器針對請求進行需求處理,這裡的process是一個方法。由于我們實作的是回顯伺服器,即直接傳回這個字元串即可。
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
注意:
這裡将處理完後的響應構造成資料報,然後發送給用戶端程式。這裡需要傳入位元組數組,填充的具體長度,(這裡需要用位元組數組的長度,不能用字元串的長度。轉換之後兩者長度是不一緻的)和用戶端的ip和端口号(getSocketAddress()方法可以獲得發送方的ip和端口号)。
當構造完成之後直接将資料報發給用戶端即可。
完整代碼展現(有詳細注釋)
public class UdpEchoSever {
//Socket對象直接操作的是網卡,在作業系統中任務Socket對象是檔案(一切皆檔案)
//通過Socket對象接收和發送資料
private DatagramSocket socket = null;
public UdpEchoSever(int port) throws SocketException {
//伺服器是被動的一方,用戶端必須找到伺服器的端口,才能找到指定程式,是以伺服器必須指定端口号
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("啟動伺服器");
while (true) {
//構造空的Packet對象,傳入緩沖數組
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
//receive從網卡接收資料,解析後填充這個空對象(輸出形參數)(可以認為寫入了緩沖數組)
//用戶端如果沒有發請求receive就會阻塞,直到用戶端發送請求(保證這裡不會一直循環)
socket.receive(requestPacket);
//根據接收的資料(由于接收的資料不友善處理),是以構造成字元串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//伺服器響應處理
String response = process(request);
//構造發送的資料報,位元組數組,位元組數組長度,IP和端口(根據響應的字元串)
//這個DatagramPacket隻認位元組數組,是以就需要擷取位元組數組的長度而不是字元的個數
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
//發送資料到Ip和端口指定的用戶端程式
socket.send(responsePacket);
//列印下,請求響應的中間結果
System.out.printf("源IP:%s 源端口:%d 請求資料:%s 響應資料:%s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
//回顯伺服器,處理直接傳回資料(響應)
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
//端口号的指定在 1024 -- 65535 裡指定
UdpEchoSever sever = new UdpEchoSever(8280);
sever.start();
}
}
UDP實作回顯用戶端
用戶端發送資料需要明确伺服器的ip和具體程序的端口号。用戶端的端口号我們不需要手動指定,因為用戶端程式是存在于客戶主機上,我們如果手動指定就很可能與其他程序端口号沖突,這樣就直接抛異常了(Address already in use)。直接讓作業系統随機配置設定一個空閑的端口号。
那麼為什麼服務端我們可以指定端口号,這樣就不怕與其他程序沖突了麼?因為伺服器在我們自己手裡,我們明确裡面的各種端口号,簡單來說就是可控的。
我們首先明确用戶端的工作流程。使用者輸入資料 --> 發送到伺服器 --> 接收伺服器的響應。這裡也使用死循環,和上面一樣receive()方法會阻塞,不會一直循環。
Scanner scanner = new Scanner(System.in);
System.out.println("輸入你要發送的資料:");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("bye bye");
break;
}
注意:
提示使用者輸入資料,這裡做了簡單的判斷。
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(severIp), severPort);
socket.send(requestPacket);
注意:
根據使用者輸入的資料構造成資料報。需要位元組數組,具體填充的長度(同樣的需要位元組數組長度而不是字元串長度),ip(由于這裡需要一個32位的ip,而上面的是字元串,是以需要轉換)和伺服器端口号。然後直接發送即可。
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
注意:
接收伺服器的響應。首先構造一個空的DatagramPacket對象,傳入緩沖數組和指定長度。receive()方法從網卡接收到資料報然後構造好這個空的對象。
receive()當沒有接收到響應前同樣的也會阻塞等待。
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
注意:
由于是資料報不利于使用者觀察資料,是以轉轉換為字元串。擷取到資料報中的緩沖數組,從0位置到指定長度來構造這個字元串,最終顯示給使用者。
完整代碼展現(有詳細注釋)
public class UdpEchoClient {
private DatagramSocket socket = null;
//用戶端需要知道伺服器的IP,和端口,這裡先存一下
private String severIp = null;
private int severPort = 0;
public UdpEchoClient(String severIp, int severPort) throws SocketException {
//用戶端不需要指定端口号,用戶端程式在使用者手裡,指定端口号就可能和其他程序重複。是以讓作業系統配置設定一個空閑的端口
//伺服器為什麼指定端口不怕重複呢?因為伺服器在程式員手裡我們清楚端口号的使用(可控的),而用戶端是(不可控的)
socket = new DatagramSocket();
this.severIp = severIp;
this.severPort = severPort;
}
//用戶端啟動
public void start() throws IOException {
//使用者輸入資料
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("輸入你要發送的資料:");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("bye bye");
break;
}
//發送資料報(構造DatagramPacket對象)
//此處的IP需要一個32位的整數,而上面的是字元串,需要轉換
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(severIp), severPort);
socket.send(requestPacket);
//接收資料報(填充這個空對象)(阻塞到伺服器發送過來資料)
//receive的阻塞作業系統實作的,JAVA隻是封裝了一下
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//顯示資料報(将資料報轉換為字元串)
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 8280);
udpEchoClient.start();
}
}
小結:
這裡大多是api的使用,我們要了解其中的原理,便能得心應手。