在現有的網絡中,網絡通訊的方式主要有兩種:
- TCP(傳輸控制協議)方式
- UDP(用戶數據報協議)方式
在網絡通訊中,TCP方式就類似於撥打電話,使用該種方式進行網絡通訊時,需要建立專門的虛擬連接,然後進行可靠的數據傳輸,如果數據發送失敗,則客戶端會自動重發該數據。 而UDP方式就類似於發送短信,使用這種方式進行網絡通訊時,不需要建立專門的虛擬連接,傳輸也不是很可靠,如果發送失敗則客戶端無法獲得。 這兩種傳輸方式都是實際的網絡編程中進行使用,重要的數據一般使用TCP方式進行數據傳輸,而大量的非核心數據則都通過UDP方式進行傳遞,在一些程序中甚至結合使用這兩種方式進行數據的傳遞。 由於TCP需要建立專用的虛擬連接以及確認傳輸是否正確,所以使用TCP方式的速度稍微慢一些,而且傳輸時產生的數據量要比UDP稍微大一些。 |
無論使用TCP方式還是UDP方式進行網絡通訊,網絡編程都是由客戶端和服務器端組成當然,B/S結構的編程中只需要實現服務器端即可。所以,下面介紹網絡編程的步驟時,均以C/S結構爲基礎進行介紹。
網絡編程技術
1、客戶端網絡編程步驟
客戶端(Client)是指網絡編程中首先發起連接的程序,客戶端一般實現程序界面和基本邏輯實現,在進行實際的客戶端編程時,無論客戶端複雜還是簡單,以及客戶端實現的方式,客戶端的編程主要由三個步驟實現:
- 建立網絡連接
客戶端網絡編程的第一步都是建立網絡連接。在建立網絡連接時需要指定連接到的服務器的IP地址和端口號,建立完成以後,會形成一條虛擬的連接,後續的操作就可以通過該連接實現數據交換了。 - 交換數據
連接建立以後,就可以通過這個連接交換數據了。交換數據嚴格按照請求響應模型進行,由客戶端發送一個請求數據到服務器,服務器反饋一個響應數據給客戶端,如果客戶端不發送請求則服務器端就不響應。根據邏輯需要,可以多次交換數據,但是還是必須遵循請求響應模型。 - 關閉網絡連接
在數據交換完成以後,關閉網絡連接,釋放程序佔用的端口、內存等系統資源,結束網絡編程。
在實際實現時,步驟2會出現重複,在進行代碼組織時,由於網絡編程是比較耗時的操作,所以一般開啓專門的現場進行網絡通訊。
2、服務器端網絡編程步驟
服務器端(Server)是指在網絡編程中被動等待連接的程序,服務器端一般實現程序的核心邏輯以及數據存儲等核心功能。服務器端的編程步驟和客戶端不同,是由四個步驟實現,依次是:
- 監聽端口
服務器端屬於被動等待連接,所以服務器端啓動以後,不需要發起連接,而只需要監聽本地計算機的某個固定端口即可。
這個端口就是服務器端開放給客戶端的端口,服務器端程序運行的本地計算機的IP地址就是服務器端程序的IP地址。
- 獲得連接
當客戶端連接到服務器端時,服務器端就可以獲得一個連接,這個連接包含客戶端的信息,例如客戶端IP地址等等,服務器端和客戶端也通過該連接進行數據交換。
一般在服務器端編程中,當獲得連接時,需要開啓專門的線程處理該連接,每個連接都由獨立的線程實現。
- 交換數據
服務器端通過獲得的連接進行數據交換。服務器端的數據交換步驟是首先接收客戶端發送過來的數據,然後進行邏輯處理,再把處理以後的結果數據發送給客戶端。簡單來說,就是先接收再發送,這個和客戶端的數據交換數序不同。
其實,服務器端獲得的連接和客戶端連接是一樣的,只是數據交換的步驟不同。
當然,服務器端的數據交換也是可以多次進行的。
在數據交換完成以後,關閉和客戶端的連接。
- 關閉連接
當服務器程序關閉時,需要關閉服務器端,通過關閉服務器端使得服務器監聽的端口以及佔用的內存可以釋放出來,實現了連接的關閉。
TCP方式是需要建立連接的,對於服務器端的壓力比較大,而UDP是不需要建立連接的,對於服務器端的壓力比較小罷了。
Java網絡編程技術
和網絡編程有關的基本API位於java.net包中,該包中包含了基本的網絡編程實現,該包是網絡編程的基礎。該包中既包含基礎的網絡編程類,也包含封裝後的專門處理WEB相關的處理類。
InetAddress類
該類的功能是代表一個IP地址,並且將IP地址和域名相關的操作方法包含在該類的內部。
先來個Demo
- public static void main(String[] args) throws IOException {
- try {
- //使用域名創建對象
- InetAddress address=InetAddress.getByName("www.163.com");
- System.out.println(address);
- //使用ip創建對象
- InetAddress address2=InetAddress.getByName("222.184.115.167");
- System.out.println(address2);
- //獲得本機地址對象
- InetAddress address3 = InetAddress.getLocalHost();
- System.out.println(address3);
- //獲得對象中存儲的域名
- System.out.println("域名:"+address3.getHostName());
- //獲得對象中存儲的ip地址
- System.out.println("IP地址:"+address3.getHostAddress());
- } catch (Exception e) {
- // TODO: handle exception
- }
- }
由於該代碼中包含一個互聯網的網址,所以運行該程序時需要聯網,否則將產生異常。
在後續的使用中,經常包含需要使用InetAddress對象代表IP地址的構造方法,當然,該類的使用不是必須的,也可以使用字符串來代表IP地址進行實現。
TCP編程
在Java語言中,對於TCP方式的網絡編程提供了良好的支持,在實際實現時,以java.net.Socket類代表客戶端連接,以java.net.ServerSocket類代表服務器端連接。在進行網絡編程時,底層網絡通訊的細節已經實現了比較高的封裝,所以在程序員實際編程時,只需要指定IP地址和端口號碼就可以建立連接了。正是由於這種高度的封裝,一方面簡化了Java語言網絡編程的難度,另外也使得使用Java語言進行網絡編程時無法深入到網絡的底層,所以使用Java語言進行網絡底層系統編程很困難,具體點說,Java語言無法實現底層的網絡嗅探以及獲得IP包結構等信息。但是由於Java語言的網絡編程比較簡單,所以還是獲得了廣泛的使用。
在使用TCP方式進行網絡編程時,需要按照前面介紹的網絡編程的步驟進行,下面分別介紹一下在Java語言中客戶端和服務器端的實現步驟。
在客戶端網絡編程中,首先需要建立連接,在Java API中以java.net.Socket類的對象代表網絡連接,所以建立客戶端網絡連接,也就是創建Socket類型的對象,該對象代表網絡連接
- // socket1實現的是連接到IP地址是192.168.1.103的計算機的10000號端口
- Socket socket1 = new Socket("192.168.1.103", 10000);
- // socket2實現的是連接到域名是www.sohu.com的計算機的80號端口
- Socket socket2 = new Socket("www.sohu.com", 80);
底層網絡如何實現建立連接,對於程序員來說是完全透明的。如果建立連接時,本機網絡不通,或服務器端程序未開啓,則會拋出異常。
連接一旦建立,則完成了客戶端編程的第一步,緊接着的步驟就是按照“請求-響應”模型進行網絡數據交換,在Java語言中,數據傳輸功能由Java IO實現,也就是說只需要從連接中獲得輸入流和輸出流即可,然後將需要發送的數據寫入連接對象的輸出流中,在發送完成以後從輸入流中讀取數據即可。
- //獲得輸出流
- OutputStream outputStream = socket1.getOutputStream();
- //獲得輸入流
- InputStream inputStream=socket1.getInputStream();
這裏獲得的只是最基本的輸出流和輸入流對象,還可以根據前面學習到的IO知識,使用流的嵌套將這些獲得到的基本流對象轉換成需要的裝飾流對象,從而方便數據的操作。
最後當數據交換完成以後,關閉網絡連接,釋放網絡連接佔用的系統端口和內存等資源,完成網絡操作,示例代碼如下:
- socket1.close();
以上就是最基本的網絡編程功能介紹。
接下來寫個客戶端的Demo,程序在客戶端發送字符串到服務器,並將服務器端的反饋顯示到控制檯,數據交換隻進行一次,當數據交換進行完成以後關閉網絡連接,程序結束。
先來客戶端的代碼
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.Socket;
- public class Client {
- public static void main(String[] args) {
- Socket socket = null;
- InputStream is = null;
- OutputStream os = null;
- try {
- String msg = "Hello";
- String ip = "localhost";
- int port = 9898;
- // 建立連接
- socket = new Socket(ip, port);
- // 發送數據
- os = socket.getOutputStream();
- os.write(msg.getBytes());
- // 接收數據
- is = socket.getInputStream();
- byte b[]= new byte[1024];
- int n =is.read(b);
- System.out.println(new String(b,0,n));
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- } finally {
- try {
- //關閉連接和流
- is.close();
- os.close();
- socket.close();
- } catch (Exception e2) {
- // TODO: handle exception
- e2.printStackTrace();
- }
- }
- }
- }
代碼中建服務器端的代碼:
- public class Server {
- public static void main(String[] args) {
- ServerSocket serverSocket=null;
- Socket socket=null;
- InputStream is =null;
- OutputStream os =null;
- try {
- serverSocket = new ServerSocket(9898);
- socket = serverSocket.accept();
- is = socket.getInputStream();
- byte b[] = new byte[1024];
- int n = is.read(b);
- System.out.println("客戶端發送了:"+new String(b,0,n));
- os = socket.getOutputStream();
- os.write("接收成功!".getBytes());
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }finally{
- try {
- is.close();
- os.close();
- socket.close();
- serverSocket.close();
- } catch (Exception e2) {
- // TODO: handle exception
- }
- }
- }
- }
先運行服務器端,然後運行客戶端,服務器接收到數據將數據打印出來之後再返回數據到客戶端,客戶端打印出來
在該示例代碼中建立了一個監聽當前計算機9898號端口的服務器端Socket連接,然後獲得客戶端發送過來的連接,如果有連接到達時,讀取連接中發送過來的內容,並將發送的內容在控制檯進行輸出,輸出完成以後將客戶端發送的內容再反饋給客戶端。最後關閉流和連接對象,結束程序。
在服務器端程序編程中,由於服務器端實現的是被動等待連接,所以服務器端編程的第一個步驟是監聽端口,也就是監聽是否有客戶端連接到達。實現服務器端監聽的代碼爲:
- // 該代碼實現的功能是監聽當前計算機的9898號端口,如果在執行該代碼時,
- // 10000號端口已經被別的程序佔用,那麼將拋出異常。否則將實現監聽。
- serverSocket = new ServerSocket(9898);
服務器端編程的第二個步驟是獲得連接。該步驟的作用是當有客戶端連接到達時,建立一個和客戶端連接對應的Socket連 接對象,從而釋放客戶端連接對於服務器端端口的佔用。 通過獲得連接,使得客戶端的連接在服務器端獲得了保持,另外使得服務器端的端口釋放出來,可以繼續等待其它的客戶端連接。 實現獲得連接的代碼是:
- socket = serverSocket.accept();
該代碼實現的功能是獲得當前連接到服務器端的客戶端連接。需要說明的是accept和前面IO部分介紹的read方法一樣,都是一個阻塞方法,也就是當無連接時,該方法將阻塞程序的執行,直到連接到達時才執行該行代碼。另外獲得的連接會在服務器端的該端口註冊,這樣以後就可以通過在服務器端的註冊信息直接通信,而註冊以後服務器端的端口就被釋放出來,又可以繼續接受其它的連接了。
連接獲得以後,後續的編程就和客戶端的網絡編程類似了,這裏獲得的Socket類型的連接就和客戶端的網絡連接一樣了,只是服務器端需要首先讀取發送過來的數據,然後進行邏輯處理以後再發送給客戶端,也就是交換數據的順序和客戶端交換數據的步驟剛好相反。這部分的內容和客戶端很類似。
--------------------------
上面這個示例只是演示了網絡編程的基本步驟以及各個功能方法的基本使用,只是爲網絡編程打下了一個基礎,下面將就幾個問題來深入介紹網絡編程深層次的一些知識。
1.如何複用Socket連接?
撥通一次電話以後多次對話,這就是複用客戶端連接。
建立連接以後,將數據交換的邏輯寫到一個循環中,只要循環不結束則連接就不會被關閉,按照這種思路,可以改造一下上面的代碼,讓該程序可以在建立連接一次以後,發送三次數據,當然這裏的次數也可以是多次
現在看下新的服務器代碼和客戶端代碼:
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.ServerSocket;
- import java.net.Socket;
- /**
- * 服務器代碼
- * */
- public class Server {
- public static void main(String[] args) {
- ServerSocket serverSocket = null;
- Socket socket = null;
- InputStream is = null;
- OutputStream os = null;
- try {
- serverSocket = new ServerSocket(9898);
- socket = serverSocket.accept();
- is = socket.getInputStream();
- os = socket.getOutputStream();
- byte b[] = new byte[1024];
- for (int i = 0; i < 3; i++) {
- int n = is.read(b);
- os.write(("客戶端發送的內容:" + new String(b, 0, n)).getBytes());
- }
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- } finally {
- try {
- is.close();
- os.close();
- socket.close();
- serverSocket.close();
- } catch (Exception e2) {
- // TODO: handle exception
- }
- }
- }
- }
再看下新的客戶端代碼:
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.Socket;
- /**
- * 客戶端代碼
- * */
- public class Client {
- public static void main(String[] args) {
- Socket socket = null;
- InputStream is = null;
- OutputStream os = null;
- try {
- String msg[] = { "one", "two", "three" };
- String ip = "localhost";
- int port = 9898;
- // 建立連接
- socket = new Socket(ip, port);
- // 發送數據
- os = socket.getOutputStream();
- // 接收數據
- is = socket.getInputStream();
- byte b[] = new byte[1024];
- for (int i = 0; i < msg.length; i++) {
- os.write(msg[i].getBytes());
- int n = is.read(b);
- System.out.println(new String(b, 0, n));
- }
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- } finally {
- try {
- // 關閉連接和流
- is.close();
- os.close();
- socket.close();
- } catch (Exception e2) {
- // TODO: handle exception
- e2.printStackTrace();
- }
- }
- }
- }
上面的代碼雖然比較簡單,但是通用性比較差。
在該程序中,比較明顯的體現出了“請求-響應”模型,也就是在客戶端發起連接以後,首先發送字符串“First”給服務器端,服務器端輸出客戶端發送的內容“First”,然後將客戶端發送的內容再反饋給客戶端,這樣客戶端也輸出服務器反饋“First”,這樣就完成了客戶端和服務器端的一次對話
三次會話的過程一樣,在這個過程中,每次都是客戶端程序首先發送數據給服務器端,服務器接收數據以後,將結果反饋給客戶端,客戶端接收到服務器端的反饋,從而完成一次通訊過程。
2、如何使服務器端支持多個客戶端同時工作?
一個服務器端一般都需要同時爲多個客戶端提供通訊,如果需要同時支持多個客戶端,則必須使用前面介紹的線程的概念。簡單來說,也就是當服務器端接收到一個連接時,啓動一個專門的線程處理和該客戶端的通訊。
改造之後的服務器代碼,可以接收多個客戶端的數據。
在該示例代碼中,實現了一個while形式的死循環,由於accept方法是阻塞方法,所以當客戶端連接未到達時,將阻塞該程序的執行,當客戶端到達時接收該連接,並啓動一個新的LogicThread線程處理該連接,然後按照循環的執行流程,繼續等待下一個客戶端連接。這樣當任何一個客戶端連接到達時,都開啓一個專門的線程處理,通過多個線程支持多個客戶端同時處理。 |
- /**
- * 支持多客戶端的服務器代碼
- * */
- public class Server {
- public static void main(String[] args) {
- ServerSocket serverSocket = null;
- Socket socket = null;
- try {
- serverSocket = new ServerSocket(9898);
- while (true) {
- socket = serverSocket.accept();
- // 啓動線程
- // 實現接收客戶端連接,然後開啓專門的邏輯線程處理該連接,
- // LogicThread類實現對於一個客戶端連接的邏輯處理,將處理的邏輯放置在該類的run方法中。
- new LogicThread(socket);
- }
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- } finally {
- try {
- socket.close();
- serverSocket.close();
- } catch (Exception e2) {
- // TODO: handle exception
- }
- }
- }
- static class LogicThread extends Thread {
- Socket socket = null;
- public LogicThread(Socket socket) {
- this.socket = socket;
- start();
- }
- @Override
- public void run() {
- byte b[] = new byte[1024];
- InputStream is = null;
- OutputStream os = null;
- try {
- is = socket.getInputStream();
- os = socket.getOutputStream();
- int n = is.read(b);
- os.write(("客戶端發送的內容:" + new String(b, 0, n)).getBytes());
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- } finally {
- try {
- is.close();
- os.close();
- socket.close();
- } catch (Exception e2) {
- // TODO: handle exception
- }
- }
- }
- }
- }
這裏的示例還只是基礎的服務器端實現,在實際的服務器端實現中,由於硬件和端口數的限制,所以不能無限制的創建線程對象,而且頻繁的創建線程對象效率也比較低,所以程序中都實現了線程池來提高程序的執行效率。
這裏簡單介紹一下線程池的概念,線程池(Thread pool)是池技術的一種,就是在程序啓動時首先把需要個數的線程對象創建好,例如創建5000個線程對象,然後當客戶端連接到達時從池中取出一個已經創建完成的線程對象使用即可。當客戶端連接關閉以後,將該線程對象重新放入到線程池中供其它的客戶端重複使用,這樣可以提高程序的執行速度,優化程序對於內存的佔用等。 |
關於基礎的TCP方式的網絡編程就介紹這麼多,下面一章介紹UDP方式的網絡編程在Java語言中的實現。