為了一步一步的掌握網絡編程,下面再研究網絡編程中的兩個基本問題,通過解決這兩個問題將對網絡編程的認識深入一層。
1、如何復用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,表示客戶端退出呢,這就涉及到網絡協議的內容了,會在後續的網絡應用示例部分詳細介紹。下面開始介紹另外一個網絡編程的突出問題。
2、如何使服務器端支持多個客戶端同時工作?
前面介紹的服務器端程序,只是實現了概念上的服務器端,離實際的服務器端程序結構距離還很遙遠,如果需要讓服務器端能夠實際使用,那麼最需要解決的問題就是——如何支持多個客戶端同時工作。
一個服務器端一般都需要同時為多個客戶端提供通訊,如果需要同時支持多個客戶端,則必須使用前面介紹的線程的概念。簡單來說,也就是當服務器端接收到一個連接時,啟動一個專門的線程處理和該客戶端的通訊。
按照這個思路改寫的服務端示例程序將由兩個部分組成,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語言中的實現。