B/S結構 :全稱爲Browser/Server結構,是指瀏覽器和伺服器結構。常見瀏覽器有谷歌、火狐等。
兩種架構各有優勢,但是無論哪種架構,都離不開網路的支援。網路程式設計,就是在一定的協定下,實現兩臺計算機的通訊的程式。
**網路通訊協定:**通過計算機網路可以使多臺計算機實現連線,位於同一個網路中的計算機在進行連線和通訊時需要遵守一定的規則,這就好比在道路中行駛的汽車一定要遵守交通規則一樣。在計算機網路中,這些連線和通訊的規則被稱爲網路通訊協定,它對數據的傳輸格式、傳輸速率、傳輸步驟等做了統一規定,通訊雙方必須同時遵守才能 纔能完成數據交換。
TCP/IP協定: 傳輸控制協定/因特網互聯協定( Transmission Control Protocol/Internet Protocol),是Internet最基本、最廣泛的協定。它定義了計算機如何連入因特網,以及數據如何在它們之間傳輸的標準。它的內部包含一系列的用於處理數據通訊的協定,並採用了4層的分層模型,每一層都呼叫它的下一層所提供的協定來完成自己的需求。
上圖中,TCP/IP協定中的四層分別是應用層、傳輸層、網路層和鏈路層,每層分別負責不同的通訊功能。
鏈路層:鏈路層是用於定義物理傳輸通道,通常是對某些網路連線裝置的驅動協定,例如針對光纖、網線提供的驅動。
網路層:網路層是整個TCP/IP協定的核心,它主要用於將傳輸的數據進行分組,將分組數據發送到目標計算機或者網路。
運輸層:主要使網路程式進行通訊,在進行網路通訊時,可以採用TCP協定,也可以採用UDP協定。
應用層:主要負責應用程式的協定,例如HTTP協定、FTP協定等。
通訊的協定還是比較複雜的,java.net
包中包含的類和介面,它們提供低層次的通訊細節。我們可以直接使用這些類和介面,來專注於網路程式開發,而不用考慮通訊的細節。
java.net
包中提供了兩種常見的網路協定的支援:
UDP:用戶數據報協定(User Datagram Protocol)。UDP是無連線通訊協定,即在數據傳輸時,數據的發送端和接收端不建立邏輯連線。簡單來說,當一臺計算機向另外一臺計算機發送數據時,發送端不會確認接收端是否存在,就會發出數據,同樣接收端在收到數據時,也不會向發送端反饋是否收到數據。
由於使用UDP協定消耗資源小,通訊效率高,所以通常都會用於音訊、視訊和普通數據的傳輸例如視訊會議都使用UDP協定,因爲這種情況即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響。
但是在使用UDP協定傳送數據時,由於UDP的面向無連線性,不能保證數據的完整性,因此在傳輸重要數據時不建議使用UDP協定。UDP的交換過程如下圖所示。
特點:數據被限制在64kb以內,超出這個範圍就不能發送了。
數據報(Datagram):網路傳輸的基本單位
TCP:傳輸控制協定 (Transmission Control Protocol)。TCP協定是面向連接的通訊協定,即傳輸數據之前,在發送端和接收端建立邏輯連線,然後再傳輸數據,它提供了兩臺計算機之間可靠無差錯的數據傳輸。
在TCP連線中必須要明確用戶端與伺服器端,由用戶端向伺服器端發出連線請求,每次連線的建立都需要經過「三次握手」。
完成三次握手,連線建立後,用戶端和伺服器就可以開始進行數據傳輸了。由於這種面向連接的特性,TCP協定可以保證傳輸數據的安全,所以應用十分廣泛,例如下載檔案、瀏覽網頁等。
IP地址分類
IPv4:是一個32位元的二進制數,通常被分爲4個位元組,表示成a.b.c.d
的形式,例如192.168.65.100
。其中a、b、c、d都是0~255之間的十進制整數,那麼最多可以表示42億個。
IPv6:由於網際網路的蓬勃發展,IP地址的需求量愈來愈大,但是網路地址資源有限,使得IP的分配越發緊張。
爲了擴大地址空間,擬通過IPv6重新定義地址空間,採用128位元地址長度,每16個位元組一組,分成8組十六進制數,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,號稱可以爲全世界的每一粒沙子編上一個網址,這樣就解決了網路地址資源數量不夠的問題。
常用命令
ipconfig
ping 空格 IP地址
ping 220.181.57.216
特殊的IP地址
127.0.0.1
、localhost
。網路的通訊,本質上是兩個進程(應用程式)的通訊。每台計算機都有很多的進程,那麼在網路通訊時,如何區分這些進程呢?
如果說IP地址可以唯一標識網路中的裝置,那麼埠號就可以唯一標識裝置中的進程(應用程式)了。
利用協定
+IP地址
+埠號
三元組合,就可以標識網路中的進程了,那麼進程間的通訊就可以利用這個標識與其它進程進行互動。
TCP通訊能實現兩臺計算機之間的數據互動,通訊的兩端,要嚴格區分爲用戶端(Client)與伺服器端(Server)。
兩端通訊時步驟:
在Java中,提供了兩個類用於實現TCP通訊程式:
java.net.Socket
類表示。建立Socket
物件,向伺服器端發出連線請求,伺服器端響應請求,兩者建立連線開始通訊。java.net.ServerSocket
類表示。建立ServerSocket
物件,相當於開啓一個服務,並等待用戶端的連線。Socket
類:該類實現用戶端通訊端,通訊端指的是兩臺裝置之間通訊的端點。
public Socket(String host, int port)
:建立通訊端物件並將其連線到指定主機上的指定埠號。如果指定的host是null ,則相當於指定地址爲回送地址。
小貼士:回送地址(127.x.x.x) 是本機回送地址(Loopback Address),主要用於網路軟體測試以及本地機進程間通訊,無論什麼程式,一旦使用回送地址發送數據,立即返回,不進行任何網路傳輸。
構造舉例,程式碼如下:
Socket client = new Socket("127.0.0.1", 6666);
public InputStream getInputStream()
: 返回此通訊端的輸入流。
public OutputStream getOutputStream()
: 返回此通訊端的輸出流。
public void close()
:關閉此通訊端。
public void shutdownOutput()
: 禁用此通訊端的輸出流。
ServerSocket
類:這個類實現了伺服器通訊端,該物件等待通過網路的請求。
public ServerSocket(int port)
:使用該構造方法在建立ServerSocket物件時,就可以將其系結到一個指定的埠號上,參數port就是埠號。構造舉例,程式碼如下:
ServerSocket server = new ServerSocket(6666);
public Socket accept()
:偵聽並接受連線,返回一個新的Socket物件,用於和用戶端實現通訊。該方法會一直阻塞直到建立連線。到此,用戶端向伺服器端發送數據成功。
自此,伺服器端向用戶端回寫數據。
伺服器端實現:
public class ServerTCP {
public static void main(String[] args) throws IOException {
System.out.println("伺服器端啓動 , 等待連線 .... ");
// 1.建立 ServerSocket物件,系結埠,開始等待連線
ServerSocket ss = new ServerSocket(6666);
// 2.接收連線 accept 方法, 返回 socket 物件.
Socket server = ss.accept();
// 3.通過socket 獲取輸入流
InputStream is = server.getInputStream();
// 4.一次性讀取數據
// 4.1 建立位元組陣列
byte[] b = new byte[1024];
// 4.2 據讀取到位元組陣列中.
int len = is.read(b);
// 4.3 解析陣列,列印字串資訊
String msg = new String(b, 0, len);
System.out.println(msg);
//5.關閉資源.
is.close();
server.close();
}
}
用戶端實現:
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("用戶端 發送數據");
// 1.建立 Socket ( ip , port ) , 確定連線到哪裏.
Socket client = new Socket("localhost", 6666);
// 2.獲取流物件 . 輸出流
OutputStream os = client.getOutputStream();
// 3.寫出數據.
os.write("你好麼? tcp ,我來了".getBytes());
// 4. 關閉資源 .
os.close();
client.close();
}
}
伺服器端實現:
public class ServerTCP {
public static void main(String[] args) throws IOException {
System.out.println("伺服器端啓動 , 等待連線 .... ");
// 1.建立 ServerSocket物件,系結埠,開始等待連線
ServerSocket ss = new ServerSocket(6666);
// 2.接收連線 accept 方法, 返回 socket 物件.
Socket server = ss.accept();
// 3.通過socket 獲取輸入流
InputStream is = server.getInputStream();
// 4.一次性讀取數據
// 4.1 建立位元組陣列
byte[] b = new byte[1024];
// 4.2 據讀取到位元組陣列中.
int len = is.read(b);
// 4.3 解析陣列,列印字串資訊
String msg = new String(b, 0, len);
System.out.println(msg);
// =================回寫數據=======================
// 5. 通過 socket 獲取輸出流
OutputStream out = server.getOutputStream();
// 6. 回寫數據
out.write("我很好,謝謝你".getBytes());
// 7.關閉資源.
out.close();
is.close();
server.close();
}
}
用戶端實現:
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("用戶端 發送數據");
// 1.建立 Socket ( ip , port ) , 確定連線到哪裏.
Socket client = new Socket("localhost", 6666);
// 2.通過Scoket,獲取輸出流物件
OutputStream os = client.getOutputStream();
// 3.寫出數據.
os.write("你好麼? tcp ,我來了".getBytes());
// ==============解析回寫=========================
// 4. 通過Scoket,獲取 輸入流物件
InputStream in = client.getInputStream();
// 5. 讀取數據數據
byte[] b = new byte[100];
int len = in.read(b);
System.out.println(new String(b, 0, len));
// 6. 關閉資源 .
in.close();
os.close();
client.close();
}
}
伺服器端實現:
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("伺服器 啓動..... ");
// 1. 建立伺服器端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 建立連線
Socket accept = serverSocket.accept();
// 3. 建立流物件
// 3.1 獲取輸入流,讀取檔案數據
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
// 3.2 建立輸出流,儲存到本地 .
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));
// 4. 讀寫數據
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
//5. 關閉 資源
bos.close();
bis.close();
accept.close();
System.out.println("檔案上傳已儲存");
}
}
用戶端實現:
public class FileUPload_Client {
public static void main(String[] args) throws IOException {
// 1.建立流物件
// 1.1 建立輸入流,讀取本地檔案
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
// 1.2 建立輸出流,寫到伺服器端
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//2.寫出數據.
byte[] b = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(b))!=-1) {
bos.write(b, 0, len);
bos.flush();
}
System.out.println("檔案發送完畢");
// 3.釋放資源
bos.close();
socket.close();
bis.close();
System.out.println("檔案上傳完畢 ");
}
}
檔名稱寫死的問題
伺服器端,儲存檔案的名稱如果寫死,那麼最終導致伺服器硬碟,只會保留一個檔案,建議使用系統時間優化,保證檔名稱唯一,程式碼如下:
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg") // 檔名稱
BufferedOutputStream bos = new BufferedOutputStream(fis);
回圈接收的問題
伺服器端,指儲存一個檔案就關閉了,之後的使用者無法再上傳,這是不符合實際的,使用回圈改進,可以不斷的接收不同使用者的檔案,程式碼如下:
// 每次接收新的連線,建立一個Socket
while(true){
Socket accept = serverSocket.accept();
......
}
效率問題
伺服器端,在接收大檔案時,可能耗費幾秒鐘的時間,此時不能接收其他使用者上傳,所以,使用多執行緒技術優化,程式碼如下:
while(true){
Socket accept = serverSocket.accept();
// accept 交給子執行緒處理.
new Thread(() -> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("伺服器 啓動..... ");
// 1. 建立伺服器端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 回圈接收,建立連線
while (true) {
Socket accept = serverSocket.accept();
/*
3. socket物件交給子執行緒處理,進行讀寫操作
Runnable介面中,只有一個run方法,使用lambda表達式簡化格式
*/
new Thread(() -> {
try (
//3.1 獲取輸入流物件
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//3.2 建立輸出流物件, 儲存到本地 .
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);) {
// 3.3 讀寫數據
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
//4. 關閉 資源
bos.close();
bis.close();
accept.close();
System.out.println("檔案上傳已儲存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
前四步與基本檔案上傳一致.
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("伺服器 啓動..... ");
// 1. 建立伺服器端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 回圈接收,建立連線
while (true) {
Socket accept = serverSocket.accept();
/*
3. socket物件交給子執行緒處理,進行讀寫操作
Runnable介面中,只有一個run方法,使用lambda表達式簡化格式
*/
new Thread(() -> {
try (
//3.1 獲取輸入流物件
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//3.2 建立輸出流物件, 儲存到本地 .
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);
) {
// 3.3 讀寫數據
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
// 4.=======資訊回寫===========================
System.out.println("back ........");
OutputStream out = accept.getOutputStream();
out.write("上傳成功".getBytes());
out.close();
//================================
//5. 關閉 資源
bos.close();
bis.close();
accept.close();
System.out.println("檔案上傳已儲存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
用戶端實現:
public class FileUpload_Client {
public static void main(String[] args) throws IOException {
// 1.建立流物件
// 1.1 建立輸入流,讀取本地檔案
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
// 1.2 建立輸出流,寫到伺服器端
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//2.寫出數據.
byte[] b = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(b))!=-1) {
bos.write(b, 0, len);
}
// 關閉輸出流,通知伺服器端,寫出數據完畢
socket.shutdownOutput();
System.out.println("檔案發送完畢");
// 3. =====解析回寫============
InputStream in = socket.getInputStream();
byte[] back = new byte[20];
in.read(back);
System.out.println(new String(back));
in.close();
// ============================
// 4.釋放資源
socket.close();
bis.close();
}
}
模擬網站伺服器,使用瀏覽器存取自己編寫的伺服器端程式,檢視網頁效果。
準備頁面數據,web資料夾。
複製到我們Module中,比如複製到day08中
我們模擬伺服器端,ServerSocket類監聽埠,使用瀏覽器存取
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8000);
Socket socket = server.accept();
InputStream in = socket.getInputStream();
byte[] bytes = new byte[1024];
int len = in.read(bytes);
System.out.println(new String(bytes,0,len));
socket.close();
server.close();
}
GET/web/index.html HTTP/1.1是瀏覽器的請求訊息。/web/index.html爲瀏覽器想要請求的伺服器端的資源,使用字串切割方式獲取到請求的資源。
//轉換流,讀取瀏覽器請求第一行
BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出請求資源的路徑
String[] strArr = requst.split(" ");
//去掉web前面的/
String path = strArr[1].substring(1);
System.out.println(path);
伺服器端實現:
public class SerDemo {
public static void main(String[] args) throws IOException {
System.out.println("伺服器端 啓動 , 等待連線 .... ");
// 建立ServerSocket 物件
ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();
// 轉換流讀取瀏覽器的請求訊息
BufferedReader readWb = new
BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
// 取出請求資源的路徑
String[] strArr = requst.split(" ");
// 去掉web前面的/
String path = strArr[1].substring(1);
// 讀取用戶端請求的資原始檔
FileInputStream fis = new FileInputStream(path);
byte[] bytes= new byte[1024];
int len = 0 ;
// 位元組輸出流,將檔案寫會用戶端
OutputStream out = socket.getOutputStream();
// 寫入HTTP協定響應頭,固定寫法
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());
// 必須要寫入空行,否則瀏覽器不解析
out.write("\r\n".getBytes());
while((len = fis.read(bytes))!=-1){
out.write(bytes,0,len);
}
fis.close();
out.close();
readWb.close();
socket.close();
server.close();
}
}
小貼士:不同的瀏覽器,內核不一樣,解析效果有可能不一樣。
發現瀏覽器中出現很多的叉子,說明瀏覽器沒有讀取到圖片資訊導致。
瀏覽器工作原理是遇到圖片會開啓一個執行緒進行單獨的存取,因此在伺服器端加入執行緒技術。
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
while(true){
Socket socket = server.accept();
new Thread(new Web(socket)).start();
}
}
static class Web implements Runnable{
private Socket socket;
public Web(Socket socket){
this.socket=socket;
}
public void run() {
try{
//轉換流,讀取瀏覽器請求第一行
BufferedReader readWb = new
BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出請求資源的路徑
String[] strArr = requst.split(" ");
System.out.println(Arrays.toString(strArr));
String path = strArr[1].substring(1);
System.out.println(path);
FileInputStream fis = new FileInputStream(path);
System.out.println(fis);
byte[] bytes= new byte[1024];
int len = 0 ;
//向瀏覽器 回寫數據
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());
out.write("\r\n".getBytes());
while((len = fis.read(bytes))!=-1){
out.write(bytes,0,len);
}
fis.close();
out.close();
readWb.close();
socket.close();
}catch(Exception ex){
}
}
}
}
存取效果: