Java語言是在網絡環境下誕生的,因此Java語言雖然不能說是對於網絡編程的支持最好的語言,可是必須說是一種對於網絡編程提供良好支持的語言,使用Java語言進行網絡編程將是一件比較輕鬆的工做。java
和網絡編程有關的基本API位於java.net包中,該包中包含了基本的網絡編程實現,該包是網絡編程的基礎。該包中既包含基礎的網絡編程類,也包含封裝後的專門處理WEB相關的處理類。在本章中,將只介紹基礎的網絡編程類。程序員
首先來介紹一個基礎的網絡類——InetAddress類。該類的功能是表明一個IP地址,而且將IP地址和域名相關的操做方法包含在該類的內部。編程
關於該類的使用,下面經過一個基礎的代碼示例演示該類的使用,代碼以下:數組
package inetaddressdemo;服務器
import java.net.*;網絡
/**併發
* 演示InetAddress類的基本使用dom
*/socket
public class InetAddressDemo {tcp
public static void main(String[] args) {
try{
//使用域名建立對象
InetAddress inet1 = InetAddress.getByName("www.163.com");
System.out.println(inet1);
//使用IP建立對象
InetAddress inet2 = InetAddress.getByName("127.0.0.1");
System.out.println(inet2);
//得到本機地址對象
InetAddress inet3 = InetAddress.getLocalHost();
System.out.println(inet3);
//得到對象中存儲的域名
String host = inet3.getHostName();
System.out.println("域名:" + host);
//得到對象中存儲的IP
String ip = inet3.getHostAddress();
System.out.println("IP:" + ip);
}catch(Exception e){}
}
}
在該示例代碼中,演示了InetAddress類的基本使用,並使用了該類中的幾個經常使用方法,該代碼的執行結果是:
www.163.com/220.181.28.50
/127.0.0.1
chen/192.168.1.100
域名:chen
IP:192.168.1.100
說明:因爲該代碼中包含一個互聯網的網址,因此運行該程序時須要聯網,不然將產生異常。
在後續的使用中,常常包含須要使用InetAddress對象表明IP地址的構造方法,固然,該類的使用不是必須的,也可使用字符串來表明IP地址進行實現。
按照前面的介紹,網絡通信的方式有TCP和UDP兩種,其中TCP方式的網絡通信是指在通信的過程當中保持鏈接,有點相似於打電話,只須要撥打一次號碼(創建一次網絡鏈接),就能夠屢次通話(屢次傳輸數據)。這樣方式在實際的網絡編程中,因爲傳輸可靠,相似於打電話,若是甲給乙打電話,乙說沒有聽清楚讓甲重複一遍,直到乙聽清楚爲止,實際的網絡傳輸也是這樣,若是發送的一方發送的數據接收方以爲有問題,則網絡底層會自動要求發送方重發,直到接收方收到爲止。
在Java語言中,對於TCP方式的網絡編程提供了良好的支持,在實際實現時,以java.net.Socket類表明客戶端鏈接,以java.net.ServerSocket類表明服務器端鏈接。在進行網絡編程時,底層網絡通信的細節已經實現了比較高的封裝,因此在程序員實際編程時,只須要指定IP地址和端口號碼就能夠創建鏈接了。正是因爲這種高度的封裝,一方面簡化了Java語言網絡編程的難度,另外也使得使用Java語言進行網絡編程時沒法深刻到網絡的底層,因此使用Java語言進行網絡底層系統編程很困難,具體點說,Java語言沒法實現底層的網絡嗅探以及得到IP包結構等信息。可是因爲Java語言的網絡編程比較簡單,因此仍是得到了普遍的使用。
在使用TCP方式進行網絡編程時,須要按照前面介紹的網絡編程的步驟進行,下面分別介紹一下在Java語言中客戶端和服務器端的實現步驟。
在客戶端網絡編程中,首先須要創建鏈接,在Java API中以java.net.Socket類的對象表明網絡鏈接,因此創建客戶端網絡鏈接,也就是建立Socket類型的對象,該對象表明網絡鏈接,示例以下:
Socket socket1 = new Socket(「192.168.1.103」,10000);
Socket socket2 = new Socket(「www.sohu.com」,80);
上面的代碼中,socket1實現的是鏈接到IP地址是192.168.1.103的計算機的10000號端口,而socket2實現的是鏈接到域名是www.sohu.com的計算機的80號端口,至於底層網絡如何實現創建鏈接,對於程序員來講是徹底透明的。若是創建鏈接時,本機網絡不通,或服務器端程序未開啓,則會拋出異常。
鏈接一旦創建,則完成了客戶端編程的第一步,緊接着的步驟就是按照「請求-響應」模型進行網絡數據交換,在Java語言中,數據傳輸功能由Java IO實現,也就是說只須要從鏈接中得到輸入流和輸出流便可,而後將須要發送的數據寫入鏈接對象的輸出流中,在發送完成之後從輸入流中讀取數據便可。示例代碼以下:
OutputStream os = socket1.getOutputStream(); //得到輸出流
InputStream is = socket1.getInputStream(); //得到輸入流
上面的代碼中,分別從socket1這個鏈接對象得到了輸出流和輸入流對象,在整個網絡編程中,後續的數據交換就變成了IO操做,也就是遵循「請求-響應」模型的規定,先向輸出流中寫入數據,這些數據會被系統發送出去,而後在從輸入流中讀取服務器端的反饋信息,這樣就完成了一次數據交換過程,固然這個數據交換過程能夠屢次進行。
這裏得到的只是最基本的輸出流和輸入流對象,還能夠根據前面學習到的IO知識,使用流的嵌套將這些得到到的基本流對象轉換成須要的裝飾流對象,從而方便數據的操做。
最後當數據交換完成之後,關閉網絡鏈接,釋放網絡鏈接佔用的系統端口和內存等資源,完成網絡操做,示例代碼以下:
socket1.close();
這就是最基本的網絡編程功能介紹。下面是一個簡單的網絡客戶端程序示例,該程序的做用是向服務器端發送一個字符串「Hello」,並將服務器端的反饋顯示到控制檯,數據交換隻進行一次,當數據交換進行完成之後關閉網絡鏈接,程序結束。實現的代碼以下:
package tcp;
import java.io.*;
import java.net.*;
/**
* 簡單的Socket客戶端
* 功能爲:發送字符串「Hello」到服務器端,並打印出服務器端的反饋
*/
public class SimpleSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
//服務器端IP地址
String serverIP = "127.0.0.1";
//服務器端端口號
int port = 10000;
//發送內容
String data = "Hello";
try {
//創建鏈接
socket = new Socket(serverIP,port);
//發送數據
os = socket.getOutputStream();
os.write(data.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) {
e.printStackTrace(); //打印異常信息
}finally{
try {
//關閉流和鏈接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
在該示例代碼中創建了一個鏈接到IP地址爲127.0.0.1,端口號碼爲10000的TCP類型的網絡鏈接,而後得到鏈接的輸出流對象,將須要發送的字符串「Hello」轉換爲byte數組寫入到輸出流中,由系統自動完成將輸出流中的數據發送出去,若是須要強制發送,能夠調用輸出流對象中的flush方法實現。在數據發送出去之後,從鏈接對象的輸入流中讀取服務器端的反饋信息,讀取時可使用IO中的各類讀取方法進行讀取,這裏使用最簡單的方法進行讀取,從輸入流中讀取到的內容就是服務器端的反饋,並將讀取到的內容在客戶端的控制檯進行輸出,最後依次關閉打開的流對象和網絡鏈接對象。
這是一個簡單的功能示例,在該示例中演示了TCP類型的網絡客戶端基本方法的使用,該代碼只起演示目的,還沒法達到實用的級別。
若是須要在控制檯下面編譯和運行該代碼,須要首先在控制檯下切換到源代碼所在的目錄,而後依次輸入編譯和運行命令:
javac –d . SimpleSocketClient.java
java tcp.SimpleSocketClient
和下面將要介紹的SimpleSocketServer服務器端組合運行時,程序的輸出結果爲:
服務器反饋:Hello
介紹完一個簡單的客戶端編程的示例,下面接着介紹一下TCP類型的服務器端的編寫。首先須要說明的是,客戶端的步驟和服務器端的編寫步驟不一樣,因此在學習服務器端編程時注意不要和客戶端混淆起來。
在服務器端程序編程中,因爲服務器端實現的是被動等待鏈接,因此服務器端編程的第一個步驟是監聽端口,也就是監聽是否有客戶端鏈接到達。實現服務器端監聽的代碼爲:
ServerSocket ss = new ServerSocket(10000);
該代碼實現的功能是監聽當前計算機的10000號端口,若是在執行該代碼時,10000號端口已經被別的程序佔用,那麼將拋出異常。不然將實現監聽。
服務器端編程的第二個步驟是得到鏈接。該步驟的做用是當有客戶端鏈接到達時,創建一個和客戶端鏈接對應的Socket連 接對象,從而釋放客戶端鏈接對於服務器端端口的佔用。實現功能就像公司的前臺同樣,當一個客戶到達公司時,會告訴前臺我找某某某,而後前臺就通知某某某, 而後就能夠繼續接待其它客戶了。經過得到鏈接,使得客戶端的鏈接在服務器端得到了保持,另外使得服務器端的端口釋放出來,能夠繼續等待其它的客戶端鏈接。 實現得到鏈接的代碼是:
Socket socket = ss.accept();
該代碼實現的功能是得到當前鏈接到服務器端的客戶端鏈接。須要說明的是accept和前面IO部分介紹的read方法同樣,都是一個阻塞方法,也就是當無鏈接時,該方法將阻塞程序的執行,直到鏈接到達時才執行該行代碼。另外得到的鏈接會在服務器端的該端口註冊,這樣之後就能夠經過在服務器端的註冊信息直接通訊,而註冊之後服務器端的端口就被釋放出來,又能夠繼續接受其它的鏈接了。
鏈接得到之後,後續的編程就和客戶端的網絡編程相似了,這裏得到的Socket類型的鏈接就和客戶端的網絡鏈接同樣了,只是服務器端須要首先讀取發送過來的數據,而後進行邏輯處理之後再發送給客戶端,也就是交換數據的順序和客戶端交換數據的步驟恰好相反。這部分的內容和客戶端很相似,因此就不重複了,若是還不熟悉,能夠參看下面的示例代碼。
最後,在服務器端通訊完成之後,關閉服務器端鏈接。實現的代碼爲:
ss.close();
這就是基本的TCP類型的服務器端編程步驟。下面以一個簡單的echo服務實現爲例子,介紹綜合使用示例。echo的意思就是「回聲」,echo服務器端實現的功能就是將客戶端發送的內容再原封不動的反饋給客戶端。實現的代碼以下:
package tcp;
import java.io.*;
import java.net.*;
/**
* echo服務器
* 功能:將客戶端發送的內容反饋給客戶端
*/
public class SimpleSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//監聽端口號
int port = 10000;
try {
//創建鏈接
serverSocket = new ServerSocket(port);
//得到鏈接
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(b, 0, n);
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關閉流和鏈接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中創建了一個監聽當前計算機10000號端口的服務器端Socket鏈接,而後得到客戶端發送過來的鏈接,若是有鏈接到達時,讀取鏈接中發送過來的內容,並將發送的內容在控制檯進行輸出,輸出完成之後將客戶端發送的內容再反饋給客戶端。最後關閉流和鏈接對象,結束程序。
在控制檯下面編譯和運行該程序的命令和客戶端部分的相似。
這樣,就以一個很簡單的示例演示了TCP類型的網絡編程在Java語言中的基本實現,這個示例只是演示了網絡編程的基本步驟以及各個功能方法的基本使用,只是爲網絡編程打下了一個基礎,下面將就幾個問題來深刻介紹網絡編程深層次的一些知識。
爲了一步一步的掌握網絡編程,下面再研究網絡編程中的兩個基本問題,經過解決這兩個問題將對網絡編程的認識深刻一層。
一、如何複用Socket鏈接?
在前面的示例中,客戶端中創建了一次鏈接,只發送一次數據就關閉了,這就至關於撥打電話時,電話打通了只對話一次就關閉了,其實更加經常使用的應該是撥通一次電話之後屢次對話,這就是複用客戶端鏈接。
那 麼如何實現創建一次鏈接,進行屢次數據交換呢?其實很簡單,創建鏈接之後,將數據交換的邏輯寫到一個循環中就能夠了。這樣只要循環不結束則鏈接就不會被關 閉。按照這種思路,能夠改造一下上面的代碼,讓該程序能夠在創建鏈接一次之後,發送三次數據,固然這裏的次數也能夠是屢次,示例代碼以下:
package tcp;
import java.io.*;
import java.net.*;
/**
* 複用鏈接的Socket客戶端
* 功能爲:發送字符串「Hello」到服務器端,並打印出服務器端的反饋
*/
public class MulSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
//服務器端IP地址
String serverIP = "127.0.0.1";
//服務器端端口號
int port = 10000;
//發送內容
String data[] ={"First","Second","Third"};
try {
//創建鏈接
socket = new Socket(serverIP,port);
//初始化流
os = socket.getOutputStream();
is = socket.getInputStream();
byte[] b = new byte[1024];
for(int i = 0;i < data.length;i++){
//發送數據
os.write(data[i].getBytes());
//接收數據
int n = is.read(b);
//輸出反饋數據
System.out.println("服務器反饋:" + new String(b,0,n));
}
} catch (Exception e) {
e.printStackTrace(); //打印異常信息
}finally{
try {
//關閉流和鏈接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
該示例程序和前面的代碼相比,將數據交換部分的邏輯寫在一個for循環的內容,這樣就能夠創建一次鏈接,依次將data數組中的數據按照順序發送給服務器端了。
若是仍是使用前面示例代碼中的服務器端程序運行該程序,則該程序的結果是:
java.net.SocketException: Software caused connection abort: recv failed
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at java.net.SocketInputStream.read(SocketInputStream.java:90)
at tcp.MulSocketClient.main(MulSocketClient.java:30)
服務器反饋:First
顯然,客戶端在實際運行時出現了異常,出現異常的緣由是什麼呢?若是仔細閱讀前面的代碼,應該還記得前面示例代碼中的服務器端是對話一次數據之後就關閉了鏈接,若是服務器端程序關閉了,客戶端繼續發送數據確定會出現異常,這就是出現該問題的緣由。
按照客戶端實現的邏輯,也能夠複用服務器端的鏈接,實現的原理也是將服務器端的數據交換邏輯寫在循環中便可,按照該種思路改造之後的服務器端代碼爲:
package tcp;
import java.io.*;
import java.net.*;
/**
* 複用鏈接的echo服務器
* 功能:將客戶端發送的內容反饋給客戶端
*/
public class MulSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//監聽端口號
int port = 10000;
try {
//創建鏈接
serverSocket = new ServerSocket(port);
System.out.println("服務器已啓動:");
//得到鏈接
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);
//輸出
System.out.println("客戶端發送內容爲:" + new String(b,0,n));
//向客戶端發送反饋內容
os.write(b, 0, n);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關閉流和鏈接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,也將數據發送和接收的邏輯寫在了一個for循環內部,只是在實現時硬性的將循環次數規定成了3次,這樣代碼雖然比較簡單,可是通用性比較差。
以該服務器端代碼實現爲基礎運行前面的客戶端程序時,客戶端的輸出爲:
服務器反饋:First
服務器反饋:Second
服務器反饋:Third
服務器端程序的輸出結果爲:
服務器已啓動:
客戶端發送內容爲:First
客戶端發送內容爲:Second
客戶端發送內容爲:Third
在該程序中,比較明顯的體現出了「請求-響應」模型,也就是在客戶端發起鏈接之後,首先發送字符串「First」給服務器端,服務器端輸出客戶端發送的內容「First」,而後將客戶端發送的內容再反饋給客戶端,這樣客戶端也輸出服務器反饋「First」,這樣就完成了客戶端和服務器端的一次對話,緊接着客戶端發送「Second」給服務器端,服務端輸出「Second」,而後將「Second」再反饋給客戶端,客戶端再輸出「Second」,從而完成第二次會話,第三次會話的過程和這個同樣。在這個過程當中,每次都是客戶端程序首先發送數據給服務器端,服務器接收數據之後,將結果反饋給客戶端,客戶端接收到服務器端的反饋,從而完成一次通信過程。
在該示例中,雖然解決了屢次發送的問題,可是客戶端和服務器端的次數控制還不夠靈活,若是客戶端的次數不固定怎麼辦呢?是否可使用某個特殊的字符串,例如quit,表示客戶端退出呢,這就涉及到網絡協議的內容了,會在後續的網絡應用示例部分詳細介紹。下面開始介紹另一個網絡編程的突出問題。
二、如何使服務器端支持多個客戶端同時工做?
前面介紹的服務器端程序,只是實現了概念上的服務器端,離實際的服務器端程序結構距離還很遙遠,若是須要讓服務器端可以實際使用,那麼最須要解決的問題就是——如何支持多個客戶端同時工做。
一個服務器端通常都須要同時爲多個客戶端提供通信,若是須要同時支持多個客戶端,則必須使用前面介紹的線程的概念。簡單來講,也就是當服務器端接收到一個鏈接時,啓動一個專門的線程處理和該客戶端的通信。
按照這個思路改寫的服務端示例程序將由兩個部分組成,MulThreadSocketServer類實現服務器端控制,實現接收客戶端鏈接,而後開啓專門的邏輯線程處理該鏈接,LogicThread類實現對於一個客戶端鏈接的邏輯處理,將處理的邏輯放置在該類的run方法中。該示例的代碼實現爲:
package tcp;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 支持多客戶端的服務器端實現
*/
public class MulThreadSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
//監聽端口號
int port = 10000;
try {
//創建鏈接
serverSocket = new ServerSocket(port);
System.out.println("服務器已啓動:");
while(true){
//得到鏈接
socket = serverSocket.accept();
//啓動線程
new LogicThread(socket);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關閉鏈接
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,實現了一個while形式的死循環,因爲accept方法是阻塞方法,因此當客戶端鏈接未到達時,將阻塞該程序的執行,當客戶端到達時接收該鏈接,並啓動一個新的LogicThread線程處理該鏈接,而後按照循環的執行流程,繼續等待下一個客戶端鏈接。這樣當任何一個客戶端鏈接到達時,都開啓一個專門的線程處理,經過多個線程支持多個客戶端同時處理。
下面再看一下LogicThread線程類的源代碼實現:
package tcp;
import java.io.*;
import java.net.*;
/**
* 服務器端邏輯線程
*/
public class LogicThread extends Thread {
Socket socket;
InputStream is;
OutputStream os;
public LogicThread(Socket socket){
this.socket = socket;
start(); //啓動線程
}
public void run(){
byte[] b = new byte[1024];
try{
//初始化流
os = socket.getOutputStream();
is = socket.getInputStream();
for(int i = 0;i < 3;i++){
//讀取數據
int n = is.read(b);
//邏輯處理
byte[] response = logic(b,0,n);
//反饋數據
os.write(response);
}
}catch(Exception e){
e.printStackTrace();
}finally{
close();
}
}
/**
* 關閉流和鏈接
*/
private void close(){
try{
//關閉流和鏈接
os.close();
is.close();
socket.close();
}catch(Exception e){}
}
/**
* 邏輯處理方法,實現echo邏輯
* @param b 客戶端發送數據緩衝區
* @param off 起始下標
* @param len 有效數據長度
* @return
*/
private byte[] logic(byte[] b,int off,int len){
byte[] response = new byte[len];
//將有效數據拷貝到數組response中
System.arraycopy(b, 0, response, 0, len);
return response;
}
}
在該示例代碼中,每次使用一個鏈接對象構造該線程,該鏈接對象就是該線程須要處理的鏈接,在線程構造完成之後,該線程就被啓動起來了,而後在run方法內部對客戶端鏈接進行處理,數據交換的邏輯和前面的示例代碼一致,只是這裏將接收到客戶端發送過來的數據並進行處理的邏輯封裝成了logic方法,按照前面介紹的IO編程的內容,客戶端發送過來的內容存儲在數組b的起始下標爲0,長度爲n箇中,這些數據是客戶端發送過來的有效數據,將有效的數據傳遞給logic方法,logic方法實現的是echo服務的邏輯,也就是將客戶端發送的有效數據造成之後新的response數組,並做爲返回值反饋。
在線程中將logic方法的返回值反饋給客戶端,這樣就完成了服務器端的邏輯處理模擬,其餘的實現和前面的介紹相似,這裏就不在重複了。
這裏的示例還只是基礎的服務器端實現,在實際的服務器端實現中,因爲硬件和端口數的限制,因此不能無限制的建立線程對象,並且頻繁的建立線程對象效率也比較低,因此程序中都實現了線程池來提升程序的執行效率。
這裏簡單介紹一下線程池的概念,線程池(Thread pool)是池技術的一種,就是在程序啓動時首先把須要個數的線程對象建立好,例如建立5000個線程對象,而後當客戶端鏈接到達時從池中取出一個已經建立完成的線程對象使用便可。當客戶端鏈接關閉之後,將該線程對象從新放入到線程池中供其它的客戶端重複使用,這樣能夠提升程序的執行速度,優化程序對於內存的佔用等。
關於基礎的TCP方式的網絡編程就介紹這麼多,下面介紹UDP方式的網絡編程在Java語言中的實現。
網絡通信的方式除了TCP方式之外,還有一種實現的方式就是UDP方式。UDP(User Datagram Protocol),中文意思是用戶數據報協議,方式相似於發短信息,是一種物美價廉的通信方式,使用該種方式無需創建專用的虛擬鏈接,因爲無需創建專用的鏈接,因此對於服務器的壓力要比TCP小不少,因此也是一種常見的網絡編程方式。可是使用該種方式最大的不足是傳輸不可靠,固然也不是說常常丟失,就像你們發短信息同樣,理論上存在收不到的可能,這種可能性多是1%,反正比較小,可是因爲這種可能的存在,因此平時咱們都以爲重要的事情仍是打個電話吧(相似TCP方式),通常的事情才發短信息(相似UDP方式)。網絡編程中也是這樣,必需要求可靠傳輸的信息通常使用TCP方式實現,通常的數據才使用UDP方式實現。
UDP方式的網絡編程也在Java語言中得到了良好的支持,因爲其在傳輸數據的過程當中不須要創建專用的鏈接等特色,因此在Java API中設計的實現結構和TCP方式不太同樣。固然,須要使用的類仍是包含在java.net包中。
在Java API中,實現UDP方式的編程,包含客戶端網絡編程和服務器端網絡編程,主要由兩個類實現,分別是:
l DatagramSocket
DatagramSocket類實現「網絡鏈接」,包括客戶端網絡鏈接和服務器端網絡鏈接。雖然UDP方式的網絡通信不須要創建專用的網絡鏈接,可是畢竟仍是須要發送和接收數據,DatagramSocket實現的就是發送數據時的發射器,以及接收數據時的監聽器的角色。類比於TCP中的網絡鏈接,該類既能夠用於實現客戶端鏈接,也能夠用於實現服務器端鏈接。
l DatagramPacket
DatagramPacket類實現對於網絡中傳輸的數據封裝,也就是說,該類的對象表明網絡中交換的數據。在UDP方式的網絡編程中,不管是須要發送的數據仍是須要接收的數據,都必須被處理成DatagramPacket類型的對象,該對象中包含發送到的地址、發送到的端口號以及發送的內容等。其實DatagramPacket類的做用相似於現實中的信件,在信件中包含信件發送到的地址以及接收人,還有發送的內容等,郵局只須要按照地址傳遞便可。在接收數據時,接收到的數據也必須被處理成DatagramPacket類型的對象,在該對象中包含發送方的地址、端口號等信息,也包含數據的內容。和TCP方式的網絡傳輸相比,IO編程在UDP方式的網絡編程中變得不是必須的內容,結構也要比TCP方式的網絡編程簡單一些。
下面介紹一下UDP方式的網絡編程中,客戶端和服務器端的實現步驟,以及經過基礎的示例演示UDP方式的網絡編程在Java語言中的實現方式。
UDP方式的網絡編程,編程的步驟和TCP方式相似,只是使用的類和方法存在比較大的區別,下面首先介紹一下UDP方式的網絡編程客戶端實現過程。
UDP客戶端編程涉及的步驟也是4個部分:創建鏈接、發送數據、接收數據和關閉鏈接。
首先介紹UDP方式的網絡編程中創建鏈接的實現。其中UDP方式的創建鏈接和TCP方式不一樣,只須要創建一個鏈接對象便可,不須要指定服務器的IP和端口號碼。實現的代碼爲:
DatagramSocket ds = new DatagramSocket();
這樣就創建了一個客戶端鏈接,該客戶端鏈接使用系統隨機分配的一個本地計算機的未用端口號。在該鏈接中,不指定服務器端的IP和端口,因此UDP方式的網絡鏈接更像一個發射器,而不是一個具體的鏈接。
固然,能夠經過制定鏈接使用的端口號來建立客戶端鏈接。
DatagramSocket ds = new DatagramSocket(5000);
這樣就是使用本地計算機的5000號端口創建了一個鏈接。通常在創建客戶端鏈接時沒有必要指定端口號碼。
接着,介紹一下UDP客戶端編程中發送數據的實現。在UDP方式的網絡編程中,IO技術不是必須的,在發送數據時,須要將須要發送的數據內容首先轉換爲byte數組,而後將數據內容、服務器IP和服務器端口號一塊兒構形成一個DatagramPacket類型的對象,這樣數據的準備就完成了,發送時調用網絡鏈接對象中的send方法發送該對象便可。例如將字符串「Hello」發送到IP是127.0.0.1,端口號是10001的服務器,則實現發送數據的代碼以下:
String s = 「Hello」;
String host = 「127.0.0.1」;
int port = 10001;
//將發送的內容轉換爲byte數組
byte[] b = s.getBytes();
//將服務器IP轉換爲InetAddress對象
InetAddress server = InetAddress.getByName(host);
//構造發送的數據包對象
DatagramPacket sendDp = new DatagramPacket(b,b.length,server,port);
//發送數據
ds.send(sendDp);
在該示例代碼中,無論發送的數據內容是什麼,都須要轉換爲byte數組,而後將服務器端的IP地址構形成InetAddress類型的對象,在準備完成之後,將這些信息構形成一個DatagramPacket類型的對象,在UDP編程中,發送的數據內容、服務器端的IP和端口號,都包含在DatagramPacket對象中。在準備完成之後,調用鏈接對象ds的send方法把DatagramPacket對象發送出去便可。
按照UDP協議的約定,在進行數據傳輸時,系統只是盡全力傳輸數據,可是並不保證數據必定被正確傳輸,若是數據在傳輸過程當中丟失,那就丟失了。
UDP方式在進行網絡通信時,也遵循「請求-響應」模型,在發送數據完成之後,就能夠接收服務器端的反饋數據了。
下面介紹一下UDP客戶端編程中接收數據的實現。當數據發送出去之後,就能夠接收服務器端的反饋信息了。接收數據在Java語言中的實現是這樣的:首先構造一個數據緩衝數組,該數組用於存儲接收的服務器端反饋數據,該數組的長度必須大於或等於服務器端反饋的實際有效數據的長度。而後以該緩衝數組爲基礎構造一個DatagramPacket數據包對象,最後調用鏈接對象的receive方法接收數據便可。接收到的服務器端反饋數據存儲在DatagramPacket類型的對象內部。實現接收數據以及顯示服務器端反饋內容的示例代碼以下:
//構造緩衝數組
byte[] data = new byte[1024];
//構造數據包對象
DatagramPacket received = new DatagramPacket(data,data.length);
//接收數據
ds.receive(receiveDp);
//輸出數據內容
byte[] b = receiveDp.getData(); //得到緩衝數組
int len = receiveDp.getLength(); //得到有效數據長度
String s = new String(b,0,len);
System.out.println(s);
在該代碼中,首先構造緩衝數組data,這裏設置的長度1024是預估的接收到的數據長度,要求該長度必須大於或等於接收到的數據長度,而後以該緩衝數組爲基礎,構造數據包對象,使用鏈接對象ds的receive方法接收反饋數據,因爲在Java語言中,除String之外的其它對象都是按照地址傳遞,因此在receive方法內部能夠改變數據包對象receiveDp的內容,這裏的receiveDp的功能和返回值相似。數據接收到之後,只須要從數據包對象中讀取出來就能夠了,使用DatagramPacket對象中的getData方法能夠得到數據包對象的緩衝區數組,可是緩衝區數組的長度通常大於有效數據的長度,換句話說,也就是緩衝區數組中只有一部分數據是反饋數據,因此須要使用DatagramPacket對象中的getLength方法得到有效數據的長度,則有效數據就是緩衝數組中的前有效數據長度個內容,這些纔是真正的服務器端反饋的數據的內容。
UDP方式客戶端網絡編程的最後一個步驟就是關閉鏈接。雖然UDP方式不創建專用的虛擬鏈接,可是鏈接對象仍是須要佔用系統資源,因此在使用完成之後必須關閉鏈接。關閉鏈接使用鏈接對象中的close方法便可,實現的代碼以下:
ds.close();
須要說明的是,和TCP創建鏈接的方式不一樣,UDP方式的同一個網絡鏈接對象,能夠發送到達不一樣服務器端IP或端口的數據包,這點是TCP方式沒法作到的。
介紹完了UDP方式客戶端網絡編程的基礎知識之後,下面再來介紹一下UDP方式服務器端網絡編程的基礎知識。
UDP方式網絡編程的服務器端實現和TCP方式的服務器端實現相似,也是服務器端監聽某個端口,而後得到數據包,進行邏輯處理之後將處理之後的結果反饋給客戶端,最後關閉網絡鏈接,下面依次進行介紹。
首先UDP方式服務器端網絡編程須要創建一個鏈接,該鏈接監聽某個端口,實現的代碼爲:
DatagramSocket ds = new DatagramSocket(10010);
因爲服務器端的端口須要固定,因此通常在創建服務器端鏈接時,都指定端口號。例如該示例代碼中指定10010端口爲服務器端使用的端口號,客戶端端在鏈接服務器端時鏈接該端口號便可。
接着服務器端就開始接收客戶端發送過來的數據,其接收的方法和客戶端接收的方法一直,其中receive方法的做用相似於TCP方式中accept方法的做用,該方法也是一個阻塞方法,其做用是接收數據。
接收到客戶端發送過來的數據之後,服務器端對該數據進行邏輯處理,而後將處理之後的結果再發送給客戶端,在這裏發送時就比客戶端要麻煩一些,由於服務器端須要得到客戶端的IP和客戶端使用的端口號,這個均可以從接收到的數據包中得到。示例代碼以下:
//得到客戶端的IP
InetAddress clientIP = receiveDp.getAddress();
//得到客戶端的端口號
Int clientPort = receiveDp.getPort();
使用以上代碼,就能夠從接收到的數據包對象receiveDp中得到客戶端的IP地址和客戶端的端口號,這樣就能夠在服務器端中將處理之後的數據構形成數據包對象,而後將處理之後的數據內容反饋給客戶端了。
最後,當服務器端實現完成之後,關閉服務器端鏈接,實現的方式爲調用鏈接對象的close方法,示例代碼以下:
ds.close();
介紹完了UDP方式下的客戶端編程和服務器端編程的基礎知識之後,下面經過一個簡單的示例演示UDP網絡編程的基本使用。
該示例的功能是實現將客戶端程序的系統時間發送給服務器端,服務器端接收到時間之後,向客戶端反饋字符串「OK」。實現該功能的客戶端代碼以下所示:
package udp;
import java.net.*;
import java.util.*;
/**
* 簡單的UDP客戶端,實現向服務器端發生系統時間功能
*/
public class SimpleUDPClient {
public static void main(String[] args) {
DatagramSocket ds = null; //鏈接對象
DatagramPacket sendDp; //發送數據包對象
DatagramPacket receiveDp; //接收數據包對象
String serverHost = "127.0.0.1"; //服務器IP
int serverPort = 10010; //服務器端口號
try{
//創建鏈接
ds = new DatagramSocket();
//初始化發送數據
Date d = new Date(); //當前時間
String content = d.toString(); //轉換爲字符串
byte[] data = content.getBytes();
//初始化發送包對象
InetAddress address = InetAddress.getByName(serverHost);
sendDp = new DatagramPacket(data,data.length,address,serverPort);
//發送
ds.send(sendDp);
//初始化接收數據
byte[] b = new byte[1024];
receiveDp = new DatagramPacket(b,b.length);
//接收
ds.receive(receiveDp);
//讀取反饋內容,並輸出
byte[] response = receiveDp.getData();
int len = receiveDp.getLength();
String s = new String(response,0,len);
System.out.println("服務器端反饋爲:" + s);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//關閉鏈接
ds.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,首先創建UDP方式的網絡鏈接,而後得到當前系統時間,這裏得到的系統時間是客戶端程序運行的本地計算機的時間,而後將時間字符串以及服務器端的IP和端口,構形成發送數據包對象,調用鏈接對象ds的send方法發送出去。在數據發送出去之後,構造接收數據的數據包對象,調用鏈接對象ds的receive方法接收服務器端的反饋,並輸出在控制檯。最後在finally語句塊中關閉客戶端網絡鏈接。
和下面將要介紹的服務器端一塊兒運行時,客戶端程序的輸出結果爲:
服務器端反饋爲:OK
下面是該示例程序的服務器端代碼實現:
package udp;
import java.net.*;
/**
* 簡單UDP服務器端,實現功能是輸出客戶端發送數據,
並反饋字符串「OK"給客戶端
*/
public class SimpleUDPServer {
public static void main(String[] args) {
DatagramSocket ds = null; //鏈接對象
DatagramPacket sendDp; //發送數據包對象
DatagramPacket receiveDp; //接收數據包對象
final int PORT = 10010; //端口
try{
//創建鏈接,監聽端口
ds = new DatagramSocket(PORT);
System.out.println("服務器端已啓動:");
//初始化接收數據
byte[] b = new byte[1024];
receiveDp = new DatagramPacket(b,b.length);
//接收
ds.receive(receiveDp);
//讀取反饋內容,並輸出
InetAddress clientIP = receiveDp.getAddress();
int clientPort = receiveDp.getPort();
byte[] data = receiveDp.getData();
int len = receiveDp.getLength();
System.out.println("客戶端IP:" + clientIP.getHostAddress());
System.out.println("客戶端端口:" + clientPort);
System.out.println("客戶端發送內容:" + new String(data,0,len));
//發送反饋
String response = "OK";
byte[] bData = response.getBytes();
sendDp = new DatagramPacket(bData,bData.length,clientIP,clientPort);
//發送
ds.send(sendDp);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//關閉鏈接
ds.close();
}catch(Exception e){}
}
}
}
在該服務器端實現中,首先監聽10010號端口,和TCP方式的網絡編程相似,服務器端的receive方法是阻塞方法,若是客戶端不發送數據,則程序會在該方法處阻塞。當客戶端發送數據到達服務器端時,則接收客戶端發送過來的數據,而後將客戶端發送的數據內容讀取出來,並在服務器端程序中打印客戶端的相關信息,從客戶端發送過來的數據包中能夠讀取出客戶端的IP以及客戶端端口號,將反饋數據字符串「OK」發送給客戶端,最後關閉服務器端鏈接,釋放佔用的系統資源,完成程序功能示例。
和前面TCP方式中的網絡編程相似,這個示例也僅僅是網絡編程的功能示例,也存在前面介紹的客戶端沒法進行屢次數據交換,以及服務器端不支持多個客戶端的問題,這兩個問題也須要對於代碼進行處理才能夠很方便的進行解決。
在解決該問題之前,須要特別指出的是UDP方式的網絡編程因爲不創建虛擬的鏈接,因此在實際使用時和TCP方式存在不少的不一樣,最大的一個不一樣就是「無狀態」。該特色指每次服務器端都收到信息,可是這些信息和鏈接無關,換句話說,也就是服務器端只是從信息是沒法識別出是誰發送的,這樣就要求發送信息時的內容須要多一些,這個在後續的示例中能夠看到。
下面是實現客戶端屢次發送以及服務器端支持多個數據包同時處理的程序結構,實現的原理和TCP方式相似,在客戶端將數據的發送和接收放入循環中,而服務器端則將接收到的每一個數據包啓動一個專門的線程進行處理。實現的代碼以下:
package udp;
import java.net.*;
import java.util.*;
/**
* 簡單的UDP客戶端,實現向服務器端發生系統時間功能
* 該程序發送3次數據到服務器端
*/
public class MulUDPClient {
public static void main(String[] args) {
DatagramSocket ds = null; //鏈接對象
DatagramPacket sendDp; //發送數據包對象
DatagramPacket receiveDp; //接收數據包對象
String serverHost = "127.0.0.1"; //服務器IP
int serverPort = 10012; //服務器端口號
try{
//創建鏈接
ds = new DatagramSocket();
//初始化
InetAddress address = InetAddress.getByName(serverHost);
byte[] b = new byte[1024];
receiveDp = new DatagramPacket(b,b.length);
System.out.println("客戶端準備完成");
//循環30次,每次間隔0.01秒
for(int i = 0;i < 30;i++){
//初始化發送數據
Date d = new Date(); //當前時間
String content = d.toString(); //轉換爲字符串
byte[] data = content.getBytes();
//初始化發送包對象
sendDp = new DatagramPacket(data,data.length,address, serverPort);
//發送
ds.send(sendDp);
//延遲
Thread.sleep(10);
//接收
ds.receive(receiveDp);
//讀取反饋內容,並輸出
byte[] response = receiveDp.getData();
int len = receiveDp.getLength();
String s = new String(response,0,len);
System.out.println("服務器端反饋爲:" + s);
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//關閉鏈接
ds.close();
}catch(Exception e){}
}
}
}
在該示例中,將和服務器端進行數據交換的邏輯寫在一個for循環的內部,這樣就能夠實現和服務器端的屢次交換了,考慮到服務器端的響應速度,在每次發送之間加入0.01秒的時間間隔。最後當數據交換完成之後關閉鏈接,結束程序。
實現該邏輯的服務器端程序代碼以下:
package udp;
import java.net.*;
/**
* 能夠併發處理數據包的服務器端
* 功能爲:顯示客戶端發送的內容,並向客戶端反饋字符串「OK」
*/
public class MulUDPServer {
public static void main(String[] args) {
DatagramSocket ds = null; //鏈接對象
DatagramPacket receiveDp; //接收數據包對象
final int PORT = 10012; //端口
byte[] b = new byte[1024];
receiveDp = new DatagramPacket(b,b.length);
try{
//創建鏈接,監聽端口
ds = new DatagramSocket(PORT);
System.out.println("服務器端已啓動:");
while(true){
//接收
ds.receive(receiveDp);
//啓動線程處理數據包
new LogicThread(ds,receiveDp);
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//關閉鏈接
ds.close();
}catch(Exception e){}
}
}
}
該代碼實現了服務器端的接收邏輯,使用一個循環來接收客戶端發送過來的數據包,當接收到數據包之後啓動一個LogicThread線程處理該數據包。這樣服務器端就能夠實現同時處理多個數據包了。
實現邏輯處理的線程代碼以下:
package udp;
import java.net.*;
/**
* 邏輯處理線程
*/
public class LogicThread extends Thread {
/**鏈接對象*/
DatagramSocket ds;
/**接收到的數據包*/
DatagramPacket dp;
public LogicThread(DatagramSocket ds,DatagramPacket dp){
this.ds = ds;
this.dp = dp;
start(); //啓動線程
}
public void run(){
try{
//得到緩衝數組
byte[] data = dp.getData();
//得到有效數據長度
int len = dp.getLength();
//客戶端IP
InetAddress clientAddress = dp.getAddress();
//客戶端端口
int clientPort = dp.getPort();
//輸出
System.out.println("客戶端IP:" + clientAddress.getHostAddress());
System.out.println("客戶端端口號:" + clientPort);
System.out.println("客戶端發送內容:" + new String(data,0,len));
//反饋到客戶端
byte[] b = "OK".getBytes();
DatagramPacket sendDp = new DatagramPacket(b,b.length,clientAddress,clientPort);
//發送
ds.send(sendDp);
}catch(Exception e){
e.printStackTrace();
}
}
}
在該線程中,只處理一次UDP通信,當通信結束之後線程死亡,在線程內部,每次得到客戶端發送過來的信息,將得到的信息輸出到服務器端程序的控制檯,而後向客戶端反饋字符串「OK」。
因爲UDP數據傳輸過程當中可能存在丟失,因此在運行該程序時可能會出現程序阻塞的狀況。若是須要避免該問題,能夠將客戶端的網絡發送部分也修改爲線程實現。
關於基礎的UDP網絡編程就介紹這麼多了,下面將介紹一下網絡協議的概念。
網絡協議
對於須要從事網絡編程的程序員來講,網絡協議是一個須要深入理解的概念。那麼什麼是網絡協議呢?
網絡協議是指對於網絡中傳輸的數據格式的規定。對於網絡編程初學者來講,沒有必要深刻了解TCP/IP協議簇,因此對於初學者來講去讀大部頭的《TCP/IP協議》也不是一件很合適的事情,由於深刻了解TCP/IP協議是網絡編程提升階段,也是深刻網絡編程底層時才須要作的事情。
對於通常的網絡編程來講,更多的是關心網絡上傳輸的邏輯數據內容,也就是更多的是應用層上的網絡協議,因此後續的內容均以實際應用的數據爲基礎來介紹網絡協議的概念。
那麼什麼是網絡協議呢,下面看一個簡單的例子。春節晚會上「小瀋陽」和趙本山合做的小品《不差錢》中,小瀋陽和趙本山之間就設計了一個協議,協議的內容爲:
若是點的菜價錢比較貴是,就說沒有。
按照該協議的規定,就有了下面的對話:
趙本山:4斤的龍蝦
小瀋陽:(通過判斷,得出價格比較高),沒有
趙本山:鮑魚
小瀋陽:(通過判斷,得出價格比較高),沒有
這就是一種雙方達成的一種協議約定,其實這種約定的實質和網絡協議的實質是同樣的。網絡協議的實質也是客戶端程序和服務器端程序對於數據的一種約定,只是因爲以計算機爲基礎,因此更多的是使用數字來表明內容,這樣就顯得比較抽象一些。
下 面再舉一個簡單的例子,介紹一些基礎的網絡協議設計的知識。例如須要設計一個簡單的網絡程序:網絡計算器。也就是在客戶端輸入須要計算的數字和運算符,在 服務器端實現計算,並將計算的結果反饋給客戶端。在這個例子中,就須要約定兩個數據格式:客戶端發送給服務器端的數據格式,以及服務器端反饋給客戶端的數 據格式。
可能你以爲這個比較簡單,例如客戶端輸入的數字依次是12和432,輸入的運算符是加號,可能最容易想到的數據格式是造成字符串「12+432」,這樣格式的確比較容易閱讀,可是服務器端在進行計算時,邏輯就比較麻煩,由於須要首先拆分該字符串,而後才能進行計算,因此可用的數據格式就有了一下幾種:
「12,432,+」 格式爲:第一個數字,第二個數字,運算符
「12,+,432」 格式爲:第一個數字,運算符,第二個數字
其實以上兩種數據格式很接近,比較容易閱讀,在服務器端收到該數據格式之後,使用「,」爲分隔符分割字符串便可。
假設對於運算符再進行一次約定,例如約定數字0表明+,1表明減,2表明乘,3表明除,總體格式遵循以上第一種格式,則上面的數字生產的協議數據爲:
「12,432,0」
這就是一種基本的發送的協議約定了。
另 外一個須要設計的協議格式就是服務器端反饋的數據格式,其實服務器端主要反饋計算結果,可是在實際接受數據時,有可能存在格式錯誤的狀況,這樣就須要簡單 的設計一下服務器端反饋的數據格式了。例如規定,若是發送的數據格式正確,則反饋結果,不然反饋字符串「錯誤」。這樣就有了如下的數據格式:
客戶端:「1,111,1」 服務器端:」-110」
客戶端:「123,23,0」 服務器端:「146」
客戶端:「1,2,5」 服務器端:「錯誤」
這樣就設計出了一種最最基本的網絡協議格式,從該示例中能夠看出,網絡協議就是一種格式上的約定,能夠根據邏輯的須要約定出各類數據格式,在進行設計時通常遵循「簡單、通用、容易解析」的原則進行。
而對於複雜的網絡程序來講,須要傳輸的數據種類和數據量都比較大,這樣只須要依次設計出每種狀況下的數據格式便可,例如QQ程序,在該程序中須要進行傳輸的網絡數據種類不少,那麼在設計時就能夠遵循:登陸格式、註冊格式、發送消息格式等等,一一進行設計便可。因此對於複雜的網絡程序來講,只是增長了更多的命令格式,在實際設計時的工做量增長不是太大。
無論怎麼說,在網絡編程中,對於同一個網絡程序來講,通常都會涉及到兩個網絡協議格式:客戶端發送數據格式和服務器端反饋數據格式,在實際設計時,須要一一對應。這就是最基本的網絡協議的知識。
網絡協議設計完成之後,在進行網絡編程時,就須要根據設計好的協議格式,在程序中進行對應的編碼了,客戶端程序和服務器端程序須要進行協議處理的代碼分別以下。
客戶端程序須要完成的處理爲:
一、 客戶端發送協議格式的生成
二、 服務器端反饋數據格式的解析
服務器端程序須要完成的處理爲:
一、 服務器端反饋協議格式的生成
二、 客戶端發送協議格式的解析
這裏的生成是指將計算好的數據,轉換成規定的數據格式,這裏的解析指,從反饋的數據格式中拆分出須要的數據。在進行對應的代碼編寫時,嚴格遵循協議約定便可。
因此,對於程序員來講,在進行網絡程序編寫時,須要首先根據邏輯的須要設計網絡協議格式,而後遵循協議格式約定進行協議生成和解析代碼的編寫,最後使用網絡編程技術實現整個網絡編程的功能。
因爲各類網絡程序使用不一樣的協議格式,因此不一樣網絡程序的客戶端之間沒法通用。
而對於常見協議的格式,例如HTTP(Hyper Text Transfer Protocol,超文本傳輸協議)、FTP(File Transfer Protocol,文件傳輸協議),SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協議)等等,都有通用的規定,具體能夠查閱相關的RFC文檔。
最後,對於一種網絡程序來講,網絡協議格式是該程序最核心的技術祕密,由於一旦協議格式泄漏,則任何一我的均可以根據該格式進行客戶端的編寫,這樣將影響服務器端的實現,也容易出現一些其它的影響。
13.2.6小結
關於網絡編程基本的技術就介紹這麼多,該部分介紹了網絡編程的基礎知識,以及Java語言對於網絡編程的支持,網絡編程的步驟等,並詳細介紹了TCP方式網絡編程和UDP方式網絡編程在Java語言中的實現。
網絡協議也是網絡程序的核心,因此在實際開始進行網絡編程時,設計一個良好的協議格式也是必須進行的工做。
網絡編程示例
「實踐出真知」,因此在進行技術學習時,仍是須要進行不少的練習,才能夠體會技術的奧妙,下面經過兩個簡單的示例,演示網絡編程的實際使用。
13.3.1質數判別示例
該示例實現的功能是質數判斷,程序實現的功能爲客戶端程序接收用戶輸入的數字,而後將用戶輸入的內容發送給服務器端,服務器端判斷客戶端發送的數字是不是質數,並將判斷的結果反饋給客戶端,客戶端根據服務器端的反饋顯示判斷結果。
質數的規則是:最小的質數是2,只能被1和自身整除的天然數。當用戶輸入小於2的數字,以及輸入的內容不是天然數時,都屬於非法輸入。
網絡程序的功能都分爲客戶端程序和服務器端程序實現,下面先描述一下每一個程序分別實現的功能:
一、 客戶端程序功能:
a) 接收用戶控制檯輸入
b) 判斷輸入內容是否合法
c) 按照協議格式生成發送數據
d) 發送數據
e) 接收服務器端反饋
f) 解析服務器端反饋信息,並輸出
二、 服務器端程序功能:
a) 接收客戶端發送數據
b) 按照協議格式解析數據
c) 判斷數字是不是質數
d) 根據判斷結果,生成協議數據
e) 將數據反饋給客戶端
分解好了網絡程序的功能之後,就能夠設計網絡協議格式了,若是該程序的功能比較簡單,因此設計出的協議格式也不復雜。
客戶端發送協議格式:
將用戶輸入的數字轉換爲字符串,再將字符串轉換爲byte數組便可。
例如用戶輸入16,則轉換爲字符串「16」,使用getBytes轉換爲byte數組。
客戶端發送「quit」字符串表明結束鏈接
服務器端發送協議格式:
反饋數據長度爲1個字節。數字0表明是質數,1表明不是質數,2表明協議格式錯誤。
例如客戶端發送數字12,則反饋1,發送13則反饋0,發送0則反饋2。
功能設計完成之後,就能夠分別進行客戶端和服務器端程序的編寫了,在編寫完成之後聯合起來進行調試便可。
下面分別以TCP方式和UDP方式實現該程序,注意其實現上的差別。無論使用哪一種方式實現,客戶端均可以屢次輸入數據進行判斷。對於UDP方式來講,不須要向服務器端發送quit字符串。
以TCP方式實現的客戶端程序代碼以下:
package example1;
import java.io.*;
import java.net.*;
/**
* 以TCP方式實現的質數判斷客戶端程序
*/
public class TCPPrimeClient {
static BufferedReader br;
static Socket socket;
static InputStream is;
static OutputStream os;
/**服務器IP*/
final static String HOST = "127.0.0.1";
/**服務器端端口*/
final static int PORT = 10005;
public static void main(String[] args) {
init(); //初始化
while(true){
System.out.println("請輸入數字:");
String input = readInput(); //讀取輸入
if(isQuit(input)){ //判讀是否結束
byte[] b = "quit".getBytes();
send(b);
break; //結束程序
}
if(checkInput(input)){ //校驗合法
//發送數據
send(input.getBytes());
//接收數據
byte[] data = receive();
//解析反饋數據
parse(data);
}else{
System.out.println("輸入不合法,請從新輸入!");
}
}
close(); //關閉流和鏈接
}
/**
* 初始化
*/
private static void init(){
try {
br = new BufferedReader(
new InputStreamReader(System.in));
socket = new Socket(HOST,PORT);
is = socket.getInputStream();
os = socket.getOutputStream();
} catch (Exception e) {}
}
/**
* 讀取客戶端輸入
*/
private static String readInput(){
try {
return br.readLine();
} catch (Exception e) {
return null;
}
}
/**
* 判斷是否輸入quit
* @param input 輸入內容
* @return true表明結束,false表明不結束
*/
private static boolean isQuit(String input){
if(input == null){
return false;
}else{
if("quit".equalsIgnoreCase(input)){
return true;
}else{
return false;
}
}
}
/**
* 校驗輸入
* @param input 用戶輸入內容
* @return true表明輸入符合要求,false表明不符合
*/
private static boolean checkInput(String input){
if(input == null){
return false;
}
try{
int n = Integer.parseInt(input);
if(n >= 2){
return true;
}else{
return false;
}
}catch(Exception e){
return false; //輸入不是整數
}
}
/**
* 向服務器端發送數據
* @param data 數據內容
*/
private static void send(byte[] data){
try{
os.write(data);
}catch(Exception e){}
}
/**
* 接收服務器端反饋
* @return 反饋數據
*/
private static byte[] receive(){
byte[] b = new byte[1024];
try {
int n = is.read(b);
byte[] data = new byte[n];
//複製有效數據
System.arraycopy(b, 0, data, 0, n);
return data;
} catch (Exception e){}
return null;
}
/**
* 解析協議數據
* @param data 協議數據
*/
private static void parse(byte[] data){
if(data == null){
System.out.println("服務器端反饋數據不正確!");
return;
}
byte value = data[0]; //取第一個byte
//按照協議格式解析
switch(value){
case 0:
System.out.println("質數");
break;
case 1:
System.out.println("不是質數");
break;
case 2:
System.out.println("協議格式錯誤");
break;
}
}
/**
* 關閉流和鏈接
*/
private static void close(){
try{
br.close();
is.close();
os.close();
socket.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
在該代碼中,將程序的功能使用方法進行組織,使得結構比較清晰,核心的邏輯流程在main方法中實現。
以TCP方式實現的服務器端的代碼以下:
package example1;
import java.net.*;
/**
* 以TCP方式實現的質數判別服務器端
*/
public class TCPPrimeServer {
public static void main(String[] args) {
final int PORT = 10005;
ServerSocket ss = null;
try {
ss = new ServerSocket(PORT);
System.out.println("服務器端已啓動:");
while(true){
Socket s = ss.accept();
new PrimeLogicThread(s);
}
} catch (Exception e) {}
finally{
try {
ss.close();
} catch (Exception e2) {}
}
}
}
package example1;
import java.io.*;
import java.net.*;
/**
* 實現質數判別邏輯的線程
*/
public class PrimeLogicThread extends Thread {
Socket socket;
InputStream is;
OutputStream os;
public PrimeLogicThread(Socket socket){
this.socket = socket;
init();
start();
}
/**
* 初始化
*/
private void init(){
try{
is = socket.getInputStream();
os = socket.getOutputStream();
}catch(Exception e){}
}
public void run(){
while(true){
//接收客戶端反饋
byte[] data = receive();
//判斷是不是退出
if(isQuit(data)){
break; //結束循環
}
//邏輯處理
byte[] b = logic(data);
//反饋數據
send(b);
}
close();
}
/**
* 接收客戶端數據
* @return 客戶端發送的數據
*/
private byte[] receive(){
byte[] b = new byte[1024];
try {
int n = is.read(b);
byte[] data = new byte[n];
//複製有效數據
System.arraycopy(b, 0, data, 0, n);
return data;
} catch (Exception e){}
return null;
}
/**
* 向客戶端發送數據
* @param data 數據內容
*/
private void send(byte[] data){
try{
os.write(data);
}catch(Exception e){}
}
/**
* 判斷是不是quit
* @return 是返回true,不然返回false
*/
private boolean isQuit(byte[] data){
if(data == null){
return false;
}else{
String s = new String(data);
if(s.equalsIgnoreCase("quit")){
return true;
}else{
return false;
}
}
}
private byte[] logic(byte[] data){
//反饋數組
byte[] b = new byte[1];
//校驗參數
if(data == null){
b[0] = 2;
return b;
}
try{
//轉換爲數字
String s = new String(data);
int n = Integer.parseInt(s);
//判斷是不是質數
if(n >= 2){
boolean flag = isPrime(n);
if(flag){
b[0] = 0;
}else{
b[0] = 1;
}
}else{
b[0] = 2; //格式錯誤
System.out.println(n);
}
}catch(Exception e){
e.printStackTrace();
b[0] = 2;
}
return b;
}
/**
*
* @param n
* @return
*/
private boolean isPrime(int n){
boolean b = true;
for(int i = 2;i <= Math.sqrt(n);i++){
if(n % i == 0){
b = false;
break;
}
}
return b;
}
/**
* 關閉鏈接
*/
private void close(){
try {
is.close();
os.close();
socket.close();
} catch (Exception e){}
}
}
本示例使用的服務器端的結構和前面示例中的結構一致,只是邏輯線程的實現相對來講要複雜一些,在線程類中的logic方法中實現了服務器端邏輯,根據客戶端發送過來的數據,判斷是不是質數,而後根據判斷結果按照協議格式要求,生成客戶端反饋數據,實現服務器端要求的功能。
猜數字小遊戲
下面這個示例是一個猜數字的控制檯小遊戲。該遊戲的規則是:當客戶端第一次鏈接到服務器端時,服務器端生產一個【0,50】之間的隨機數字,而後客戶端輸入數字來猜該數字,每次客戶端輸入數字之後,發送給服務器端,服務器端判斷該客戶端發送的數字和隨機數字的關係,並反饋比較結果,客戶端總共有5次猜的機會,猜中時提示猜中,當輸入」quit」時結束程序。
和 前面的示例相似,在進行網絡程序開發時,首先須要分解一下功能的實現,以爲功能是在客戶端程序中實現仍是在服務器端程序中實現。區分的規則通常是:客戶端 程序實現接收用戶輸入等界面功能,並實現一些基礎的校驗下降服務器端的壓力,而將程序核心的邏輯以及數據存儲等功能放在服務器端進行實現。遵循該原則劃分 的客戶端和服務器端功能以下所示。
客戶端程序功能列表:
一、 接收用戶控制檯輸入
二、 判斷輸入內容是否合法
三、 按照協議格式發送數據
四、 根據服務器端的反饋給出相應提示
服務器端程序功能列表:
一、 接收客戶端發送數據
二、 按照協議格式解析數據
三、 判斷髮送過來的數字和隨機數字的關係
四、 根據判斷結果生產協議數據
五、 將生產的數據反饋給客戶端
在該示例中,實際使用的網絡命令也只有兩條,因此顯得協議的格式比較簡單。
其中客戶端程序協議格式以下:
一、 將用戶輸入的數字轉換爲字符串,而後轉換爲byte數組
二、 發送「quit」字符串表明退出
其中服務器端程序協議格式以下:
一、 反饋長度爲1個字節,數字0表明相等(猜中),1表明大了,2表明小了,其它數字表明錯誤。
實現該程序的代碼比較多,下面分爲客戶端程序實現和服務器端程序實現分別進行列舉。
客戶端程序實現代碼以下:
package guess;
import java.net.*;
import java.io.*;
/**
* 猜數字客戶端
*/
public class TCPClient {
public static void main(String[] args) {
Socket socket = null;
OutputStream os = null;
InputStream is = null;
BufferedReader br = null;
byte[] data = new byte[2];
try{
//創建鏈接
socket = new Socket(
"127.0.0.1",10001);
//發送數據
os= socket.getOutputStream();
//讀取反饋數據
is = socket.getInputStream();
//鍵盤輸入流
br = new BufferedReader(
new InputStreamReader(System.in));
//屢次輸入
while(true){
System.out.println("請輸入數字:");
//接收輸入
String s = br.readLine();
//結束條件
if(s.equals("quit")){
os.write("quit".getBytes());
break;
}
//校驗輸入是否合法
boolean b = true;
try{
Integer.parseInt(s);
}catch(Exception e){
b = false;
}
if(b){ //輸入合法
//發送數據
os.write(s.getBytes());
//接收反饋
is.read(data);
//判斷
switch(data[0]){
case 0:
System.out.println("相等!祝賀你!");
break;
case 1:
System.out.println("大了!");
break;
case 2:
System.out.println("小了!");
break;
default:
System.out.println("其它錯誤!");
}
//提示猜的次數
System.out.println("你已經猜了" + data[1] + "次!");
//判斷次數是否達到5次
if(data[1] >= 5){
System.out.println("你掛了!");
//給服務器端線程關閉的機會
os.write("quit".getBytes());
//結束客戶端程序
break;
}
}else{ //輸入錯誤
System.out.println("輸入錯誤!");
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//關閉鏈接
br.close();
is.close();
os.close();
socket.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
在該示例中,首先創建一個到IP地址爲127.0.0.1的端口爲10001的鏈接,而後進行各個流的初始化工做,將邏輯控制的代碼放入在一個while循環中,這樣能夠在客戶端屢次進行輸入。在循環內部,首先判斷用戶輸入的是否爲quit字符串,若是是則結束程序,若是輸入不是quit,則首先校驗輸入的是不是數字,若是不是數字則直接輸出「輸入錯誤!」並繼續接收用戶輸入,若是是數字則發送給服務器端,並根據服務器端的反饋顯示相應的提示信息。最後關閉流和鏈接,結束客戶端程序。
服務器端程序的實現仍是分爲服務器控制程序和邏輯線程,實現的代碼分別以下:
package guess;
import java.net.*;
/**
* TCP鏈接方式的服務器端
* 實現功能:接收客戶端的數據,判斷數字關係
*/
public class TCPServer {
public static void main(String[] args) {
try{
//監聽端口
ServerSocket ss = new ServerSocket(10001);
System.out.println("服務器已啓動:");
//邏輯處理
while(true){
//得到鏈接
Socket s = ss.accept();
//啓動線程處理
new LogicThread(s);
}
}catch(Exception e){
e.printStackTrace();
}
}
}
package guess;
import java.net.*;
import java.io.*;
import java.util.*;
/**
* 邏輯處理線程
*/
public class LogicThread extends Thread {
Socket s;
static Random r = new Random();
public LogicThread(Socket s){
this.s = s;
start(); //啓動線程
}
public void run(){
//生成一個[0,50]的隨機數
int randomNumber = Math.abs(r.nextInt() % 51);
//用戶猜的次數
int guessNumber = 0;
InputStream is = null;
OutputStream os = null;
byte[] data = new byte[2];
try{
//得到輸入流
is = s.getInputStream();
//得到輸出流
os = s.getOutputStream();
while(true){ //屢次處理
//讀取客戶端發送的數據
byte[] b = new byte[1024];
int n = is.read(b);
String send = new String(b,0,n);
//結束判別
if(send.equals("quit")){
break;
}
//解析、判斷
try{
int num = Integer.parseInt(send);
//處理
guessNumber++; //猜的次數增長1
data[1] = (byte)guessNumber;
//判斷
if(num > randomNumber){
data[0] = 1;
}else if(num < randomNumber){
data[0] = 2;
}else{
data[0] = 0;
//若是猜對
guessNumber = 0; //清零
randomNumber = Math.abs(r.nextInt() % 51);
}
//反饋給客戶端
os.write(data);
}catch(Exception e){ //數據格式錯誤
data[0] = 3;
data[1] = (byte)guessNumber;
os.write(data); //發送錯誤標識
break;
}
os.flush(); //強制發送
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
is.close();
os.close();
s.close();
}catch(Exception e){}
}
}
}
在 該示例中,服務器端控制部分和前面的示例中同樣。也是等待客戶端鏈接,若是有客戶端鏈接到達時,則啓動新的線程去處理客戶端鏈接。在邏輯線程中實現程序的 核心邏輯,首先當線程執行時生產一個隨機數字,而後根據客戶端發送過來的數據,判斷客戶端發送數字和隨機數字的關係,而後反饋相應的數字的值,並記憶客戶 端已經猜過的次數,當客戶端猜中之後清零猜過的次數,使得客戶端程序能夠繼續進行遊戲。