一、網絡編程三要素
1、協議:計算機網絡通信必須遵守的規則。
網絡通信協議:通過計算機網絡可以使多臺計算機實現連接,位於同一個網絡中的計算機在進行連接和通信時需要遵守一定的規則,這就好比在道路中行駛的汽車一定要遵守交通規則一樣。在計算機網絡中,這些連接和通信的規則被稱爲網絡通信協議,它對數據的傳輸格式、傳輸速率、傳輸步驟等做了統一規定,通信雙方必須同時遵守才能完成數據交換。
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協議中,在發送數據的準備階段,客戶端與服務器之間的三次交互,以保證連接的可靠。
- 第一次握手,客戶端向服務器端發出連接請求,等待服務器確認。
- 第二次握手,服務器端向客戶端回送一個響應,通知客戶端收到了連接請求。
- 第三次握手,客戶端再次向服務器端發送確認信息,確認連接。整個交互過程如下圖所示。
完成三次握手,連接建立後,客戶端和服務器就可以開始進行數據傳輸了。由於這種面向連接的特性,TCP協議可以保證傳輸數據的安全,所以應用十分廣泛,例如下載文件、瀏覽網頁等。
2、IP地址
- IP地址:指互聯網協議地址(Internet Protocol Address),俗稱IP。IP地址用來給一個網絡中的計算機設備做唯一的編號。假如我們把“個人電腦”比作“一臺電話”的話,那麼“IP地址”就相當於“電話號碼”。
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
,號稱可以爲全世界的每一粒沙子編上一個網址,這樣就解決了網絡地址資源數量不夠的問題。
常用命令
- 查看本機IP地址,在控制檯輸入:
ipconfig
- 檢查網絡是否連通,在控制檯輸入:
ping 空格 IP地址
ping 220.181.57.216
特殊的IP地址
本機IP地址:127.0.0.1
、localhost
。
3、端口號
網絡的通信,本質上是兩個進程(應用程序)的通信。每臺計算機都有很多的進程,那麼在網絡通信時,如何區分這些進程呢?
如果說IP地址可以唯一標識網絡中的設備,那麼端口號就可以唯一標識設備中的進程(應用程序)了。
端口號:用兩個字節表示的整數,它的取值範圍是0-65535。其中,0~1023之間的端口號用於一些知名的網絡服務和應用,普通的應用程序需要使用1024以上的端口號。如果端口號被另外一個服務或應用所佔用,會導致當前程序啓動失敗。
利用協議
+IP地址
+端口號
三元組合,就可以標識網絡中的進程了,那麼進程間的通信就可以利用這個標識與其它進程進行交互。
二、TCP通信程序
TCP通信能實現兩臺計算機之間的數據交互,通信的兩端,要嚴格區分爲客戶端(Client)與服務端(Server)。
兩端通信時步驟:
- 服務端程序,需要事先啓動,等待客戶端的連接。
- 客戶端主動連接服務器端,連接成功才能通信。服務端不可以主動連接客戶端。
在Java中,提供了兩個類用於實現TCP通信程序:
- 客戶端:
java.net.Socket
類表示。創建Socket
對象,向服務端發出連接請求,服務端響應請求,兩者建立連接開始通信。 - 服務端:
java.net.ServerSocket
類表示。創建ServerSocket
對象,相當於開啓一個服務,並等待客戶端的連接。
TCP通信原理:
1.Socket類
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()
: 返回此套接字的輸入流。- 如果此Scoket具有相關聯的通道,則生成的InputStream 的所有操作也關聯該通道。
- 關閉生成的InputStream也將關閉相關的Socket。
-
public OutputStream getOutputStream()
: 返回此套接字的輸出流。- 如果此Scoket具有相關聯的通道,則生成的OutputStream 的所有操作也關聯該通道。
- 關閉生成的OutputStream也將關閉相關的Socket。
-
public void close()
:關閉此套接字。- 一旦一個socket被關閉,它不可再使用。
- 關閉此socket也將關閉相關的InputStream和OutputStream 。
-
public void shutdownOutput()
: 禁用此套接字的輸出流。- 任何先前寫出的數據將被髮送,隨後終止輸出流。
2. ServerSocket類
ServerSocket
類:這個類實現了服務器套接字,該對象等待通過網絡的請求。
構造方法
public ServerSocket(int port)
:使用該構造方法在創建ServerSocket對象時,就可以將其綁定到一個指定的端口號上,參數port就是端口號。
構造舉例,代碼如下:
ServerSocket server = new ServerSocket(6666);
成員方法
public Socket accept()
:偵聽並接受連接,返回一個新的Socket對象,用於和客戶端實現通信。該方法會一直阻塞直到建立連接。
3.簡單的TCP網絡程序
TCP通信分析圖解
-
【服務端】啓動,創建ServerSocket對象,等待連接。
-
【客戶端】啓動,創建Socket對象,請求連接。
-
【服務端】接收連接,調用accept方法,並返回一個Socket對象。
-
【客戶端】Socket對象,獲取OutputStream,向服務端寫出數據。
-
【服務端】Scoket對象,獲取InputStream,讀取客戶端發送的數據。
-
【服務端】Socket對象,獲取OutputStream,向客戶端回寫數據。
-
【客戶端】Scoket對象,獲取InputStream,解析回寫數據。
-
【客戶端】釋放資源,斷開連接。
TCPClient代碼:
/*
TCP通信的客戶端:向服務器發送連接請求,給服務器發送數據,讀取服務器回寫的數據
表示客戶端的類:
java.net.Socket:此類實現客戶端套接字(也可以就叫“套接字”)。套接字是兩臺機器間通信的端點。
套接字:包含了IP地址和端口號的網絡單位
構造方法:
Socket(String host, int port) 創建一個流套接字並將其連接到指定主機上的指定端口號。
參數:
String host:服務器主機的名稱/服務器的IP地址
int port:服務器的端口號
成員方法:
OutputStream getOutputStream() 返回此套接字的輸出流。
InputStream getInputStream() 返回此套接字的輸入流。
void close() 關閉此套接字。
實現步驟:
1.創建一個客戶端對象Socket,構造方法綁定服務器的IP地址和端口號
2.使用Socket對象中的方法getOutputStream()獲取網絡字節輸出流OutputStream對象
3.使用網絡字節輸出流OutputStream對象中的方法write,給服務器發送數據
4.使用Socket對象中的方法getInputStream()獲取網絡字節輸入流InputStream對象
5.使用網絡字節輸入流InputStream對象中的方法read,讀取服務器回寫的數據
6.釋放資源(Socket)
注意:
1.客戶端和服務器端進行交互,必須使用Socket中提供的網絡流,不能使用自己創建的流對象
2.當我們創建客戶端對象Socket的時候,就會去請求服務器和服務器經過3次握手建立連接通路
這時如果服務器沒有啓動,那麼就會拋出異常ConnectException: Connection refused: connect
如果服務器已經啓動,那麼就可以進行交互了
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
//1.創建一個客戶端對象Socket,構造方法綁定服務器的IP地址和端口號
Socket socket = new Socket("127.0.0.1",8888);
//2.使用Socket對象中的方法getOutputStream()獲取網絡字節輸出流OutputStream對象
OutputStream os = socket.getOutputStream();
//3.使用網絡字節輸出流OutputStream對象中的方法write,給服務器發送數據
os.write("你好服務器".getBytes());
//4.使用Socket對象中的方法getInputStream()獲取網絡字節輸入流InputStream對象
InputStream is = socket.getInputStream();
//5.使用網絡字節輸入流InputStream對象中的方法read,讀取服務器回寫的數據
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
//6.釋放資源(Socket)
socket.close();
}
}
服務端代碼:
/*
TCP通信的服務器端:接收客戶端的請求,讀取客戶端發送的數據,給客戶端回寫數據
表示服務器的類:
java.net.ServerSocket:此類實現服務器套接字。
構造方法:
ServerSocket(int port) 創建綁定到特定端口的服務器套接字。
服務器端必須明確一件事情,必須的知道是哪個客戶端請求的服務器
所以可以使用accept方法獲取到請求的客戶端對象Socket
成員方法:
Socket accept() 偵聽並接受到此套接字的連接。
服務器的實現步驟:
1.創建服務器ServerSocket對象和系統要指定的端口號
2.使用ServerSocket對象中的方法accept,獲取到請求的客戶端對象Socket
3.使用Socket對象中的方法getInputStream()獲取網絡字節輸入流InputStream對象
4.使用網絡字節輸入流InputStream對象中的方法read,讀取客戶端發送的數據
5.使用Socket對象中的方法getOutputStream()獲取網絡字節輸出流OutputStream對象
6.使用網絡字節輸出流OutputStream對象中的方法write,給客戶端回寫數據
7.釋放資源(Socket,ServerSocket)
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//1.創建服務器ServerSocket對象和系統要指定的端口號
ServerSocket server = new ServerSocket(8888);
//2.使用ServerSocket對象中的方法accept,獲取到請求的客戶端對象Socket
Socket socket = server.accept();
//3.使用Socket對象中的方法getInputStream()獲取網絡字節輸入流InputStream對象
InputStream is = socket.getInputStream();
//4.使用網絡字節輸入流InputStream對象中的方法read,讀取客戶端發送的數據
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
//5.使用Socket對象中的方法getOutputStream()獲取網絡字節輸出流OutputStream對象
OutputStream os = socket.getOutputStream();
//6.使用網絡字節輸出流OutputStream對象中的方法write,給客戶端回寫數據
os.write("收到謝謝".getBytes());
//7.釋放資源(Socket,ServerSocket)
socket.close();
server.close();
}
}
三、文件上傳案例解析
文件上傳分析圖解
- 【客戶端】輸入流,從硬盤讀取文件數據到程序中。
- 【客戶端】輸出流,寫出文件數據到服務端。
- 【服務端】輸入流,讀取文件數據到服務端程序。
- 【服務端】輸出流,寫出文件數據到服務器硬盤中。
TCPClient客戶端代碼:
/*
文件上傳案例的客戶端:讀取本地文件,上傳到服務器,讀取服務器回寫的數據
明確:
數據源:c:\\1.jpg
目的地:服務器
實現步驟:
1.創建一個本地字節輸入流FileInputStream對象,構造方法中綁定要讀取的數據源
2.創建一個客戶端Socket對象,構造方法中綁定服務器的IP地址和端口號
3.使用Socket中的方法getOutputStream,獲取網絡字節輸出流OutputStream對象
4.使用本地字節輸入流FileInputStream對象中的方法read,讀取本地文件
5.使用網絡字節輸出流OutputStream對象中的方法write,把讀取到的文件上傳到服務器
6.使用Socket中的方法getInputStream,獲取網絡字節輸入流InputStream對象
7.使用網絡字節輸入流InputStream對象中的方法read讀取服務回寫的數據
8.釋放資源(FileInputStream,Socket)
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
//1.創建一個本地字節輸入流FileInputStream對象,構造方法中綁定要讀取的數據源
FileInputStream fis = new FileInputStream("c:\\1.jpg");
//2.創建一個客戶端Socket對象,構造方法中綁定服務器的IP地址和端口號
Socket socket = new Socket("127.0.0.1",8888);
//3.使用Socket中的方法getOutputStream,獲取網絡字節輸出流OutputStream對象
OutputStream os = socket.getOutputStream();
//4.使用本地字節輸入流FileInputStream對象中的方法read,讀取本地文件
int len = 0;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes))!=-1){
//5.使用網絡字節輸出流OutputStream對象中的方法write,把讀取到的文件上傳到服務器
os.write(bytes,0,len);
}
解決:上傳完文件,給服務器寫一個結束標記
void shutdownOutput() 禁用此套接字的輸出流。
對於 TCP 套接字,任何以前寫入的數據都將被髮送,並且後跟 TCP 的正常連接終止序列。
*/
socket.shutdownOutput();
//6.使用Socket中的方法getInputStream,獲取網絡字節輸入流InputStream對象
InputStream is = socket.getInputStream();
//7.使用網絡字節輸入流InputStream對象中的方法read讀取服務回寫的數據
while((len = is.read(bytes))!=-1){
System.out.println(new String(bytes,0,len));
}
//8.釋放資源(FileInputStream,Socket)
fis.close();
socket.close();
}
}
TCPServer服務器端代碼:
/*
文件上傳案例服務器端:讀取客戶端上傳的文件,保存到服務器的硬盤,給客戶端回寫"上傳成功"
明確:
數據源:客戶端上傳的文件
目的地:服務器的硬盤 d:\\upload\\1.jpg
實現步驟:
1.創建一個服務器ServerSocket對象,和系統要指定的端口號
2.使用ServerSocket對象中的方法accept,獲取到請求的客戶端Socket對象
3.使用Socket對象中的方法getInputStream,獲取到網絡字節輸入流InputStream對象
4.判斷d:\\upload文件夾是否存在,不存在則創建
5.創建一個本地字節輸出流FileOutputStream對象,構造方法中綁定要輸出的目的地
6.使用網絡字節輸入流InputStream對象中的方法read,讀取客戶端上傳的文件
7.使用本地字節輸出流FileOutputStream對象中的方法write,把讀取到的文件保存到服務器的硬盤上
8.使用Socket對象中的方法getOutputStream,獲取到網絡字節輸出流OutputStream對象
9.使用網絡字節輸出流OutputStream對象中的方法write,給客戶端回寫"上傳成功"
10.釋放資源(FileOutputStream,Socket,ServerSocket)
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//1.創建一個服務器ServerSocket對象,和系統要指定的端口號
ServerSocket server = new ServerSocket(8888);
//2.使用ServerSocket對象中的方法accept,獲取到請求的客戶端Socket對象
/*
讓服務器一直處於監聽狀態(死循環accept方法)
有一個客戶端上傳文件,就保存一個文件
*/
while(true){
Socket socket = server.accept();
/*
使用多線程技術,提高程序的效率
有一個客戶端上傳文件,就開啓一個線程,完成文件的上傳
*/
new Thread(new Runnable() {
//完成文件的上傳
@Override
public void run() {
try {
//3.使用Socket對象中的方法getInputStream,獲取到網絡字節輸入流InputStream對象
InputStream is = socket.getInputStream();
//4.判斷d:\\upload文件夾是否存在,不存在則創建
File file = new File("d:\\upload");
if(!file.exists()){
file.mkdirs();
}
/*
自定義一個文件的命名規則:防止同名的文件被覆蓋
規則:域名+毫秒值+隨機數
*/
String fileName = "itcast"+System.currentTimeMillis()+new Random().nextInt(999999)+".jpg";
//5.創建一個本地字節輸出流FileOutputStream對象,構造方法中綁定要輸出的目的地
//FileOutputStream fos = new FileOutputStream(file+"\\1.jpg");
FileOutputStream fos = new FileOutputStream(file+"\\"+fileName);
//6.使用網絡字節輸入流InputStream對象中的方法read,讀取客戶端上傳的文件
int len =0;
byte[] bytes = new byte[1024];
while((len = is.read(bytes))!=-1){
//7.使用本地字節輸出流FileOutputStream對象中的方法write,把讀取到的文件保存到服務器的硬盤上
fos.write(bytes,0,len);
}
//8.使用Socket對象中的方法getOutputStream,獲取到網絡字節輸出流OutputStream對象
//9.使用網絡字節輸出流OutputStream對象中的方法write,給客戶端回寫"上傳成功"
socket.getOutputStream().write("上傳成功".getBytes());
//10.釋放資源(FileOutputStream,Socket,ServerSocket)
fos.close();
socket.close();
}catch (IOException e){
System.out.println(e);
}
}
}).start();
}
//服務器就不用關閉
//server.close();
}
}