21天學會Java之(Java SE第十三篇):網絡編程、TCP/UDP通信

如今,計算機已經成爲人們學習、工作、生活必不可少的工具。人們利用計算機可以和親朋好友在網上聊天,玩網遊或發郵件等,這些功能的實現都離不開計算機網絡。計算機網絡實現了不同計算機之間的通信,而這些必須依靠人們編寫網絡程序來實現。在Java中提供相應的類包讓大家編寫網絡程序,即下文說要提到的內容。

基礎概念

在學習如何編程之前,首先要了解關於網絡通信的一些概念。本文僅介紹基礎的概念,如果想了解相關的知識,可以翻閱相關的書籍。

計算機網絡

計算機網絡是指將地理位置不同的具有獨立功能的多臺計算機及其外部設備,通過通信線路連接起來,在網絡操作系統、網絡管理軟件及網絡通信協議的管理和協調下,實現資源共享和信息傳遞的計算機系統。

簡要的說就是以下內容:

  1. 計算機網絡的作用是資源共享和信息傳遞。
  2. 計算機網絡的組成包括:
    • 計算機硬件:計算機(大中小型服務器、臺式機、筆記本等)、外部設備(路由器、交換機等)、通信線路(雙絞線、光纖等)。
    • 計算機軟件:網絡操作系統(Windows Server/Adcance Server、Unix、Linux等)、網絡管理軟件(WorkWin、SugarNMS等)、網絡通信協議(TCP/IP等)。
  3. 計算機網絡中的多臺計算機是具有獨立功能的,而不是脫離網絡就無法存在的。

網絡通信協議

  • 網絡通信協議

國際標準組織定義了網絡通信協議的基本框架,被稱爲開放系統互聯模型,即OSI模型。OSI模型將通信標準按層次進行劃分,每一個層次解決一個類型的問題,這樣就是的標準的制定沒那麼複雜。OSI模型制定的七層標準模型,分別是應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層和物理層。

OSI的七層協議模型如下圖所示:

OSI的七層協議模型

雖然國際化標準組織制定了這樣一個網絡通信協議的模型,但是實際上互聯網通信使用最多的還是TCP/IP網絡通信協議。

TCP/IP是一個協議族,按照層次劃分爲四層,分別是應用層、傳輸層、互連網絡層和網絡接口層(物理+數據鏈路層)。

OSI網絡通信協議模型僅是一個參考模型,而TCP/IP協議是事實上的標準。TCP/IP協議參考了OSI模型但是並沒有嚴格按照OSI規定的七層標準去劃分,而只劃分了四層,這樣會更簡單。以免在劃分太多層次時,人們很難區分某個協議是屬於哪個層次的。TCP/IP中有兩個重要的協議,傳輸層的TCP協議和互連網絡層的IP協議,因此就拿這兩個協議來命名整個協議族,TCP/IP協議就是指整個協議族。

  • 網絡協議的分層

由於網絡節點之間的聯繫很複雜,因此協議把複雜的內容分解成簡單的內容,再將它們複合起來。最常用的複合方式是層次方式,即同層間可以通信,上一層可以調用下一層,而與再下一層不發生關係。

用戶應用程序爲最高層,物理通信線路爲最低層,其間的協議處理分爲若干層並規定每層處理的任務,也規定每層的接口標準。

OSI模型與TCP/IP模型的對應關係如下圖所示:

OSI模型與TCP/IP模型的對應關係

數據封裝與解封

由於用戶傳輸的數據一般都比較大,甚至以兆字節計算,一次發送出去十分困難,因此就需要把數據分成很多片段,再按照一定的次序發送出去。這個過程就需要對數據進行封裝。

數據封裝(Data Encapsulation)是指將協議數據單元(PDU)封裝在一組協議頭和協議尾中的過程。在OSI七層參考模型中,每層主要負責與其他機器上的對等層進行通信。該過程是在協議數據單元(PDU)中實現的,其中每層的PDU一般由本層的協議頭、協議尾和數據封裝構成。

  1. 數據發送處理過程
    • 應用層將數據轉交給傳輸層,傳輸層添加上TCP的控制信息(稱爲TCP頭部),這個數據單元稱爲段(Segment),加入控制信息的過程稱爲封裝。然後,將段交給網絡層。
    • 網絡層接收到段,再添加上IP頭部,這個數據單元稱爲包(Packet)。然後,將包交給數據鏈路層。
    • 數據鏈路層接收到包,在添加MAC頭部和尾部,這個數據單元稱爲幀(Frame)。然後將幀交給物理層。
    • 物理層將接收到的數據轉化爲比特流,然後在網線中傳輸。
  2. 數據接收處理過程
    • 物理層接收到比特流,經過處理後將數據交給數據鏈路層。
    • 數據鏈路層將接收到的數據轉化爲數據幀,再去除MAC頭部和尾部,這個去除控制信息的過程稱爲解封,然後將包交給網絡層。
    • 網絡層接收到包,再去除IP頭部,然後將段交給傳輸層。
    • 傳輸層接收到段,再去除TCP頭部,然後將數據交給應用層。

綜上所述,我們可以總結如下:

  • 發送方的數據處理方式是從高層到底層,逐層進行數據封裝。
  • 接收方的數據處理方式是從底層到高層,逐層進行數據解封。

接收方的每一層只把對該層有意義的數據拿走,或者說每一層只能處理與發送方同等層的數據,然後把其餘的部分傳遞給上一層,這就是對等層通信的概念。

IP地址與端口

  • IP地址

IP地址用來標識網絡中的一個通信實體的地址。通信實體可以是計算機、路由器等。例如,互聯網的每個服務器都要有自己的IP地址,而局域網的每臺計算機要進行通信也要配置IP地址。路由器是連接多個網絡的網絡設備。

目前主流IP地址使用的是IPv4協議,但是隨着網絡規模的不斷擴大,採用IPv4協議的可用地址數量面臨着枯竭的危險,所以推出IPv6協議。

IPv4協議採用32位地址,並以8位爲一個單位,分成四部分,以點分十進制表示。如192.168.0.1。8位二進制的計數範圍是00000000 ~ 11111111,對應十進制的0 ~ 255。

IPv6協議爲128位地址(16字節),寫成8個16位的無符號整數。每個整數用4個十六進制位表示,每個數之間用冒號(:)隔開,例如:3ffe:3201:1280:c8ff:fe4d:db39:1984。

在IP地址中有一些特殊的地址:

127.0.0.1爲本機地址。
192.168.0.0~192.168.255.255爲私有地址,屬於非法註冊地址,專門爲組織機構內部使用。
  • 端口

IP地址用來標識一臺計算機,但是一臺計算機上可能提供多種網絡應用程序,區分這些應用程序就需要使用到了端口。

端口是虛擬的概念,並不是在主機上真的有若干個端口。通過端口,可以在一臺主機上運行多個網絡程序。端口用一個16位的二進制整數表示,對用十進制的範圍就是0~65535。

IP地址就像是每個人的門牌號,而端口就是房間號。必須同時指定IP地址和端口號才能夠正確發送接收數據。

URL

在因特網上,每一個信息資源都有統一且唯一的地址,該地址就叫做URL(Uniform Resource Locator),它是因特網的統一資源定位符。URL由四部分所組成:協議、存放資源的主機域名、資源文件名和端口號。如果未指定端口號,則使用協議默認的端口。例如HTTP協議的默認端口號爲80。在瀏覽器中訪問網頁時,地址欄顯示的地址就是URL。

在Java中,java.util包中提供了URL類,該類封裝了大量涉及從遠程站點獲取信息的複雜細節。

Socket

Socket即套接字,它就像是傳輸層爲應用層打開的一個小窗口,應用程序通過這個小窗口向遠程發送數據,或者接收遠程發來的數據;當數據進入這個口之後,或者數據從這個口出來之前,外接不知道也不需要知道的,更不會關心它如何傳輸,這屬於網絡其他層的工作。

Socket實際是傳輸層供給應用層的編程接口。Socket就是應用層與傳輸層之間的橋樑。使用Socket編程可以開發客戶機和服務器應用程序,可以再本地網絡上進行通信,也可通過Internet在全球範圍內通信。

TCP協議和UDP協議

TCP協議和UDP協議的聯繫與區別

TCP協議和UDP協議是傳輸層的兩種協議。Socket是傳輸層提供給應用層的編程接口,所以Socket編程就分爲TCP編程和UDP編程兩類。

TCP與UDP的主要區別:

  • TCP是面向連接的,傳輸數據安全、穩定,效率相對較低。TCP就類似於打電話,使用這種方式進行網絡通信時,需要建立專門的虛擬連接,然後進行可靠的數據傳輸,如果數據發送失敗,則客戶端會自動重新發送該數據。
  • UDP是面向無連接的,傳輸數據不安全,但效率較高。UDP則類似於發送短信,使用這種方式進行網絡通信時,不需要建立專門的虛擬連接,傳輸也是不是很可靠,如果發送失敗則客戶端無法獲得數據。

TCP協議

TCP協議是面向連接的,所謂面向連接,就是當計算機雙方通信時必須經過先建立連接,然後傳送數據,最後拆除連接三個過程。

TCP在建立連接時分爲以下三步:

  1. 請求端/客戶端發送一個含SYN即同步(Synchronize)標誌的TCP報文,SYN同步報文會指明客戶端使用的端口以及TCP連接的初始序號。
  2. 服務器在收到客戶端的SYN報文後,將返回一個SYN+ACK(確認Acknowledgement)報文,表示客戶端的請求被接收。同時TCP序號被加1。
  3. 客戶端返回一個確認報文ACK給服務器端,同樣TCP序號被加1,至此一個TCP連接完成。然後纔開始通信的第二部,數據處理。

以上就是場所的TCP的三次握手(Three-way Handshake)。

UDP協議

基於TCP協議可以建立穩定連接的點對點通信。這種通信方式實時、快速、安全性高,但是很佔用系統的資源。

在網絡傳輸上,還有另一種基於UDP協議的通信方式,稱爲數據報通信方式。在這種方式中,每個數據發送單元被統一封裝成數據報包的方式,發送方將數據報包發送到網絡,數據報包在網絡中去尋找它的目的地。

Java網絡編程中的常用類

在Java中,爲了可移植性,不允許直接調用操作系統,而是由java.net包來提供網絡功能。Java虛擬機負責提供與操作系統的實際連接,下文將介紹結構java.net包中的常用類。

InetAddress

  1. 作用:InetAddress用於封裝計算機的IP地址和DNS(沒有端口信息)。
  2. 特點:InetAddress類沒有構造器。如果要得到對象,只能通過靜態方法getLocalHost()、getByName()、getAllByName()、getAddress()和getHostName()實現。

下面是這些方法的使用例子:

import java.net.InetAddress;
import java.net.UnknownHostException;
/**
 * IP:定位一個節點:計算機、路由、通訊設備等
 * InetAddress兩個靜態方法:getLocalHost():本機	getByName():根據域名解析(DNS)|根據IP地址解析IP地址
 * 兩個成員方法: getHostAddress():返回地址|getHostName():返回計算機名或域名
 * @author WHZ
 *
 */
public class IPTest {
	public static void main(String[] args) throws UnknownHostException {
		//使用getLocalHost方法創建InetAddress對象
		InetAddress address=InetAddress.getLocalHost();
		System.out.println(address.getHostAddress());  //返回address的IP地址:192.168.31.76
		System.out.println(address.getHostName());  //返回主機名:WHZ-PC
		//根據域名得到InetAddress對象
		address=InetAddress.getByName("baidu.com");
		System.out.println(address.getHostAddress());  //返回address的IP地址:220.181.38.148
		System.out.println(address.getHostName());   //輸出baidu.com
		//根據IP得到InetAddress對象
		address=InetAddress.getByName("49.232.138.233");
		System.out.println(address.getHostAddress());  //返回address的IP地址:49.232.138.233
		System.out.println(address.getHostName());  //通過IP地址解析域名
	}
}

InetSocketAddress

InetSocketAddress用於包含IP地址和端口信息,常用語Socket通信。該類實現IP套接字地址(IP地址+端口號),不依賴任何協議。

import java.net.InetSocketAddress;
/**
 * 端口:1.區分軟件 2.兩個字節:0-65535 3.TCP和UDP協議,同一個協議端口不能衝突 4.定義端口越大越好
 * InetSocketAddress:1.構造器new InetSocketAddress(地址|域名, 端口);
 * 2.方法:getAddress()|getHostName()|getPort()
 * @author WHZ
 *
 */
public class PortTest {
	public static void main(String[] args) {
		//包含端口
		InetSocketAddress socketAddress1=new InetSocketAddress("127.0.0.1", 8080);
		InetSocketAddress socketAddress2=new InetSocketAddress("localhost", 9000);
		System.out.println(socketAddress1.getHostName());  //獲得主機名
		System.out.println(socketAddress2.getAddress());  //獲得地址
		System.out.println(socketAddress1.getPort());  //獲得端口
		System.out.println(socketAddress1.getHostString());  //獲得主機名字符串
	}
}

URL類

IP地址唯一標識了Internet上的計算機,而URL則標識了這些計算機上的資源。URL類代表一個統一資源定位符,它是指向互聯網資源的指針。資源可以是簡單的文件或者目錄,也可以是對更爲複雜對象的引用,例如對數據庫或搜索引擎進行查詢。

爲了方便程序員編程,JDK提供了URL類,該類的全名是java.net.URL。有了這樣一個類,就可以使用它的各種方法來對URL對象進行分割、合併等處理。

import java.net.MalformedURLException;
import java.net.URL;

/**
 * URL:統一資源定位器,互聯網三大基石之一(URL,html,http),區分資源
 * 1.協議	2.域名|IP|計算機	3.端口	4.請求資源
 * http://www.baidu.com:80/index.html?uname=whz&age=21#a
 * @author WHZ
 *
 */
public class URLTest {
	public static void main(String[] args) throws MalformedURLException {
		URL url=new URL("http://www.baidu.com:80/index.html?uname=whz&age=21#a");
		//獲取四個值
		System.out.println("協議:"+url.getProtocol());
		System.out.println("域名|IP|計算機:"+url.getHost());
		System.out.println("端口:"+url.getPort());
		System.out.println("請求資源1:"+url.getFile());
		System.out.println("請求資源2:"+url.getPath());
		System.out.println("參數:"+url.getQuery());
		System.out.println("錨點:"+url.getRef());
	}
}

簡單的爬蟲實現

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;

/**
 * 網絡爬蟲的原理
 * @author WHZ
 *
 */
public class SpiderTest01 {
	public static void main(String[] args) throws Exception {
		//獲取資源
		URL url=new URL("https://www.jd.com");
		//下載資源
		File file=new File("jd.txt");
		InputStream is=url.openStream();
		BufferedReader br=new BufferedReader(new InputStreamReader(is,"UTF-8"));
		BufferedWriter bw=new BufferedWriter(new FileWriter(file));
		String msg=null;
		while (null!=(msg=br.readLine())) {
			System.out.println(msg);
			bw.append(msg);
			bw.newLine();
		}
		bw.close();
		br.close();
		//分析
		//處理。。。
	}
}

TCP通信的實現

上文提到TCP協議是面向連接的,在通信時客戶端與服務器端必須建立連接。在網絡通信中,第一次主動發起通信的程序被稱爲客戶端(Client)程序,簡稱客戶端;而在第一次通信中等待連接的程序被稱作服務器端(Server)程序,簡稱服務器。一旦通信建立,則客戶端和服務器端完全一樣,沒有本質的區別。

“請求-響應”模式

在“請求-響應”模式中,Socket類用於發送TCP消息;ServerSocket類用於創建服務器。

套接字Socket是一種進程間的數據交換機制。這些進程既可以在同一機器上,也可以在通過網絡連接的不同機器上。換句話說,套接字起到了通信的作用。單個套接字是一個端點,而一對套接字則構成一個雙向通信信道,使非關聯程序可以在本地或通過網絡進行數據交換。一旦建立套接字連接,數據即可在相同或不同的系統中雙向或單向發送,直到其中一個端點關閉連接。套接字與主機地址和端口地址相關聯。主機地址就是客戶端或服務器程序所在主機的IP地址。端口地址是指客戶端或服務端使用的主機的通信端口。

在客戶端和服務端中,分別創建獨立的Socket,並通過Socket的屬性將兩個Socket進行連接,這樣,客戶端和服務端通過套接字所建立的連接即可使用輸入/輸出流進行通信。

TCP/IP套接字是最可靠的雙向流協議,使用TCP/IP可以發送任意數量的數據。

實際上,套接字只是計算機上已編號的端口。如果發送方和接收方計算機確定好端口,它們之間就可以進行通信了。

TCP/IP通信連接的簡單過程

TCP/IP通信連接過程:位於A計算機上的TCP/IP軟件向B計算機發送包含端口號和消息;B計算機的TCP/IP軟件接收該消息並進行檢查,查看是否有它知道的程序正在該端口上接收消息。如果有,它將該消息交給這個程序。要是程序有效運行,就必須有一個客戶端和一個服務器。

通過Socket的編程順序

  1. 創建服務器ServerSocket。在創建時,定義ServerSocket的監聽端口(在這個端口接收客戶端發來的消息)。
  2. ServerSocket調用accept()方法,使之處於阻塞狀態。
  3. 創建客戶端Socket,並設置服務器的IP地址及端口。
  4. 客戶端發出連接請求,建立連接。
  5. 分別取得服務器和客戶端Socket的InputStream和OutputStream。
  6. 利用Socket和ServerSocket進行數據傳輸。
  7. 關閉流及Socket。

以下是TCP通信的實際使用例子,幷包括一個聊天室的實現,可以結合代碼理解使用(爲了簡化代碼,代碼中的異常都使用throws拋出了,實際開發中請遵循try_catch_finally的方法拋出異常)。

TCP單向通信例子

import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 熟悉流程:創建服務器
 * 1.指定端口 使用ServerSocket創建服務器
 * 2.阻塞式等待連接accept()
 * 3.操作:輸入流輸出流操作
 * 4.釋放資源
 * @author WHZ
 *
 */
public class Server {
	public static void main(String[] args) throws Exception{
		System.out.println("----------Server----------");
		//1.指定端口 使用ServerSocket創建服務器
		ServerSocket server=new ServerSocket(8888);
		//2.阻塞式等待連接accept()
		Socket client=server.accept();  //阻塞式
		System.out.println("一個客戶端建立了連接。");
		//3.操作:輸入流輸出流操作
		DataInputStream dis=new DataInputStream(client.getInputStream());
		String str=dis.readUTF();
		System.out.println(str);
		//4.釋放資源
		dis.close();
		client.close();
		
		server.close();
	}
}
import java.io.DataOutputStream;
import java.net.Socket;

/**
 * 熟悉流程:創建客戶端
 * 1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
 * 2.操作:輸入流輸出流操作
 * 3.釋放資源
 * @author WHZ
 *
 */
public class Client {
	public static void main(String[] args) throws Exception{
		System.out.println("----------Client----------");
		//1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
		Socket client=new Socket("localhost", 8888);
		//2.操作:輸入流輸出流操作
		DataOutputStream dos=new DataOutputStream(client.getOutputStream());
		dos.writeUTF("Hello World");
		dos.flush();
		//3.釋放資源
		dos.close();
		client.close();
	}
}

單向通信實現(模擬登陸)

import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 模擬登陸 單向:創建服務器
 * 1.指定端口 使用ServerSocket創建服務器
 * 2.阻塞式等待連接accept()
 * 3.操作:輸入流輸出流操作
 * 4.釋放資源
 * @author WHZ
 *
 */
public class LoginServer {
	public static void main(String[] args) throws Exception{
		System.out.println("----------Server----------");
		//1.指定端口 使用ServerSocket創建服務器
		ServerSocket server=new ServerSocket(8888);
		//2.阻塞式等待連接accept()
		Socket client=server.accept();
		//3.操作:輸入流輸出流操作
		DataInputStream dis=new DataInputStream(client.getInputStream());
		String data=dis.readUTF();
		//分析
		String[] datas=data.split("&");
		for (String info:datas) {
			String[] userInfo=info.split("=");
			if (userInfo[0].equals("uname")) {
				System.out.println("你的用戶名爲:"+userInfo[1]);
			} else if (userInfo[0].equals("upwd")) {
				System.out.println("你的密碼爲:"+userInfo[1]);
			}
		}
		//4.釋放資源
		dis.close();
		client.close();
		
		server.close();
	}
}
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 模擬登陸 單向:創建客戶端
 * 1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
 * 2.操作:輸入流輸出流操作
 * 3.釋放資源
 * @author WHZ
 *
 */
public class LoginClient {
	public static void main(String[] args) throws Exception{
		System.out.println("----------Client----------");
		//1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
		Socket client=new Socket("localhost", 8888);
		//2.操作:輸入流輸出流操作
		BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
		System.out.print("請輸入用戶名:");
		String uname=br.readLine();
		System.out.print("請輸入密碼:");
		String upwd=br.readLine();
		DataOutputStream dos=new DataOutputStream(client.getOutputStream());
		dos.writeUTF("uname="+uname+"&upwd="+upwd);
		dos.flush();
		//3.釋放資源
		dos.close();
		br.close();
		client.close();
		
	}
}

雙向通信實現(模擬登陸服務器返回消息)

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 模擬登陸 雙向:創建服務器
 * 1.指定端口 使用ServerSocket創建服務器
 * 2.阻塞式等待連接accept()
 * 3.操作:輸入流輸出流操作
 * 4.釋放資源
 * @author WHZ
 *
 */
public class LoginTwoWayServer {
	public static void main(String[] args) throws Exception{
		System.out.println("----------Server----------");
		//1.指定端口 使用ServerSocket創建服務器
		ServerSocket server=new ServerSocket(8888);
		//2.阻塞式等待連接accept()
		Socket client=server.accept();
		//3.操作:輸入流輸出流操作
		DataInputStream dis=new DataInputStream(client.getInputStream());
		String data=dis.readUTF();
		String uname="";
		String upwd="";
		String[] datas=data.split("&");
		for(String info:datas) {
			String[] userInfo=info.split("=");
			if (userInfo[0].equals("uname")) {
				uname=userInfo[1];
				System.out.println("你的用戶名爲:"+userInfo[1]);
			}else if (userInfo[0].equals("upwd")) {
				upwd=userInfo[1];
				System.out.println("你的密碼爲:"+userInfo[1]);
			}
		}
		DataOutputStream dos=new DataOutputStream(client.getOutputStream());
		if (uname.equals("abc")&&upwd.equals("123456")) {
			dos.writeUTF("登陸成功,歡迎回來。");
		}else {
			dos.writeUTF("登陸失敗,請檢查賬號密碼。");
		}
		//4.釋放資源
		dos.close();
		dis.close();
		client.close();
		
		server.close();
	}
}
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 模擬登陸 雙向:創建客戶端
 * 1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
 * 2.操作:輸入流輸出流操作
 * 3.釋放資源
 * @author WHZ
 *
 */
public class LoginTwoWayClient {
	public static void main(String[] args)throws Exception {
		System.out.println("----------Client----------");
		//1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
		Socket client=new Socket("localhost", 8888);
		//2.操作:輸入流輸出流操作
		BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
		System.out.print("請輸入用戶名:");
		String uname=br.readLine();
		System.out.print("請輸入密碼:");
		String upwd=br.readLine();
		DataOutputStream dos=new DataOutputStream(client.getOutputStream());
		dos.writeUTF("uname="+uname+"&upwd="+upwd);
		dos.flush();
		DataInputStream dis=new DataInputStream(client.getInputStream());
		String msg=dis.readUTF();
		System.out.println(msg);
		//3.釋放資源
		dis.close();
		dos.close();
		br.close();
		client.close();
	}
}

多賬號登陸的實現

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 模擬登陸 多個客戶端請求:創建服務器
 * 1.指定端口 使用ServerSocket創建服務器
 * 2.阻塞式等待連接accept()
 * 3.操作:輸入流輸出流操作
 * 4.釋放資源
 * @author WHZ
 *
 */
public class LoginMultiServer {
	public static void main(String[] args) throws Exception{
		System.out.println("----------Server----------");
		//1.指定端口 使用ServerSocket創建服務器
		ServerSocket server=new ServerSocket(6666);
		boolean isRunning=true;
		while (isRunning) {
			//2.阻塞式等待連接accept()
			Socket client=server.accept();
			//3.操作:輸入流輸出流操作
			new Thread(new Channel(client)).start();
		}
		//4.釋放資源
		server.close();
	}
	
	static class Channel implements Runnable{
		private Socket client;
		private DataInputStream dis=null;  //輸入流
		private DataOutputStream dos=null;  //輸出流
		public Channel(Socket client) {
			this.client=client;
			try {
				dis=new DataInputStream(client.getInputStream());  //輸入
				dos=new DataOutputStream(client.getOutputStream());  //輸出
			} catch (IOException e) {
				e.printStackTrace();
				close();
			}
		}
		//接收數據
		private String receive() {
			String data="";
			try {
				data=dis.readUTF();
			} catch (IOException e) {
				e.printStackTrace();
			}
			return data;
		}
		//發送數據
		private void send(String msg) {
			try {
				dos.writeUTF(msg);
				dos.flush();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		@Override
		public void run() {
			System.out.println("建立了一個客戶端連接");
			String uname="";
			String upwd="";
			String[] datas=receive().split("&");
			for (String info:datas) {
				String[] userInfo=info.split("=");
				if (userInfo[0].equals("uname")) {
					uname=userInfo[1];
					System.out.println("你的用戶名是:"+uname);
				}else if (userInfo[0].equals("upwd")) {
					upwd=userInfo[1];
					System.out.println("你的密碼是:"+upwd);
				}
			}
			if (uname.equals("abc") && upwd.equals("123456")) {
				System.out.println("賬號:"+uname+",登陸成功,歡迎回來。");
				send("賬號:"+uname+",登陸成功,歡迎回來。");
			}else {
				System.out.println("賬號:"+uname+",登陸失敗,請檢測賬號密碼。");
				send("賬號:"+uname+",登陸失敗,請檢測賬號密碼。");
			}
			close();
		}
		//釋放資源
		private void close() {
			try {
				if (null!=dos) {
				dos.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				if (null!=dis) {
					dis.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				if (null!=client) {
					client.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 模擬登陸 多個客戶端請求:創建客戶端
 * 1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
 * 2.操作:輸入流輸出流操作
 * 3.釋放資源
 * @author WHZ
 *
 */
public class LoginMultiClient {
	public static void main(String[] args)throws Exception {
		System.out.println("----------Client----------");
		//1.建立連接 使用Socket創建客戶端,需要指定服務器的地址和端口
		Socket client=new Socket("localhost",6666);
		//2.操作:輸入流輸出流操作
		new Send(client).send();
		new Receive(client).receive();
		//3.釋放資源
		client.close();
	}
	//發送
	static class Send{
		private Socket client;
		private DataOutputStream dos;
		private BufferedReader br;
		private String msg;
		public Send(Socket client) {
			br=new BufferedReader(new InputStreamReader(System.in));
			this.msg=init();
			this.client=client;
			try {
				dos=new DataOutputStream(client.getOutputStream());
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		//初始化
		private String init() {
			try {
				System.out.print("請輸入用戶名:");
				String uname=br.readLine();
				System.out.print("請輸入密碼:");
				String upwd=br.readLine();
				return "uname="+uname+"&upwd="+upwd;
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return null;
		}
		
		public void send() {
			try {
				dos.writeUTF(msg);
				dos.flush();
			} catch (IOException e) {
				e.printStackTrace();
			}
			
		}
	}
	//接收
	static class Receive{
		private Socket client;
		private DataInputStream dis=null;
		public Receive(Socket client) {
			this.client=client;
			try {
				dis=new DataInputStream(client.getInputStream());
			} catch (IOException e) {
				
				e.printStackTrace();
			}
		}
		
		public void receive() {
			String msg=null;
			try {
				msg=dis.readUTF();
				System.out.println(msg);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

聊天室的設計與實現

此部分代碼僅供參考,對於新手,相關的聊天室項目,可以去網上尋找相關的博客或者更詳細的講解視頻來理解學習。

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 在線聊天室:服務器
 * 目標:私聊
 * @author WHZ
 *
 */
public class Chatroom {
	private static CopyOnWriteArrayList<Channel> all=new CopyOnWriteArrayList<Channel>();
	public static void main(String[] args) throws Exception{
		System.out.println("聊天室啓動中...");
		ServerSocket server=new ServerSocket(8888);  //創建服務器
		boolean operatingCondition=true;
		while (operatingCondition) {
			Socket client=server.accept();  //接收客戶端訪問(阻塞式接收)
			System.out.println("一個客戶端建立了連接");
			Channel channel=new Channel(client);
			all.add(channel);  //管理所有的成員
			new Thread(channel).start();
		}
		server.close();
	}
	//一個客戶端代表一個Channel
	static class Channel implements Runnable{
		private DataInputStream dis;
		private DataOutputStream dos;
		private Socket client;
		private boolean isRunning;
		private String name;
		public Channel(Socket client) {
			this.client=client;
			try {
				dis=new DataInputStream(client.getInputStream());
				dos=new DataOutputStream(client.getOutputStream());
				isRunning=true;
				name=receive();  //獲取名稱
				this.send("歡迎來到聊天室");
				this.sendOthers(this.name+"進入了聊天室", true);
			} catch (IOException e) {
				System.out.println("服務端連接時出錯");
				release();
			}
			
		}
		//接收消息
		private String receive() {
			String msg="";
			try {
				msg=dis.readUTF();
			} catch (IOException e) {
				System.out.println("服務端接收消息出錯");
				release();
			}
			return msg;
		}
		//發送消息
		private void send(String msg) {
			try {
				dos.writeUTF(msg);
				dos.flush();
			} catch (IOException e) {
				System.out.println("服務端發送消息出錯");
				release();
			}
		}
		//羣聊:獲取自己的消息發給其他人
		//私聊:約定數據格式爲@***:msg
		private void sendOthers(String msg,boolean isSys) {
			boolean isPrivate=msg.startsWith("@");
			if (isPrivate) {  //私聊
				//獲取目標數據
				int index=msg.indexOf(':');
				String uname=msg.substring(1, index);
				msg=msg.substring(index+1);
				for (Channel other : all) {
					if (other.name.equals(uname)) {
						other.send(this.name+"悄悄地對你說:"+msg);
						break;
					}
				}
			}else {
				for (Channel other:all) {
					if (other==this) {
						continue;
					}
					if (isSys) {
						other.send(msg);  //系統消息
					}else {
						other.send(this.name+"對所有人說:"+msg);  //羣聊消息
					}
				}
			}
		}
				
		
		@Override
		public void run() {
			while (isRunning) {
				String msg=receive();
				if (!msg.equals("")) {
					//send(msg);
					sendOthers(msg,false);
				}
			}
		}
		//釋放資源
		private void release() {
			isRunning=false;
			CloseUtils.close(dos,dis,client);
			//退出
			all.remove(this);
			sendOthers(this.name+"離開了聊天室。", true);
		}
	}
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 在線聊天室:客戶端
 * 目標:私聊
 * @author WHZ
 *
 */
public class Client {
	public static void main(String[] args) throws Exception {
		System.out.println("------client-----");
		BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
		System.out.print("請輸入用戶名:");
		String name=br.readLine();
		//注意:獲取用戶名在建立連接之前!!!
		Socket client=new Socket("localhost", 8888);
		new Thread(new Send(client,name)).start();
		new Thread(new Receive(client)).start();
	}
}
import java.io.Closeable;
import java.io.IOException;
/**
 * 釋放資源工具類
 * @author WHZ
 *
 */
public class CloseUtils {
	public static void close(Closeable... targets) {
		for (Closeable target:targets) {
			try {
				if (null!=target) {
					target.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;

/**
 * 使用多線程封裝接收端
 * @author WHZ
 *
 */
public class Receive implements Runnable{
	private Socket client;
	private DataInputStream dis;
	private boolean isRunning=true;
	public Receive(Socket client) {
		this.client=client;
		try {
			dis=new DataInputStream(client.getInputStream());
		} catch (IOException e) {
			System.out.println("客戶端接收端出錯");
			release();
		}
	}
	//接收消息
	private String receive() {
		String msg="";
		try {
			msg=dis.readUTF();
		} catch (IOException e) {
			System.out.println("客戶端接收消息出錯");
			receive();
		}
		return msg;
	}
	
	@Override
	public void run() {
		while (isRunning) {
			String msg=receive();
			System.out.println(msg);
		}
	}
	
	//釋放資源
	private void release() {
		isRunning=false;
		CloseUtils.close(client,dis);
	}
}
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

/**
 * 使用多線程封裝發送端
 * @author WHZ
 *
 */
public class Send implements Runnable{
	private Scanner sc;
	private Socket client;
	private DataOutputStream dos;
	private boolean isRunning=true;
	private String name;
	public Send(Socket client,String name) {
		this.client=client;
		this.sc=new Scanner(System.in);
		this.name=name;
		try {
			dos=new DataOutputStream(client.getOutputStream());
			send(this.name);  //發送名稱
		} catch (IOException e) {
			System.out.println("客戶端發送端出錯");
			release();
		}
	}
	//發送消息
	private void send(String msg) {
		try {
			dos.writeUTF(msg);
			dos.flush();
		} catch (IOException e) {
			System.out.println("客戶端發送消息出錯");
			release();
		}
	}
	
	@Override
	public void run() {
		while (isRunning) {
			String msg=sc.nextLine();
			if (!msg.equals("")) {
				send(msg);
			}
		}
	}
	
	//釋放資源
	private void release() {
		isRunning=false;
		CloseUtils.close(sc,client,dos);
	}
}

UDP通信的實現

UDP協議與上文提到的TCP協議不同,它是面向無連接的,對方不需要建立連接便可通信。UDP通信所發送的數據需要進行封包操作(使用DatagramPacket類),然後才能接收或發送(使用DatagramSocket類)。

DatagramPacket:數據容器(封包)

DatagramPacket類表示數據報包。數據報包用來實現封包的功能,其常用方法如下。

  • DatagramPacket(byte[] buf,int length):構造數據報包,用來接收長度爲length的數據包。
  • DatagramPacket(byte[] buf,int length,InetAddress address):構造數據報包,用來接收長度爲length的數據包發送到指定主機上的端口號。
  • getAddress():獲取發送或接收方計算機的IP地址,此數據報將要發往該機器或者是從該機器接收到。
  • getData():獲取發送或接收的數據。
  • setData(byte[] buf):設置發送的數據。

DatagramSocket:用於發送或接收數據報包

當服務器要向客戶端發送數據時,需要再服務器端產生一個DatagramSocket對象,在客戶端產生一個DatagramSocket對象。服務器端的DatagramSocket將DatagramPacket發送到網絡上,然後被客戶端的DatagramSocket接收。

DatagramSocket有兩種常用的構造器,一種無須任何參數,常用於客戶端;另一種需要指定端口,常用於服務器端:

  • DatagramSocket():用於構造數據報套接字,並將其綁定到本地主機上任何可用的端口。
  • DatagramSocket(int port):用於構造數據報套接字,並將其綁定到本地主機上的指定端口。

DatagramSocket類的常用方法:

  • send(DatagramPacket p):從此套接字發送數據報包。
  • receive(DatagramPacket p):從此套接字接收數據報包。
  • close():關閉此數據報套接字。

UDP通信編程基本步驟

  1. 創建客戶端的DatagramSocket。創建時,定義客戶端的監聽端口。
  2. 創建服務器端的DatagramSocket。創建時,定義服務器端的監聽端口。
  3. 在服務器端定義DatagramPacket對象,封裝發送的數據包。
  4. 客戶端將數據報包發送出去。
  5. 服務器端接收數據報包並解析。

以下是UDP通信的實際使用例子,以及一個諮詢系統的簡單實現:

UDP簡單使用例子

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * 基本流程:接收端
 * Address already in use: Cannot bind:同一個協議下端口不允許重複
 * 1.使用DatagramSocket 指定端口 創建接收端
 * 2.準備容器 封裝成DatagramPacket包裹
 * 3.阻塞式接收包裹receive(DatagramPacket p)
 * 4.分析數據:byte[] getData() getLength()
 * 5.釋放資源
 * @author WHZ
 *
 */
public class UdpServer {
	public static void main(String[] args) throws Exception {
		System.out.println("接收端啓動中...");
		//1.使用DatagramSocket 指定端口 創建接收端
		DatagramSocket server=new DatagramSocket(9999);
		//2.準備容器 封裝成DatagramPacket包裹
		byte[] container=new byte[1024*60];  //60KB
		DatagramPacket packet=new DatagramPacket(container,0,container.length);
		//3.阻塞式接收包裹receive(DatagramPacket p)
		server.receive(packet);  //阻塞式
		//4.分析數據:byte[] getData() getLength()
		byte[] datas=packet.getData();
		int len=packet.getLength();
		String str=new String(datas,0,len);  //將接收到的數據轉換成字符串
		System.out.println(str);
		//5.釋放資源
		server.close();
	}
}
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

/**
 * 基本流程:發送端
 * 1.使用DatagramSocket 指定端口 創建發送端
 * 2.準備數據 轉爲字節數組
 * 3.封裝成DatagramPacket包裹,需要指定目的地
 * 4.發送包裹send(DatagramPacket p)
 * 5.釋放資源
 * @author WHZ
 *
 */
public class UdpClient {
	public static void main(String[] args) throws Exception {
		System.out.println("發送端啓動中...");
		//1.使用DatagramSocket 指定端口 創建發送端
		DatagramSocket client=new DatagramSocket(8888);
		//2.準備數據 轉爲字節數組
		String data="乾坤未定,你我皆是黑馬!";
		byte[] datas=data.getBytes();
		//3.封裝成DatagramPacket包裹,需要指定目的地
		DatagramPacket packet=new DatagramPacket(datas, datas.length,
				new InetSocketAddress("localhost",9999));
		//4.發送包裹send(DatagramPacket p)
		client.send(packet);
		//5.釋放資源
		client.close();
	}
}

UDP傳輸基本數據類型

此處僅是一種例子,可以通過ByteArrayOutputStream和ByteArrayInputStream,傳輸各種數據。

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * 基本類型:接收端
 * Address already in use: Cannot bind:同一個協議下端口不允許重複
 * 1.使用DatagramSocket 指定端口 創建接收端
 * 2.準備容器 封裝成DatagramPacket包裹
 * 3.阻塞式接收包裹receive(DatagramPacket p)
 * 4.分析數據(將字節數組還原爲對應的類型):byte[] getData() getLength()
 * 5.釋放資源
 * @author WHZ
 *
 */
public class UdpTypeServer {
	public static void main(String[] args) throws Exception {
		System.out.println("接收端啓動中...");
		//1.使用DatagramSocket 指定端口 創建接收端
		DatagramSocket server=new DatagramSocket(9999);
		//2.準備容器 封裝成DatagramPacket包裹
		byte[] container=new byte[1024*60];
		DatagramPacket packet=new DatagramPacket(container, 0, container.length);
		//3.阻塞式接收包裹receive(DatagramPacket p)
		server.receive(packet);
		//4.分析數據(將字節數組還原爲對應的類型):byte[] getData() getLength()
		byte[] datas=packet.getData();
//		int len=packet.getLength();
		DataInputStream bis=new DataInputStream(new  ByteArrayInputStream(datas));
		String string=bis.readUTF();
		int a=bis.readInt();
		boolean flag=bis.readBoolean();
		char c=bis.readChar();
		System.out.println(string+a+flag+c);
		//5.釋放資源
		server.close();
	}
}
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

/**
 * 基本類型:發送端
 * 1.使用DatagramSocket 指定端口 創建發送端
 * 2.將基本類型 轉爲字節數組
 * 3.封裝成DatagramPacket包裹,需要指定目的地
 * 4.發送包裹send(DatagramPacket p)
 * 5.釋放資源
 * @author WHZ
 *
 */
public class UdpTypeClient {
	public static void main(String[] args) throws Exception {
		System.out.println("發送端啓動中...");
		//1.使用DatagramSocket 指定端口 創建發送端
		DatagramSocket client=new DatagramSocket(8888);
		//2.將基本類型 轉爲字節數組
		ByteArrayOutputStream baos=new ByteArrayOutputStream();
		DataOutputStream dos=new DataOutputStream(baos);
		dos.writeUTF("乾坤未定,你我皆是黑馬!");
		dos.writeInt(666);
		dos.writeBoolean(false);
		dos.writeChar('v');
		byte[] datas=baos.toByteArray();
		//3.封裝成DatagramPacket包裹,需要指定目的地
		DatagramPacket packet=new DatagramPacket(datas, datas.length, 
				new InetSocketAddress("localhost", 9999));
		//4.發送包裹send(DatagramPacket p)
		client.send(packet);
		//5.釋放資源
		client.close();
	}
}

學生-教師諮詢系統的設計與實現

限於篇幅不擴展推演,僅觀看嘗試理解其中原理即可,之後可能會出相關的一個系列講解一下各種項目。

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

/**
 * 接收端:使用面向對象封裝
 * @author WHZ
 *
 */
public class TalkReceive implements Runnable{
	private DatagramSocket server;
	private String name;
	public TalkReceive(int port,String name) {
		this.name=name;
		try {
			server=new DatagramSocket(port);  //指定端口創建接收端
		} catch (SocketException e) {
			e.printStackTrace();
		}
	}
	@Override
	public void run() {
		while (true) {
			byte[] container=new byte[1024*60];  //創建接收容器
			DatagramPacket packet=new DatagramPacket(container, container.length);  //封裝成DatagramPacket包裹
			try {
				server.receive(packet);  //阻塞式接收
				//分析數據
				byte[] datas=packet.getData();
				int len=packet.getLength();
				String msg=new String(datas,0,len);
				System.out.println(name+"說:"+msg);
				if (msg.equals("bye")) {
					System.out.println(name+"已下線。");
					break;
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		//關閉資源
		server.close();
	}

}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;

/**
 * 發送端:使用面向對象封裝
 * @author WHZ
 *
 */
public class TalkSend implements Runnable{
	private DatagramSocket client;
	private String toIP;
	private int toPort;
	public TalkSend(int port,String toIP,int toPort) {
		this.toIP=toIP;
		this.toPort=toPort;
		try {
			client=new DatagramSocket(port);  //指定端口創建接收端
		} catch (SocketException e) {
			e.printStackTrace();
		}
	}
	@Override
	public void run() {
		BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
		while (true) {
			try {
				String msg=br.readLine();  //接收BufferRead傳來的數據
				byte[] datas=msg.getBytes();  //將數據轉爲字節數組
				//封裝成DatagramPacket包裹
				DatagramPacket packet=new DatagramPacket(datas, datas.length, new InetSocketAddress(toIP, toPort));
				//發送包裹
				client.send(packet);
				if (msg.equals("bye")) {
					break;
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		//釋放資源
		client.close();
	}

}
public class TalkStudent {

	public static void main(String[] args) {
		System.out.println("學生端啓動中...");
		new Thread(new TalkReceive(6666, "老師")).start();  //接收
		new Thread(new TalkSend(9999, "localhost", 7777)).start();  //發送
	}

}
public class TalkTeacher {

	public static void main(String[] args) {
		System.out.println("教師端啓動中...");
		new Thread(new TalkSend(8888, "localhost", 6666)).start();  //發送
		new Thread(new TalkReceive(7777, "學生")).start();  //接收
	}

}

結語

本文至此結束,本篇文章主要就是講解了如何使用Java網絡編程實現TCP/UDP通信,並且配合例子理解。但是實戰項目對於新手不建議直接觀看,新手可以嘗試理解實現原理,嘗試使用封裝使代碼更加簡潔。本文主要內容僅是講通網絡編程,想了解更多網絡編程的相關知識可以翻閱相關書籍以及API文檔,之後如果有時間會出一個系列專門講解一下相關的實現項目。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章