主要內容
- 網絡通信三要素
- TCP通信
- Socket
- ServerSocket
- 軟件架構CS/BS
教學目標
- 能夠辨別UDP和TCP協議特點
- 能夠說出TCP協議下兩個常用類名稱
- 能夠編寫TCP協議下字符串數據傳輸程序
- 能夠理解TCP協議下文件上傳案例
- 能夠理解TCP協議下案例2
第一章 網絡編程入門
1.1 網絡通信協議
我們以前寫的程序,都是在自己的計算機中,內存與硬盤、內存與內存之間進行數據的傳輸、交互。都是在本機。
並沒有實現計算機之間的數據傳輸和交互。現在我們的目標是把自己本地的數據傳輸給其他的電腦。在2臺電腦,或者多臺終端設備之間要進行數據的傳輸。
首先要進行數據的傳輸,我們就必須先把多臺設備之間連接起來。多臺設備之間要進行連接,就必須通過計算機網絡。
通過計算機網絡可以使多臺計算機實現連接,位於同一個網絡中的計算機在進行連接和通信時需要遵守一定的規則,這就好比在道路中行駛的汽車一定要遵守交通規則一樣。在計算機網絡中,這些連接和通信的規則被稱爲網絡通信協議,它對數據的傳輸格式、傳輸速率、傳輸步驟等做了統一規定,通信雙方必須同時遵守才能完成數據交換。
網絡通信協議有很多種,目前應用最廣泛的是TCP/IP協議(Transmission Control Protocal/Internet Protocal傳輸控制協議/英特網互聯協議),它是一個包括TCP協議和IP協議。UDP(User Datagram Protocal)協議和其它一些協議的協議組。數據報包協議。
- TCP/IP協議: 傳輸控制協議/因特網互聯協議( Transmission Control Protocol/Internet Protocol),是Internet最基本、最廣泛的協議。它定義了計算機如何連入因特網,以及數據如何在它們之間傳輸的標準。它的內部包含一系列的用於處理數據通信的協議,並採用了4層的分層模型,每一層都呼叫它的下一層所提供的協議來完成自己的需求。
1.2 協議分類
通信的協議還是比較複雜的,java.net
包中包含的類和接口,它們提供低層次的通信細節。我們可以直接使用這些類和接口,來專注於網絡程序開發,而不用考慮通信的細節。
java.net
包中提供了兩種常見的網絡協議的支持:
- TCP:傳輸控制協議 (Transmission Control Protocol)。TCP協議是面向連接的通信協議,即傳輸數據之前,在發送端和接收端建立邏輯連接,然後再傳輸數據,它提供了兩臺計算機之間可靠無差錯的數據傳輸。
- 三次握手:TCP協議中,在發送數據的準備階段,客戶端與服務器之間的三次交互,以保證連接的可靠。
- 第一次握手,客戶端向服務器端發出連接請求,等待服務器確認。
- 第二次握手,服務器端向客戶端回送一個響應,通知客戶端收到了連接請求。
- 第三次握手,客戶端再次向服務器端發送確認信息,確認連接。整個交互過程如下圖所示。
- 三次握手:TCP協議中,在發送數據的準備階段,客戶端與服務器之間的三次交互,以保證連接的可靠。
完成三次握手,連接建立後,客戶端和服務器就可以開始進行數據傳輸了。由於這種面向連接的特性,TCP協議可以保證傳輸數據的安全,所以應用十分廣泛,例如下載文件、瀏覽網頁等。
說明:TCP協議在通信的時候,要求通信的雙方先建立起連接(面向有連接的協議),形成傳輸數據的通道,在連接中進行大數據量傳輸。
總結
TCP特點:
A:不限制數據的大小,大數據傳輸;
B:需要建立鏈接;我們稱爲客戶端和服務端的連接爲連接通道
C:安全可靠;
D:效率低;
舉例:類似於打電話或者QQ視頻。
-
UDP:用戶數據報包協議。UDP 是User Datagram Protocol的簡稱, 中文名是用戶數據報包協議。
說明:
UDP協議表示將數據源和目的地封裝成數據報包中,然後不需要建立連接。每個數據報的大小被限制在64K。發送一方,不用關心接收一方是否在線。就直接發送數據。如果對方在就可以接收數據,如果對方不在,這時數據就自動的被丟棄。所以,UDP協議不安全,不可靠,但是效率高。不能傳輸大數據。
總結:
UDP特點:
A:把數據封裝成數據報包;
B:限制大小(限制在64K);
C:不需要建立鏈接;
D:不安全,不可靠;
E:速度快;
舉例:類似於對講機。
1.4 網絡編程三要素
協議
- **協議:**計算機網絡通信必須遵守的規則,已經介紹過了,不再贅述。
IP地址
要想使網絡中的計算機能夠進行通信,必須爲每臺計算機指定一個標識號,通過這個標識號來指定接受數據的計算機或者發送數據的計算機。
處於網絡中的通信設備,它們都會分配一個IP地址。
IP:要想讓網絡中的計算機能夠互相通信,必須爲每臺計算機指定一個標識號,通過這個標識號來指定要接受數據的計算機和識別發送的計算機,在TCP/IP協議中,這個標識號就是IP地址。
也可以理解爲IP地址是對處於網絡中的某個通信終端的標識。然後通信的另外一方,就可以根據這個IP地址找到對方,和對方進行通信。
對IP地址進行說明:
1、IPv4:IP協議的版本號是4(簡稱爲IPv4,v,version版本),早期的IP地址方式。4*8共32位來表示IP,總共43億(2的32次方)個IP地址。數量有限,所以就有了IPv6。
由於計算機只識別2進制數據,所以IP地址其實是二進制的:
舉例:11000000 10101000 01010000 11000110
問題:那麼爲什麼IP地址可以使用數字表示呢?
對於用戶來講,使用一些2進制數據來標識計算機,不容易記住。計算機提供了一種方式:”點分10進制”。 IP地址使用一些10進制數據表示,中間使用”.”來分隔。
每8個位作爲一段,轉爲10進制如下:
192.168.80.198
例如上述的IPv4地址:192.168.100.107
2、IPv6:IPv6是Internet Protocol Version 6的縮寫,其中Internet Protocol是“互聯網協議”。新的IP地址方式。用來替換IPv4協議。
解決了IPv4協議帶來的網絡地址資源有限的問題。
IPv6的地址長度爲128,是IPv4地址長度的4倍。即採用 8*16 共128位來表示IP,根本用不完。
於是IPv4點分十進制格式不再適用,採用十六進制表示。
IPv6地址:每16位二進制位做1段,然後將每一段的二進制轉爲16進制。
補充(瞭解下即可):
關於IPv6協議有一種表示法叫做:0位壓縮表示法:
在某些情況下,一個IPv6地址中間可能包含很長的一段0,可以把連續的一段0壓縮爲“::”。但爲保證地址解析的唯一性,地址中”::”只能出現一次,例如:
在某些情況下,一個IPv6地址中間可能包含很長的一段0,可以把連續的一段0壓縮爲“::”。但爲保證地址解析的唯一性,地址中”::”只能出現一次,例如:
FF01:0:0:0:0:0:0:1101 → FF01::1101
例如:上述的 本地鏈接 IPv6 地址就是這種表示法 : fe80::c805:5e47:820f:174
常用命令
- 查看本機IP地址,在控制檯輸入:
ipconfig
- 檢查網絡是否連通,在控制檯輸入:
ping 空格 IP地址
ping 192.168.1.10
特殊的IP地址
- 本機IP地址:
127.0.0.1
、localhost
本地迴環地址 ,就是本機地址。。
端口號
我們通過ip可以找到網絡中具體的計算機。而具體需要訪問計算機中的哪個資源信息,這時由於計算機中運行的進程肯定很多,這時每個進程必須再給一個唯一的編號(標識)。通過這個標識才能保證我們可以沒有錯誤的訪問到指定ip地址的具體的那個進程上。
上述所說的編號(標識)就是一臺電腦上某個應用程序的端口。只有通過這個端口才能去訪問運行中的這個程序。
端口號:
A:端口是進程的唯一標識,每個進程都至少有一個端口;
B:不同進程的端口不能重複;
C:端口號是用兩個字節(16位的二進制數)表示的有效端口0~65535,0到1024之間的端口數字已經分配給本機的操作系統的應用程序佔用,因此後期我們書寫程序如果需要綁定端口,這時必須大於1024;
D:通過360可以查看每個進程的端口號;
接下來通過一個圖例來描述IP地址和端口號的作用,如下圖所示。
從上圖中可以清楚地看到,位於網絡中一臺計算機可以通過IP地址去訪問另一臺計算機,並通過端口號訪問目標計算機中的某個應用程序。
利用協議
+IP地址
+端口號
三元組合,就可以標識網絡中的進程了,那麼進程間的通信就可以利用這個標識與其它進程進行交互。
1.5 InetAddress類(理解)
在網絡中需要連接另外一段的設備,然後才能進行通信。而處於網絡中的設備都需要唯一的數字地址標識(IP)。
Java使用InetAddress類來描述IP這個事物:
說明:
1)網絡編程中的類或接口都位於java.net包下;
2)InetAddress 類將IP地址進行了封裝;
3)InetAddress 類的對象中一定包含IP地址,但是有可能包含相應的主機名(主要取決於創建對象時調用的函數);
4)InetAddress類沒有對外提供構造函數,其中提供靜態的方法可以根據指定的主機(域名)找到對應的IP,然後把IP封裝成InetAddress對象。
InetAddress 類的主要函數如下所示:
A:static InetAddress getByName(String host) 在給定主機名的情況下確定主機的 IP 地址。
說明:
1)通過這個函數獲取的對象中肯定包含IP地址和主機名;
2)域名如:www.baidu.com也可以作爲主機名,即這個函數的參數封裝成對象來獲取IP,或者主機名如:suoge也可以作爲這個函數的參數;
3)同樣也可以將一個字符串形式的IP地址作爲此函數的參數來封裝成對象,如192.168.71.100;
說明:如果參數爲IP地址時,封裝的對象中不一定包含主機名。有可能包含的情況是:在給IP地址時,有可能會反向解析,但是這種解析有可能會失敗。解析不到,那麼對象中就不會有主機名。
由於InetAddress類的對象中封裝的是IP地址和主機名,所以通過以下函數可以獲取對象中的IP和主機名:
B:String getHostAddress()返回 IP 地址字符串(以文本表現形式)。
C:String getHostName()獲取此 IP 地址的主機名。
上述函數代碼演示如下所示:
分析和步驟:
1)創建測試類InetAddressDemo ,在測試類中定義一個main函數;
2)根據InetAddress調用getByName()函數獲取InetAddress類的對象ia;
3)使用InetAddress類的對象ia調用 getHostAddress()函數獲取指定主機的IP地址;
4)使用InetAddress類的對象ia調用getHostName()函數IP 地址的主機名;
5)輸出上述獲取的IP地址和主機名;
6)使用InetAddress類調用getAllByName獲取InetAddress類的對象並放到數組中,遍歷數組依次打印數組中的內容;
/*
* 演示:InetAddress的基本使用
* 封裝IP對象:
* static InetAddress getByName(String host) 在給定主機名的情況下確定主機的 IP 地址。
* 獲取IP和主機信息:
* String getHostAddress()返回 IP 地址字符串(以文本表現形式)。
* String getHostName()獲取此 IP 地址的主機名。
*/
public class InetAddressDemo {
public static void main(String[] args) throws Exception {
// static InetAddress getByName(String host) 在給定主機名的情況下確定主機的 IP 地址。
// suoge表示我的電腦主機名字
InetAddress ia = InetAddress.getByName("suoge");// suoge/192.168.100.107
// InetAddress ia = InetAddress.getByName("192.168.100.107");//
// /192.168.100.107
// System.out.println(ia);
// String getHostAddress()返回 IP 地址字符串(以文本表現形式)。
String ip = ia.getHostAddress();
// String getHostName()獲取此 IP 地址的主機名。
String hostName = ia.getHostName();
/*
* InetAddress ia = InetAddress.getByName("suoge"):
* 192.168.100.107=====suoge
* 說明當使用主機名作爲函數參數時將計算機名和IP地址同時封裝在該對象中,此時調用getHostName獲取IP 地址的主機名時,就會獲取到指定的計算機名suoge
* ================================================================
* InetAddress ia = InetAddress.getByName("192.168.100.107"):
* 192.168.100.107=====192.168.100.107
* 說明當ip地址作爲函數參數時沒有將計算機名封裝在該對象中,此時調用getHostName獲取IP 地址的主機名 時,就會獲取到指定的IP
*/
System.out.println(ip + "=====" + hostName);
System.out.println("-------------------------");
}
}
第二章 TCP通信程序
2.1 概述
TCP通信是嚴格區分客戶端與服務器端的,在通信時,必須先由客戶端去連接服務器端才能實現通信,服務器端不可以主動連接客戶端,並且服務器端程序需要事先啓動,等待客戶端的連接。
在JDK中提供了兩個類用於實現TCP程序,一個是ServerSocket類,用於表示服務器端,一個是Socket類,用於表示客戶端。
通信時,首先創建代表服務器端的ServerSocket對象,該對象相當於開啓一個服務,並等待客戶端的連接,然後創建代表客戶端的Socket對象向服務器端發出連接請求,服務器端響應請求,兩者建立連接開始通信。
回顧TCP協議的特點:
A:先建立連接通道
B:安全可靠
C:效率低
D:不限制大小
說明:
1)TCP協議套接字有客戶端和服務端的區分。
2)使用TCP協議進行通信的時候,客戶端和服務端之間必須先建立好連接通道(客戶端和服務端的通信的橋樑),
客戶端和服務端就可以在這個通道進行數據的傳輸。
也就是說在TCP協議中,要進行通信,必須建立連接通道,數據的傳輸都是依賴於通道。
注意:
A:客戶端可以訪問服務端,但是客戶端與客戶端之間不能連接。
B:一個服務端,可以接受多個客戶端的訪問連接。
舉例:我們生活中經常訪問的淘寶、京東都是服務端,而我們每個人使用瀏覽器訪問就屬於客戶端。
2.2 Socket類
Socket
類:該類實現客戶端套接字,套接字指的是兩臺設備之間通訊的端點。
構造方法
Socket(String host, int port) 創建一個流套接字並將其連接到指定主機上的指定端口號
說明:
A:第一個參數host表示服務器端的主機名,也可以是服務器端的IP地址,只不過是String類型的。第二個參數port表示服務器端的端口;
B:對於客戶端套接字,一旦創建對象,就會嘗試與目標IP和端口的服務端建立連接通道;
注意:客戶端套接字,一旦創建,就會嘗試與目標IP和端口的服務端建立連接通道。
構造舉例,代碼如下:
Socket client = new Socket("127.0.0.1", 6666);
2.3 ServerSocket類
說明:ServerSocket類用來描述服務器端的套接字,創建完這個類的對象之後,那麼就會等待客戶端來訪問,客戶端有請求之後,服務器端有可能會有返回結果,有可能也會沒有,具體由我們書寫的代碼來決定。
構造方法
ServerSocket服務器端的套接字的構造函數如下所示:
ServerSocket(int port) 創建綁定到特定端口的服務器套接字。
說明:創建服務器端套接字對象時必須指定端口,只有這樣,客戶端訪問服務器端的時候才能根據指定的端口來訪問。
構造舉例,代碼如下:
ServerSocket server = new ServerSocket(6666);
2.4如何發送和接收數據呢?
TCP協議,要進行通信,必須建立連接通道,數據的傳輸都是依賴於通道。
如果我們要發送數據:向通道中寫數據
如果我們要接收數據:從通道中讀取數據
任何Socket底層其實都是IO流。因此,
如果我們要發送數據:我們就需要獲取一個輸出流,目的地是通道。
如果我們要接收數據:我們就需要獲取一個輸入流,數據源是通道。
如何獲取一個與通道關聯的流呢?
使用Socket類中的以下兩個函數來完成:
InputStream getInputStream()返回此套接字的輸入流,數據源是通道。
OutputStream getOutputStream()返回此套接字的輸出流,目的地是通道。
2.5 發送數據(客戶端)
分析和步驟:
TCP客戶端的使用步驟:
A:創建客戶端套接字對象;
B:獲取輸出流,關聯通道;
C:寫數據;
D:釋放資源;
1)定義一個客戶端類ClientDemo ,在這個類中定義一個main函數;
2)創建客戶端套接字對象s,指定一個服務器端IP地址:192.168.100.107,端口是10000;
3)使用客戶端套接字對象s調用getOutputStream()函數獲取輸出流對象out,關聯通道;
4)使用輸出流對象out調用write函數寫數據,一串字符串變爲字節數據作爲函數的參數;
5)使用客戶端套接字對象s調用close()函數關閉系統資源;
/*
* 演示:TCP的客戶端
* 構造函數:
* Socket(String host, int port) 創建一個流套接字並將其連接到指定主機上的指定端口號
* 獲取輸出流:
* OutputStream getOutputStream()返回此套接字的輸出流,目的地是通道。
* TCP客戶端的使用步驟:
* A:創建客戶端套接字對象;
* B:獲取輸出流,關聯通道;
* C:寫數據;
* D:釋放資源;
*注意:TCP需要先建立鏈接,因此,服務端必須先存在,客戶端才能執行,否則會報如下異常:
*Exception in thread "main" java.net.ConnectException: Connection refused: connect
*/
public class ClientDemo {
public static void main(String[] args) throws Exception, IOException {
// A:創建客戶端套接字對象;
Socket s = new Socket("192.168.100.107",10000);
//B:獲取輸出流,關聯通道;
OutputStream os = s.getOutputStream();
//C:寫數據;
os.write("hello,TCP,我來了".getBytes());
//D:釋放資源;
//os.close();
s.close();//在關閉客戶端的同時,這個通道輸出流也會被關閉
}
}
說明:
1)在關閉客戶端的同時,這個通道輸出流,也會被關閉,所以不用在單獨關閉輸出流;
2)TCP需要先建立連接,因此,服務端必須先存在,客戶端才能執行。否則會報如下異常:
java.net.ConnectException: Connection refused: connect
2.6 接收數據
分析:
我們發現在ServerSocket類中並沒有獲取輸入流的方法。那麼原因是什麼呢?
假設ServerSocket真的給我們提供了getInputStream方法,那麼與ServerSocket連接的客戶端有很多個,通道也有很多個。這個時候,如果我們調用了getInputStream方法,也不知道獲取的是哪個通道的輸入流,這樣很混亂。
解決辦法:假如我們可以獲取到一個具體的客戶端Socket對象,然後從這個對象中獲取輸入或者輸出流,這樣拿到的就是具體某個通道的流了。
問題來了:如何獲取到某個客戶端Socket對象呢?
可以使用ServerSocket類中的 Socket accept() 函數,這個函數表示偵聽並接受到此套接字的連接。 此方法在連接傳入之前一直阻塞。
TCP服務端的使用步驟:
A:創建服務端套接字;
B:偵聽並獲取客戶端套接字;
C:獲取輸入流,數據源是通道;
D:讀取數據;
E:釋放資源;
/*
* 演示:TCP的服務端
* 構造函數:
* ServerSocket(int port) 創建綁定到特定端口的服務器套接字。
* 獲取輸入流:
* InputStream getInputStream()返回此套接字的輸入流,數據源是通道。
* 獲取某個客戶端的Socket對象:
* Socket accept() 偵聽並接受到此套接字的連接。
* TCP服務端的使用步驟:
* A:創建服務端套接字;
* B:偵聽並獲取客戶端套接字;
* C:獲取輸入流,數據源是通道;
* D:讀取數據;
* E:釋放資源;
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
// A:創建服務端套接字;
ServerSocket serverSocket = new ServerSocket(10000);
// B:偵聽並獲取客戶端套接字;
Socket socket = serverSocket.accept();
//C:獲取輸入流,數據源是通道;
InputStream is = socket.getInputStream();
//D:讀取數據;
byte[] buf=new byte[1024];
int len=0;
while((len=is.read(buf))!=-1)
{
//輸出數據
System.out.println(new String(buf,0,len));
}
//關閉資源
socket.close();
serverSocket.close();
}
}
2.7 TCP傳輸的簡單圖解
第三章 綜合案例
3.1 文件上傳案例(帶反饋信息的文件的上傳實現)
文件上傳分析圖解
- 【客戶端】輸入流,從硬盤讀取文件數據到程序中。數據源是客戶端的本地硬盤文件。
- 【客戶端】輸出流,寫出文件數據到通道。目的地是通道。
- 【服務端】輸入流,從通道中讀取數據到服務端程序。數據源是通道。
- 【服務端】輸出流,將讀取的數據寫出到服務器硬盤中。目的地是服務器硬盤。
基本實現
客戶端代碼:
分析:
數據源:文件,FileReader。關聯項目下的bw.txt文件。一次讀取一行。readLine()
目的地:通道,getOutputStream,所以也需要轉爲字符流,OutputStreamWriter,然後需要換行,所以用BufferedWriter。
/*
* TCP的客戶端 帶反饋信息的文件的上傳實現
* 數據源:文件,FileReader。
* 目的地:通道,getOutputStream,所以也需要轉爲字符流,OutputStreamWriter,然後需要換行,所以用BufferedWriter。
*/
public class ClientDemo {
public static void main(String[] args) throws Exception{
// 創建客戶端套接字對象
Socket s = new Socket("127.0.0.1", 16888);
//創建輸入流關聯數據源文件
BufferedReader br = new BufferedReader(new FileReader("bw.txt"));
//創建輸出流關聯通道
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
//讀寫數據
String line=null;
//這裏是讀取數據,讀取到文件末尾,有結束標識null,讀取完畢循環結束
while((line=br.readLine())!=null)
{
bw.write(line);
bw.newLine();
bw.flush();
}
//關閉資源
br.close();//由於輸入流直接關聯的是文件,所以要單獨關閉資源
s.close();
}
}
服務端代碼:
分析:
數據源:通道,getInputStream,需要轉爲字符流。
目的地:工程下面的一個文件 copy.txt
/*
* TCP服務端代碼:
* 分析:
* 數據源:通道,getInputStream,需要轉爲字符流。
* 目的地:工程下面的一個文件 copy.txt
*/
public class ServerDemo {
public static void main(String[] args) throws Exception{
// 創建服務端套接字對象
ServerSocket ss = new ServerSocket(16888);
// 偵聽並獲取客戶端套接字對象
Socket s = ss.accept();
// 獲取輸入流,關聯通道
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 創建輸出流關聯目的地文件
BufferedWriter bw = new BufferedWriter(new FileWriter("copy.txt"));
// 讀取數據
String line = null;
while ((line = br.readLine()) != null) {
// 將從客戶端獲取的數據寫到目的地文件中
bw.write(line);
bw.newLine();
bw.flush();
}
// 關閉資源
s.close();
ss.close();
bw.close();//由於輸出流直接關聯的是文件,所以要單獨關閉資源
}
}
文件上傳優化分析
1.文件名稱寫死的問題
服務端,保存文件的名稱如果寫死,那麼最終導致服務器硬盤,只會保留一個文件,建議使用系統時間優化,保證文件名稱唯一,代碼如下:
// 創建輸出流關聯目的地文件
//System.currentTimeMillis()+".txt" 表示文件名稱 例如:17161616156.txt
BufferedWriter bw = new BufferedWriter(new FileWriter(System.currentTimeMillis()+".txt"));
2.循環接收的問題
服務端,只保存一個文件就關閉了,之後的用戶無法再上傳,這是不符合實際的,使用循環改進,可以不斷的接收不同用戶的文件,代碼如下:
// 1. 創建服務端套接字對象
ServerSocket ss = new ServerSocket(16888);
// 每次接收新的連接,獲取一個新的Socket
while(true){
// 偵聽並獲取客戶端套接字對象
Socket s = ss.accept();
// 獲取輸入流,關聯通道
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 創建輸出流關聯目的地文件
BufferedWriter bw = new BufferedWriter(new FileWriter("day11\\"+System.currentTimeMillis()+".txt"));
// 讀取數據
String line = null;
while ((line = br.readLine()) != null) {
// 將從客戶端獲取的數據寫到目的地文件中
bw.write(line);
bw.newLine();
bw.flush();
}
// 關閉資源
s.close();
// ss.close();//此時要注意了,由於要一直使用服務端Socket對象ss,所以這裏要關閉
bw.close();//由於輸出流直接關聯的是文件,所以要單獨關閉資源
}
3.效率問題
服務端,在接收大文件時,可能耗費幾秒鐘的時間,此時不能接收其他用戶上傳,所以,使用多線程技術優化,代碼如下:
public static void main(String[] args) throws Exception{
// 創建服務端套接字對象
ServerSocket ss = new ServerSocket(16888);
while (true) {
// 偵聽並獲取客戶端套接字對象
Socket s = ss.accept();
//s交給子線程來處理
new Thread(()->{
try {
// 獲取輸入流,關聯通道
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 創建輸出流關聯目的地文件
BufferedWriter bw = new BufferedWriter(new FileWriter("day11\\"+System.currentTimeMillis()+".txt"));
// 讀取數據
String line = null;
while ((line = br.readLine()) != null) {
// 將從客戶端獲取的數據寫到目的地文件中
bw.write(line);
bw.newLine();
bw.flush();
}
// 關閉資源
s.close();
// ss.close();
bw.close();//由於輸出流直接關聯的是文件,所以要單獨關閉資源
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
優化實現
public class ServerDemo {
public static void main(String[] args) throws Exception {
// 1. 創建服務端套接字對象
ServerSocket ss = new ServerSocket(12306);
// 2. 循環接收,建立連接
while (true) {
// 偵聽並獲取客戶端套接字對象
Socket s = ss.accept();
/*
3. socket對象交給子線程處理,進行讀寫操作
Runnable接口中,只有一個run方法,使用lambda表達式簡化格式
*/
new Thread(() -> {
try (// 3.1 獲取輸入流,關聯通道
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 3.2 創建輸出流關聯目的地文件 System.currentTimeMillis()+".txt" 表示文件名稱 例如:17161616156.txt
BufferedWriter bw = new BufferedWriter(new FileWriter("day11\\" + System.currentTimeMillis() + ".txt"));) {
// 3.3 讀取數據
String line = null;
while ((line = br.readLine()) != null) {
// 將從客戶端獲取的數據寫到目的地文件中
bw.write(line);
bw.newLine();
bw.flush();
}
s.close();
bw.close();//由於輸出流直接關聯的是文件,所以要單獨關閉資源
// ss.close();//這裏千萬不要將服務端流進行關閉
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
信息回寫分析圖解
前四步與基本文件上傳一致.
- 【服務端】獲取輸出流,向通道中給客戶端回寫數據。
- 【客戶端】獲取輸入流,從通道中讀取回寫數據。
回寫實現
客戶端代碼:
/*
* TCP的客戶端 帶反饋信息的文件的上傳實現
* 數據源:文件,FileReader。
* 目的地:通道,getOutputStream,所以也需要轉爲字符流,OutputStreamWriter,然後需要換行,所以用BufferedWriter。
*/
public class ClientDemo {
public static void main(String[] args) throws Exception{
// 創建客戶端套接字對象
Socket s = new Socket("127.0.0.1", 16888);
//創建輸入流關聯數據源文件
BufferedReader br = new BufferedReader(new FileReader("day11\\bw.txt"));//day11表示模塊名
//創建輸出流關聯通道
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
//讀寫數據
String line=null;
//這裏是讀取數據,讀取到文件末尾,有結束標識null,讀取完畢循環結束
while((line=br.readLine())!=null)
{
bw.write(line);
bw.newLine();
bw.flush();
}
/*
* 原因:客戶端沒有向通道中髮結束標記
* 解決:發送結束標記。
* 1)約定一個結束條件。
* 但是,如果文件中本身內容包含了結束標記,就有可能出錯。
* 舉例:我們在bw.txt書寫如下內容:
* 你好嗎
* over
* 在嗎
* 我是鎖哥
* 上傳服務器成功之後,服務器文件中內容應該是:
* 你好嗎
* over
* 在嗎
* 我是鎖哥
* 但是結果只有:
* 你好嗎
* 因爲當服務器端讀取數據的時候,遇到了over,那麼就會停止服務端的while循環,就會結束上傳,直接反饋,這樣導致數據丟失。
* 2)API中提供了結束方法:void shutdownOutput() 終止輸出流,發送結束標記
*/
//客戶端發完數據後,發送結束標記。
/*bw.write("over");
bw.newLine();
bw.flush();*/
s.shutdownOutput();
//接收反饋信息
//獲得輸入流
InputStream is = s.getInputStream();
byte[] buf=new byte[1024];
//客戶端:讀取文本文件,可以讀取到結束標記,然後循環結束,等待服務端反饋信息,於是阻塞了。
int len = is.read(buf);//這個read函數接收反饋信息是一個阻塞的方法,此時jvm停留在這裏了
System.out.println(new String(buf,0,len));
//關閉資源
br.close();//由於輸入流直接關聯的是文件,所以要單獨關閉資源
s.close();
}
}
服務端代碼:
/*
* TCP服務端代碼:
* 分析:
* 數據源:通道,getInputStream,需要轉爲字符流。
* 目的地:工程下面的一個文件 copy.txt
* 分析:
* 客戶端:讀取文本文件,可以讀取到結束標記,然後循環結束,等待服務端反饋信息,於是阻塞了。
* 服務端:讀取通道數據,客戶端並沒有在通道寫結束標記,服務端不知道客戶端已經結束了。
* 於是,服務端還在等待客戶端傳遞數據,肯定不會發反饋信息。於是服務端也阻塞。
* 原因:客戶端沒有向通道中髮結束標記
* 解決:發送結束標記。
* 1)約定一個結束條件。
* 但是,如果文件中本身內容包含了結束標記,就有可能出錯。
* 2)API中提供了結束方法:void shutdownOutput() 終止輸出流,發送結束標記
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
// 1. 創建服務端套接字對象
ServerSocket ss = new ServerSocket(12306);
// 2. 循環接收,建立連接
while (true) {
// 偵聽並獲取客戶端套接字對象
Socket s = ss.accept();
/*
3. socket對象交給子線程處理,進行讀寫操作
Runnable接口中,只有一個run方法,使用lambda表達式簡化格式
*/
new Thread(() -> {
try (// 3.1 獲取輸入流,關聯通道
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 3.2 創建輸出流關聯目的地文件 System.currentTimeMillis()+".txt" 表示文件名稱 例如:17161616156.txt
BufferedWriter bw = new BufferedWriter(new FileWriter("day06\\" + System.currentTimeMillis() + ".txt"));) {
// 3.3 讀取數據
String line = null;
while ((line = br.readLine()) != null) {
// 將從客戶端獲取的數據寫到目的地文件中
bw.write(line);
bw.newLine();
bw.flush();
}
//寫反饋信息
OutputStream os = s.getOutputStream();
os.write("上傳成功".getBytes());
s.close();
bw.close();//由於輸出流直接關聯的是文件,所以要單獨關閉資源
// ss.close();//這裏千萬不要將服務端流進行關閉
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
說明:
查看上述代碼反饋信息的效果辦法:
1)運行服務端ServerDemo 類;
2)運行客戶端ClientDemo 類;
運行結果:
客戶端和服務端產生互相等待現象。程序卡住了。
說明:上述代碼書寫完反饋信息之後,運行出現了問題:
程序運行時出現客戶端和服務端互相等待的現象:文件已經完成了上傳,但是客戶端並沒有接收到反饋信息。
分析出現上述客戶端和服務端互相等待問題的原因:
客戶端:讀取文本文件,可以讀取到結束標記,然後循環結束,等待服務端反饋信息,於是阻塞了。
阻塞在:int len = is.read(buf) 函數這裏了,在等待服務端反饋信息。
服務端:讀取通道數據,客戶端並沒有在通道寫結束標記,服務端不知道客戶端已經結束了。
於是,服務端還在等待客戶端傳遞數據,肯定不會發反饋信息。於是服務端也阻塞。
阻塞在:while ((line = br.readLine()) != null) 。readLine函數阻塞。
產生的原因:客戶端沒有向通道中髮結束標記。
解決:發送結束標記。
1)約定一個結束條件。
bw.write("over");
bw.newLine();
bw.flush();
但是,如果文件中本身內容包含了結束標記,就有可能出錯。
舉例:我們在bw.txt書寫如下內容:
你好嗎
over
在嗎
我是鎖哥
上傳服務器成功之後,服務器文件中內容應該是:
你好嗎
over
在嗎
我是鎖哥
但是結果只有:
你好嗎
因爲當服務器端讀取數據的時候,遇到了over,那麼就會停止服務端的while循環,就會結束上傳,直接反饋,這樣導致數據丟失。
2)在Socket類的API中提供了結束方法:void shutdownOutput() 終止輸出流,發送結束標記,可以使用這個函數給服務端發送結束標記。
注意:客戶端Socket類中的shutdownOutput()函數表示向服務器端發送結束標記。
3.2 模擬服務器(擴展)
模擬網站服務器,使用瀏覽器訪問自己編寫的服務端程序,查看網頁效果。
案例分析
-
準備頁面數據,web文件夾。
將今天下發的資料文件夾中的web整個文件夾複製到項目day11下面
- 我們模擬服務器端,ServerSocket類監聽端口,使用瀏覽器訪問
模擬服務器代碼如下所示:
public static void main(String[] args) throws IOException {
// 創建服務端套接字對象
ServerSocket ss = new ServerSocket(10086);
//偵聽並獲取客戶端套接字
Socket s = ss.accept();
//創建輸入流關聯通道
InputStream is = s.getInputStream();
//讀取數據
byte[] buf=new byte[1024*100];
int len = is.read(buf);
//輸出數據
System.out.println(new String(buf,0,len));
s.close();
ss.close();
}
谷歌瀏覽器訪問:
http://127.0.0.1:10086/web/index.html
瀏覽器的請求:
請求中的格式分成三部分:
1)請求行
GET /web/index.html HTTP/1.1
請求行由三部分組成:
A:請求方式:GET。GET是客戶端向服務端發送請求時使用的請求方式。請求方式有8種,現在流行只有2種。GET、POST。
B:請求資源路徑:/web/index.html。也可以這麼理解:/開始表示的是客戶端請求服務端的資源信息。而這裏web/index.html屬於服務端項目下的資源信息
C:請求協議:HTTP/1.1。HTTP/1.1是客戶端請求服務端時使用的協議以及版本。
2)請求頭
Host: 127.0.0.1:10086
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8
說明:請求頭由key和value值組成,key和value之間使用冒號隔開。
3)請求體
在請求頭下面,空行後,就是請求體。一般post請求才有請求體
用戶發送的真實數據。 請求體需要和請求頭之間用空格隔開。
只有請求方式是Post的時候,纔有請求體,如果是get方式,是沒有請求體數據的。
由於我們這裏屬於get請求,所以沒有請求體的數據。
總結:通過上述案例我們知道其實瀏覽器內部就是個套接字。
根據以上分析,客戶端想訪問服務器端的web/index.html頁面資源,我們可以使用字符串切割方式獲取到請求的資源。在服務端進行讀取並返回給客戶端瀏覽器。
//轉換流,讀取瀏覽器請求第一行
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
//讀取第一行數據 GET /web/index.html HTTP/1.1
String requst = br.readLine();
//取出請求資源的路徑 按照空格進行切割,然後我們需要索引是1的資源路徑
String[] strArr = requst.split(" ");
//strArr[1]--->/web/index.html
//去掉web前面的/
String path = strArr[1].substring(1);//path---->web/index.html
System.out.println(path);
案例實現
服務端實現:
說明:服務端再向瀏覽器通道中寫數據之前,需要寫入HTTP協議響應頭,屬於固定寫法。
/*
響應頭中包含響應的http協議版本HTTP/1.1
服務器返回的狀態碼 200
狀態值:OK
*/
os.write("HTTP/1.1 200 OK\r\n".getBytes());
//Content-Type:text/html表示響應文本的類型
os.write("Content-Type:text/html\r\n".getBytes());
// 必須要寫入空行,否則瀏覽器不解析
os.write("\r\n".getBytes());
具體代碼如下:
public class ServerDemo {
public static void main(String[] args) throws Exception{
System.out.println("服務端 啓動 , 等待連接 .... ");
// 創建服務端套接字對象
ServerSocket ss = new ServerSocket(10086);
//偵聽並獲取客戶端套接字
Socket s = ss.accept();
//創建輸入流關聯通道
InputStream is = s.getInputStream();
//將字節流變爲字符緩衝流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//讀取瀏覽器的請求信息 即請求行 GET /web/index.html HTTP/1.1
String request = br.readLine();
//按空格進行切割
String[] arr = request.split(" ");
//拿出請求資源路徑,並將web前面的/去掉
String path = arr[1].substring(1);
// System.out.println(path);//web/index.html
//創建字節輸入流對象關聯客戶端請求服務器端的資源文件即web/index.html中的內容
FileInputStream fis = new FileInputStream(path);//相對路徑
//定義數組
byte[] buf = new byte[1024];
//定義變量保存每次讀取的字節個數
int len=0;
//獲取向通道寫數據的字節輸出流
OutputStream os = s.getOutputStream();
// 寫入HTTP協議響應頭,固定寫法
/*
響應頭中包含響應的http協議版本HTTP/1.1
服務器返回的狀態碼 200
狀態值:OK
*/
os.write("HTTP/1.1 200 OK\r\n".getBytes());
//Content-Type:text/html表示響應文本的類型
os.write("Content-Type:text/html\r\n".getBytes());
// 必須要寫入空行,否則瀏覽器不解析
os.write("\r\n".getBytes());
//循環
while ((len=fis.read(buf))!=-1) {
//向通道中書寫讀取的數據
os.write(buf,0,len);
}
s.close();
ss.close();
}
}
訪問效果
小貼士:不同的瀏覽器,內核不一樣,解析效果有可能不一樣。
發現瀏覽器中出現很多的叉子,說明瀏覽器沒有讀取到圖片信息導致。
瀏覽器工作原理是遇到圖片會開啓一個線程進行單獨的訪問,因此在服務器端加入線程技術。
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("服務端 啓動 , 等待連接 .... ");
// 創建服務端套接字對象
ServerSocket ss = new ServerSocket(10086);
while (true) {
//偵聽並獲取客戶端套接字
Socket s = ss.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
//創建輸入流關聯通道
InputStream is = s.getInputStream();
//將字節流變爲字符緩衝流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//讀取瀏覽器的請求信息 即請求行 GET /web/index.html HTTP/1.1
String request = br.readLine();
//按空格進行切割
String[] arr = request.split(" ");
//拿出請求資源路徑,並將web前面的/去掉
String path = arr[1].substring(1);
// System.out.println(path);//web/index.html
//創建字節輸入流對象關聯客戶端請求服務器端的資源文件即web/index.html中的內容
FileInputStream fis = new FileInputStream(path);//相對路徑
//定義數組
byte[] buf = new byte[1024];
//定義變量保存每次讀取的字節個數
int len = 0;
//獲取向通道寫數據的字節輸出流
OutputStream os = s.getOutputStream();
// 寫入HTTP協議響應頭,固定寫法
/*
響應頭中包含響應的http協議版本HTTP/1.1
服務器返回的狀態碼 200
狀態值:OK
*/
os.write("HTTP/1.1 200 OK\r\n".getBytes());
//Content-Type:text/html表示響應文本的類型
os.write("Content-Type:text/html\r\n".getBytes());
// 必須要寫入空行,否則瀏覽器不解析
os.write("\r\n".getBytes());
//循環
while ((len = fis.read(buf)) != -1) {
//向通道中書寫讀取的數據
os.write(buf, 0, len);
}
s.close();//關閉客戶端
} catch (Exception e) {
}
}
}).start();
}
}
}
訪問效果:
訪問過程圖解:
通過上述代碼我們可以書寫代碼模擬服務端,其實在企業真實開發過程中我們是很少自己書寫代碼模擬服務器的,我們使用的是第三方服務器公司給我們提供好的服務器,
我們現在企業開發中比較主流的服務器有Tomcat、JBoss、Oracle WebLogic 等。其中最爲流行的服務器就是Apache 公司的Tomcat服務器。
第四章 軟件架構
軟件架構:
CS:Client/Server 客戶端/服務端。
特點:開發軟件的時候,程序員需要開發2套軟件,一個是給用戶使用的客戶端程序,一個是給服務器運行服務端程序。
好處:可以把一些運算放在客戶端的電腦上運行。降低服務器的壓力。
弊端:開發時,需要開發兩個程序(軟件)。這樣會導致開發週期長,開發的成本高,後期維護不方便的問題。
BS:Browser / Server 瀏覽器/ 服務端(基於網頁開發的).
瀏覽器通常是由第三方公司提供。常用:IE、谷歌、火狐。
特點:程序員在開發的時候,只需要開發服務端的程序即可。
好處:因爲只需要開發一端程序(軟件),開發成本降低、開發週期縮短、後期的維護也相對方便一些。
弊端:因爲只有一端程序在運行,把以前可以在客戶端運行的計算,全部轉嫁到服務端,這樣導致服務端壓力比較大。
結論:兩種架構各有優勢,但是無論哪種架構,都離不開網絡的支持。