為了一步一步的掌握網(wǎng)絡(luò)編程,下面再研究網(wǎng)絡(luò)編程中的兩個(gè)基本問(wèn)題,通過(guò)解決這兩個(gè)問(wèn)題將對(duì)網(wǎng)絡(luò)編程的認(rèn)識(shí)深入一層。
1、如何復(fù)用Socket連接?
在前面的示例中,客戶端中建立了一次連接,只發(fā)送一次數(shù)據(jù)就關(guān)閉了,這就相當(dāng)于撥打電話時(shí),電話打通了只對(duì)話一次就關(guān)閉了,其實(shí)更加常用的應(yīng)該是撥通一次電話以后多次對(duì)話,這就是復(fù)用客戶端連接。
那
么如何實(shí)現(xiàn)建立一次連接,進(jìn)行多次數(shù)據(jù)交換呢?其實(shí)很簡(jiǎn)單,建立連接以后,將數(shù)據(jù)交換的邏輯寫到一個(gè)循環(huán)中就可以了。這樣只要循環(huán)不結(jié)束則連接就不會(huì)被關(guān)
閉。按照這種思路,可以改造一下上面的代碼,讓該程序可以在建立連接一次以后,發(fā)送三次數(shù)據(jù),當(dāng)然這里的次數(shù)也可以是多次,示例代碼如下:
package tcp;
import java.io.*;
import java.net.*;
/**
* 復(fù)用連接的Socket客戶端
* 功能為:發(fā)送字符串“Hello”到服務(wù)器端,并打印出服務(wù)器端的反饋
*/
public class MulSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
//服務(wù)器端IP地址
String serverIP = "127.0.0.1";
//服務(wù)器端端口號(hào)
int port = 10000;
//發(fā)送內(nèi)容
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++){
//發(fā)送數(shù)據(jù)
os.write(data[i].getBytes());
//接收數(shù)據(jù)
int n = is.read(b);
//輸出反饋數(shù)據(jù)
System.out.println("服務(wù)器反饋:" + new String(b,0,n));
}
} catch (Exception e) {
e.printStackTrace(); //打印異常信息
}finally{
try {
//關(guān)閉流和連接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
該示例程序和前面的代碼相比,將數(shù)據(jù)交換部分的邏輯寫在一個(gè)for循環(huán)的內(nèi)容,這樣就可以建立一次連接,依次將data數(shù)組中的數(shù)據(jù)按照順序發(fā)送給服務(wù)器端了。
如果還是使用前面示例代碼中的服務(wù)器端程序運(yùn)行該程序,則該程序的結(jié)果是:
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)
服務(wù)器反饋:First
顯然,客戶端在實(shí)際運(yùn)行時(shí)出現(xiàn)了異常,出現(xiàn)異常的原因是什么呢?如果仔細(xì)閱讀前面的代碼,應(yīng)該還記得前面示例代碼中的服務(wù)器端是對(duì)話一次數(shù)據(jù)以后就關(guān)閉了連接,如果服務(wù)器端程序關(guān)閉了,客戶端繼續(xù)發(fā)送數(shù)據(jù)肯定會(huì)出現(xiàn)異常,這就是出現(xiàn)該問(wèn)題的原因。
按照客戶端實(shí)現(xiàn)的邏輯,也可以復(fù)用服務(wù)器端的連接,實(shí)現(xiàn)的原理也是將服務(wù)器端的數(shù)據(jù)交換邏輯寫在循環(huán)中即可,按照該種思路改造以后的服務(wù)器端代碼為:
package tcp;
import java.io.*;
import java.net.*;
/**
* 復(fù)用連接的echo服務(wù)器
* 功能:將客戶端發(fā)送的內(nèi)容反饋給客戶端
*/
public class MulSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//監(jiān)聽(tīng)端口號(hào)
int port = 10000;
try {
//建立連接
serverSocket = new ServerSocket(port);
System.out.println("服務(wù)器已啟動(dòng):");
//獲得連接
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("客戶端發(fā)送內(nèi)容為:" + new String(b,0,n));
//向客戶端發(fā)送反饋內(nèi)容
os.write(b, 0, n);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關(guān)閉流和連接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,也將數(shù)據(jù)發(fā)送和接收的邏輯寫在了一個(gè)for循環(huán)內(nèi)部,只是在實(shí)現(xiàn)時(shí)硬性的將循環(huán)次數(shù)規(guī)定成了3次,這樣代碼雖然比較簡(jiǎn)單,但是通用性比較差。
以該服務(wù)器端代碼實(shí)現(xiàn)為基礎(chǔ)運(yùn)行前面的客戶端程序時(shí),客戶端的輸出為:
服務(wù)器反饋:First
服務(wù)器反饋:Second
服務(wù)器反饋:Third
服務(wù)器端程序的輸出結(jié)果為:
服務(wù)器已啟動(dòng):
客戶端發(fā)送內(nèi)容為:First
客戶端發(fā)送內(nèi)容為:Second
客戶端發(fā)送內(nèi)容為:Third
在該程序中,比較明顯的體現(xiàn)出了“請(qǐng)求-響應(yīng)”模型,也就是在客戶端發(fā)起連接以后,首先發(fā)送字符串“First”給服務(wù)器端,服務(wù)器端輸出客戶端發(fā)送的內(nèi)容“First”,然后將客戶端發(fā)送的內(nèi)容再反饋給客戶端,這樣客戶端也輸出服務(wù)器反饋“First”,這樣就完成了客戶端和服務(wù)器端的一次對(duì)話,緊接著客戶端發(fā)送“Second”給服務(wù)器端,服務(wù)端輸出“Second”,然后將“Second”再反饋給客戶端,客戶端再輸出“Second”,從而完成第二次會(huì)話,第三次會(huì)話的過(guò)程和這個(gè)一樣。在這個(gè)過(guò)程中,每次都是客戶端程序首先發(fā)送數(shù)據(jù)給服務(wù)器端,服務(wù)器接收數(shù)據(jù)以后,將結(jié)果反饋給客戶端,客戶端接收到服務(wù)器端的反饋,從而完成一次通訊過(guò)程。
在該示例中,雖然解決了多次發(fā)送的問(wèn)題,但是客戶端和服務(wù)器端的次數(shù)控制還不夠靈活,如果客戶端的次數(shù)不固定怎么辦呢?是否可以使用某個(gè)特殊的字符串,例如quit,表示客戶端退出呢,這就涉及到網(wǎng)絡(luò)協(xié)議的內(nèi)容了,會(huì)在后續(xù)的網(wǎng)絡(luò)應(yīng)用示例部分詳細(xì)介紹。下面開(kāi)始介紹另外一個(gè)網(wǎng)絡(luò)編程的突出問(wèn)題。
2、如何使服務(wù)器端支持多個(gè)客戶端同時(shí)工作?
前面介紹的服務(wù)器端程序,只是實(shí)現(xiàn)了概念上的服務(wù)器端,離實(shí)際的服務(wù)器端程序結(jié)構(gòu)距離還很遙遠(yuǎn),如果需要讓服務(wù)器端能夠?qū)嶋H使用,那么最需要解決的問(wèn)題就是——如何支持多個(gè)客戶端同時(shí)工作。
一個(gè)服務(wù)器端一般都需要同時(shí)為多個(gè)客戶端提供通訊,如果需要同時(shí)支持多個(gè)客戶端,則必須使用前面介紹的線程的概念。簡(jiǎn)單來(lái)說(shuō),也就是當(dāng)服務(wù)器端接收到一個(gè)連接時(shí),啟動(dòng)一個(gè)專門的線程處理和該客戶端的通訊。
按照這個(gè)思路改寫的服務(wù)端示例程序?qū)⒂蓛蓚€(gè)部分組成,MulThreadSocketServer類實(shí)現(xiàn)服務(wù)器端控制,實(shí)現(xiàn)接收客戶端連接,然后開(kāi)啟專門的邏輯線程處理該連接,LogicThread類實(shí)現(xiàn)對(duì)于一個(gè)客戶端連接的邏輯處理,將處理的邏輯放置在該類的run方法中。該示例的代碼實(shí)現(xiàn)為:
package tcp;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 支持多客戶端的服務(wù)器端實(shí)現(xiàn)
*/
public class MulThreadSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
//監(jiān)聽(tīng)端口號(hào)
int port = 10000;
try {
//建立連接
serverSocket = new ServerSocket(port);
System.out.println("服務(wù)器已啟動(dòng):");
while(true){
//獲得連接
socket = serverSocket.accept();
//啟動(dòng)線程
new LogicThread(socket);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關(guān)閉連接
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,實(shí)現(xiàn)了一個(gè)while形式的死循環(huán),由于accept方法是阻塞方法,所以當(dāng)客戶端連接未到達(dá)時(shí),將阻塞該程序的執(zhí)行,當(dāng)客戶端到達(dá)時(shí)接收該連接,并啟動(dòng)一個(gè)新的LogicThread線程處理該連接,然后按照循環(huán)的執(zhí)行流程,繼續(xù)等待下一個(gè)客戶端連接。這樣當(dāng)任何一個(gè)客戶端連接到達(dá)時(shí),都開(kāi)啟一個(gè)專門的線程處理,通過(guò)多個(gè)線程支持多個(gè)客戶端同時(shí)處理。
下面再看一下LogicThread線程類的源代碼實(shí)現(xiàn):
package tcp;
import java.io.*;
import java.net.*;
/**
* 服務(wù)器端邏輯線程
*/
public class LogicThread extends Thread {
Socket socket;
InputStream is;
OutputStream os;
public LogicThread(Socket socket){
this.socket = socket;
start(); //啟動(dòng)線程
}
public void run(){
byte[] b = new byte[1024];
try{
//初始化流
os = socket.getOutputStream();
is = socket.getInputStream();
for(int i = 0;i < 3;i++){
//讀取數(shù)據(jù)
int n = is.read(b);
//邏輯處理
byte[] response = logic(b,0,n);
//反饋數(shù)據(jù)
os.write(response);
}
}catch(Exception e){
e.printStackTrace();
}finally{
close();
}
}
/**
* 關(guān)閉流和連接
*/
private void close(){
try{
//關(guān)閉流和連接
os.close();
is.close();
socket.close();
}catch(Exception e){}
}
/**
* 邏輯處理方法,實(shí)現(xiàn)echo邏輯
* @param b 客戶端發(fā)送數(shù)據(jù)緩沖區(qū)
* @param off 起始下標(biāo)
* @param len 有效數(shù)據(jù)長(zhǎng)度
* @return
*/
private byte[] logic(byte[] b,int off,int len){
byte[] response = new byte[len];
//將有效數(shù)據(jù)拷貝到數(shù)組response中
System.arraycopy(b, 0, response, 0, len);
return response;
}
}
在該示例代碼中,每次使用一個(gè)連接對(duì)象構(gòu)造該線程,該連接對(duì)象就是該線程需要處理的連接,在線程構(gòu)造完成以后,該線程就被啟動(dòng)起來(lái)了,然后在run方法內(nèi)部對(duì)客戶端連接進(jìn)行處理,數(shù)據(jù)交換的邏輯和前面的示例代碼一致,只是這里將接收到客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù)并進(jìn)行處理的邏輯封裝成了logic方法,按照前面介紹的IO編程的內(nèi)容,客戶端發(fā)送過(guò)來(lái)的內(nèi)容存儲(chǔ)在數(shù)組b的起始下標(biāo)為0,長(zhǎng)度為n個(gè)中,這些數(shù)據(jù)是客戶端發(fā)送過(guò)來(lái)的有效數(shù)據(jù),將有效的數(shù)據(jù)傳遞給logic方法,logic方法實(shí)現(xiàn)的是echo服務(wù)的邏輯,也就是將客戶端發(fā)送的有效數(shù)據(jù)形成以后新的response數(shù)組,并作為返回值反饋。
在線程中將logic方法的返回值反饋給客戶端,這樣就完成了服務(wù)器端的邏輯處理模擬,其他的實(shí)現(xiàn)和前面的介紹類似,這里就不在重復(fù)了。
這里的示例還只是基礎(chǔ)的服務(wù)器端實(shí)現(xiàn),在實(shí)際的服務(wù)器端實(shí)現(xiàn)中,由于硬件和端口數(shù)的限制,所以不能無(wú)限制的創(chuàng)建線程對(duì)象,而且頻繁的創(chuàng)建線程對(duì)象效率也比較低,所以程序中都實(shí)現(xiàn)了線程池來(lái)提高程序的執(zhí)行效率。
這里簡(jiǎn)單介紹一下線程池的概念,線程池(Thread pool)是池技術(shù)的一種,就是在程序啟動(dòng)時(shí)首先把需要個(gè)數(shù)的線程對(duì)象創(chuàng)建好,例如創(chuàng)建5000個(gè)線程對(duì)象,然后當(dāng)客戶端連接到達(dá)時(shí)從池中取出一個(gè)已經(jīng)創(chuàng)建完成的線程對(duì)象使用即可。當(dāng)客戶端連接關(guān)閉以后,將該線程對(duì)象重新放入到線程池中供其它的客戶端重復(fù)使用,這樣可以提高程序的執(zhí)行速度,優(yōu)化程序?qū)τ趦?nèi)存的占用等。
關(guān)于基礎(chǔ)的TCP方式的網(wǎng)絡(luò)編程就介紹這么多,下面介紹UDP方式的網(wǎng)絡(luò)編程在Java語(yǔ)言中的實(shí)現(xiàn)。