8.3 基于Socket(套接字)的低層次Java網(wǎng)絡(luò)編程
8.3.1 Socket通訊
網(wǎng)絡(luò)上的兩個(gè)程序通過一個(gè)雙向的通訊連接實(shí)現(xiàn)數(shù)據(jù)的交換,這個(gè)雙向鏈路的一端稱為一個(gè)Socket。Socket通常用來實(shí)現(xiàn)客戶方和服務(wù)方的連接。Socket是TCP/IP協(xié)議的一個(gè)十分流行的編程界面,一個(gè)Socket由一個(gè)IP地址和一個(gè)端口號唯一確定。
在傳統(tǒng)的UNIX環(huán)境下可以操作TCP/IP協(xié)議的接口不止Socket一個(gè),Socket所支持的協(xié)議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯(lián)系的。在Java環(huán)境下,Socket編程主要是指基于TCP/IP協(xié)議的網(wǎng)絡(luò)編程。
說Socket編程是低層次網(wǎng)絡(luò)編程并不等于它功能不強(qiáng)大,恰恰相反,正因?yàn)閷哟蔚停琒ocket編程比基于URL的網(wǎng)絡(luò)編程提供了更強(qiáng)大的功能和更靈活的控制,但是卻要更復(fù)雜一些。由于Java本身的特殊性,Socket編程在Java中可能已經(jīng)是層次最低的網(wǎng)絡(luò)編程接口,在Java中要直接操作協(xié)議中更低的層次,需要使用Java的本地方法調(diào)用(JNI),在這里就不予討論了。
8.3.2 Socket通訊的一般過
前面已經(jīng)提到Socket通常用來實(shí)現(xiàn)C/S結(jié)構(gòu)。
使用Socket進(jìn)行Client/Server程序設(shè)計(jì)的一般連接過程是這樣的:Server端Listen(監(jiān)聽)某個(gè)端口是否有連接請求, Client端向Server端發(fā)出Connect(連接)請求,Server端向Client端發(fā)回Accept(接受)消息。一個(gè)連接就建立起來了。 Server端和Client端都可以通過Send,Write等方法與對方通信。
對于一個(gè)功能齊全的Socket,都要包含以下基本結(jié)構(gòu),其工作過程包含以下四個(gè)基本的步驟:
?。?) 創(chuàng)建Socket;
?。?) 打開連接到Socket的輸入/出流;
?。?) 按照一定的協(xié)議對Socket進(jìn)行讀/寫操作;
(4) 關(guān)閉Socket.
第三步是程序員用來調(diào)用Socket和實(shí)現(xiàn)程序功能的關(guān)鍵步驟,其他三步在各種程序中基本相同。
以上4個(gè)步驟是針對TCP傳輸而言的,使用UDP進(jìn)行傳輸時(shí)略有不同,在后面會有具體講解。
8.3.3 創(chuàng)建Socket
java在包java.net中提供了兩個(gè)類Socket和ServerSocket,分別用來表示雙向連接的客戶端和服務(wù)端。這是兩個(gè)封裝得非常好的類,使用很方便。其構(gòu)造方法如下:
Socket(InetAddress address, int port);
Socket(InetAddress address, int port, boolean stream);
Socket(String host, int prot);
Socket(String host, int prot, boolean stream);
Socket(SocketImpl impl)
Socket(String host, int port, InetAddress localAddr, int localPort)
Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
ServerSocket(int port);
ServerSocket(int port, int backlog);
ServerSocket(int port, int backlog, InetAddress bindAddr)
其中address、host和port分別是雙向連接中另一方的IP地址、主機(jī)名和端口號,stream指明socket是流socket還是數(shù)據(jù)報(bào) socket,localPort表示本地主機(jī)的端口號,localAddr和bindAddr是本地機(jī)器的地址(ServerSocket的主機(jī)地址),impl是socket的父類,既可以用來創(chuàng)建serverSocket又可以用來創(chuàng)建Socket。count則表示服務(wù)端所能支持的最大連接數(shù)。例如:
Socket client = new Socket("127.0.01.", 80);
ServerSocket server = new ServerSocket(80);
注意,在選擇端口時(shí),必須小心。每一個(gè)端口提供一種特定的服務(wù),只有給出正確的端口,才能獲得相應(yīng)的服務(wù)。0~1023的端口號為系統(tǒng)所保留,例如 http服務(wù)的端口號為80,telnet服務(wù)的端口號為21,ftp服務(wù)的端口號為23, 所以我們在選擇端口號時(shí),最好選擇一個(gè)大于1023的數(shù)以防止發(fā)生沖突。
在創(chuàng)建socket時(shí)如果發(fā)生錯(cuò)誤,將產(chǎn)生IOException,在程序中必須對之作出處理。所以在創(chuàng)建Socket或ServerSocket是必須捕獲或拋出例外。
8.3.4 客戶端的Socket
下面是一個(gè)典型的創(chuàng)建客戶端Socket的過程。
try{
Socket socket=new Socket("127.0.0.1",4700);
//127.0.0.1是TCP/IP協(xié)議中默認(rèn)的本機(jī)地址
}catch(IOException e){
System.out.println("Error:"+e);
}
這是最簡單的在客戶端創(chuàng)建一個(gè)Socket的一個(gè)小程序段,也是使用Socket進(jìn)行網(wǎng)絡(luò)通訊的第一步,程序相當(dāng)簡單,在這里不作過多解釋了。在后面的程序中會用到該小程序段。
8.3.5 服務(wù)器端的ServerSocket
下面是一個(gè)典型的創(chuàng)建Server端ServerSocket的過程。
ServerSocket server=null;
try {
server=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(IOException e){
System.out.println("can not listen to :"+e);
}
Socket socket=null;
try {
socket=server.accept();
//accept()是一個(gè)阻塞的方法,一旦有客戶請求,它就會返回一個(gè)Socket對象用于同客戶進(jìn)行交互
}catch(IOException e){
System.out.println("Error:"+e);
}
以上的程序是Server的典型工作模式,只不過在這里Server只能接收一個(gè)請求,接受完后Server就退出了。實(shí)際的應(yīng)用中總是讓它不停的循環(huán)接收,一旦有客戶請求,Server總是會創(chuàng)建一個(gè)服務(wù)線程來服務(wù)新來的客戶,而自己繼續(xù)監(jiān)聽。程序中accept()是一個(gè)阻塞函數(shù),所謂阻塞性方法就是說該方法被調(diào)用后,將等待客戶的請求,直到有一個(gè)客戶啟動(dòng)并請求連接到相同的端口,然后accept()返回一個(gè)對應(yīng)于客戶的socket。這時(shí),客戶方和服務(wù)方都建立了用于通信的socket,接下來就是由各個(gè)socket分別打開各自的輸入/輸出流。
8.3.6 打開輸入/出流
類Socket提供了方法getInputStream ()和getOutStream()來得到對應(yīng)的輸入/輸出流以進(jìn)行讀/寫操作,這兩個(gè)方法分別返回InputStream和OutputSteam類對象。為了便于讀/寫數(shù)據(jù),我們可以在返回的輸入/輸出流對象上建立過濾流,如DataInputStream、DataOutputStream或 PrintStream類對象,對于文本方式流對象,可以采用InputStreamReader和OutputStreamWriter、 PrintWirter等處理。
例如:
PrintStream os=new PrintStream(new BufferedOutputStreem(socket.getOutputStream()));
DataInputStream is=new DataInputStream(socket.getInputStream());
PrintWriter out=new PrintWriter(socket.getOutStream(),true);
BufferedReader in=new ButfferedReader(new InputSteramReader(Socket.getInputStream()));
輸入輸出流是網(wǎng)絡(luò)編程的實(shí)質(zhì)性部分,具體如何構(gòu)造所需要的過濾流,要根據(jù)需要而定,能否運(yùn)用自如主要看讀者對Java中輸入輸出部分掌握如何。
8.3.7 關(guān)閉Socket
每一個(gè)Socket存在時(shí),都將占用一定的資源,在Socket對象使用完畢時(shí),要其關(guān)閉。關(guān)閉Socket可以調(diào)用Socket的Close()方法。在關(guān)閉Socket之前,應(yīng)將與Socket相關(guān)的所有的輸入/輸出流全部關(guān)閉,以釋放所有的資源。而且要注意關(guān)閉的順序,與Socket相關(guān)的所有的輸入/輸出該首先關(guān)閉,然后再關(guān)閉Socket。
os.close();
is.close();
socket.close();
盡管Java有自動(dòng)回收機(jī)制,網(wǎng)絡(luò)資源最終是會被釋放的。但是為了有效的利用資源,建議讀者按照合理的順序主動(dòng)釋放資源。
8.3.8 簡單的Client/Server程序設(shè)計(jì)
下面我們給出一個(gè)用Socket實(shí)現(xiàn)的客戶和服務(wù)器交互的典型的C/S結(jié)構(gòu)的演示程序,讀者通過仔細(xì)閱讀該程序,會對前面所討論的各個(gè)概念有更深刻的認(rèn)識。程序的意義請參考注釋。
1. 客戶端程序
import java.io.*;
import java.net.*;
public class TalkClient {
public static void main(String args[]) {
try{
Socket socket=new Socket("127.0.0.1",4700);
//向本機(jī)的4700端口發(fā)出客戶請求
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
PrintWriter os=new PrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
String readline;
readline=sin.readLine(); //從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
while(!readline.equals("bye")){
//若從標(biāo)準(zhǔn)輸入讀入的字符串為 "bye"則停止循環(huán)
os.println(readline);
//將從系統(tǒng)標(biāo)準(zhǔn)輸入讀入的字符串輸出到Server
os.flush();
//刷新輸出流,使Server馬上收到該字符串
System.out.println("Client:"+readline);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印讀入的字符串
System.out.println("Server:"+is.readLine());
//從Server讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
readline=sin.readLine(); //從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
}catch(Exception e) {
System.out.println("Error"+e); //出錯(cuò),則打印出錯(cuò)信息
}
}
} 2. 服務(wù)器端程序
import java.io.*;
import java.net.*;
import java.applet.Applet;
public class TalkServer{
public static void main(String args[]) {
try{
ServerSocket server=null;
try{
server=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(Exception e) {
System.out.println("can not listen to:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
Socket socket=null;
try{
socket=server.accept();
//使用accept()阻塞等待客戶請求,有客戶
//請求到來則產(chǎn)生一個(gè)Socket對象,并繼續(xù)執(zhí)行
}catch(Exception e) {
System.out.println("Error."+e);
//出錯(cuò),打印出錯(cuò)信息
}
String line;
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
PrintWriter os=newPrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
System.out.println("Client:"+is.readLine());
//在標(biāo)準(zhǔn)輸出上打印從客戶端讀入的字符串
line=sin.readLine();
//從標(biāo)準(zhǔn)輸入讀入一字符串
while(!line.equals("bye")){
//如果該字符串為 "bye",則停止循環(huán)
os.println(line);
//向客戶端輸出該字符串
os.flush();
//刷新輸出流,使Client馬上收到該字符串
System.out.println("Server:"+line);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印讀入的字符串
System.out.println("Client:"+is.readLine());
//從Client讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
line=sin.readLine();
//從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
server.close(); //關(guān)閉ServerSocket
}catch(Exception e){
System.out.println("Error:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
}
}
從上面的兩個(gè)程序中我們可以看到,socket四個(gè)步驟的使用過程。讀者可以分別將Socket使用的四個(gè)步驟的對應(yīng)程序段選擇出來,這樣便于讀者對socket的使用有進(jìn)一步的了解。
讀者可以在單機(jī)上試驗(yàn)該程序,最好是能在真正的網(wǎng)絡(luò)環(huán)境下試驗(yàn)該程序,這樣更容易分辨輸出的內(nèi)容和客戶機(jī),服務(wù)器的對應(yīng)關(guān)系。同時(shí)也可以修改該程序,提供更為強(qiáng)大的功能,或更加滿足讀者的意圖。
8.3.9 支持多客戶的client/server程序設(shè)計(jì)
前面提供的Client/Server程序只能實(shí)現(xiàn)Server和一個(gè)客戶的對話。在實(shí)際應(yīng)用中,往往是在服務(wù)器上運(yùn)行一個(gè)永久的程序,它可以接收來自其他多個(gè)客戶端的請求,提供相應(yīng)的服務(wù)。為了實(shí)現(xiàn)在服務(wù)器方給多個(gè)客戶提供服務(wù)的功能,需要對上面的程序進(jìn)行改造,利用多線程實(shí)現(xiàn)多客戶機(jī)制。服務(wù)器總是在指定的端口上監(jiān)聽是否有客戶請求,一旦監(jiān)聽到客戶請求,服務(wù)器就會啟動(dòng)一個(gè)專門的服務(wù)線程來響應(yīng)該客戶的請求,而服務(wù)器本身在啟動(dòng)完線程之后馬上又進(jìn)入監(jiān)聽狀態(tài),等待下一個(gè)客戶的到來。
客戶端的程序和上面程序是完全一樣的,讀者如果仔細(xì)閱讀過上面的程序,可以跳過不讀,把主要精力集中在Server端的程序上。
2. 服務(wù)器端程序: MultiTalkServer.java
import java.io.*;
import java.net.*;
import ServerThread;
public class MultiTalkServer{
static int clientnum=0; //靜態(tài)成員變量,記錄當(dāng)前客戶的個(gè)數(shù)
public static void main(String args[]) throws IOException {
ServerSocket serverSocket=null;
boolean listening=true;
try{
serverSocket=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(IOException e) {
System.out.println("Could not listen on port:4700.");
//出錯(cuò),打印出錯(cuò)信息
System.exit(-1); //退出
}
while(listening){ //永遠(yuǎn)循環(huán)監(jiān)聽
new ServerThread(serverSocket.accept(),clientnum).start();
//監(jiān)聽到客戶請求,根據(jù)得到的Socket對象和
客戶計(jì)數(shù)創(chuàng)建服務(wù)線程,并啟動(dòng)之
clientnum++; //增加客戶計(jì)數(shù)
}
serverSocket.close(); //關(guān)閉ServerSocket
}
}
3. 程序ServerThread.java
import java.io.*;
import java.net.*;
public class ServerThread extends Thread{
Socket socket=null; //保存與本線程相關(guān)的Socket對象
int clientnum; //保存本進(jìn)程的客戶計(jì)數(shù)
public ServerThread(Socket socket,int num) { //構(gòu)造函數(shù)
this.socket=socket; //初始化socket變量
clientnum=num+1; //初始化clientnum變量
}
public void run() { //線程主體
try{
String line;
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
PrintWriter os=newPrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
System.out.println("Client:"+ clientnum +is.readLine());
//在標(biāo)準(zhǔn)輸出上打印從客戶端讀入的字符串
line=sin.readLine();
//從標(biāo)準(zhǔn)輸入讀入一字符串
while(!line.equals("bye")){
//如果該字符串為 "bye",則停止循環(huán)
os.println(line);
//向客戶端輸出該字符串
os.flush();
//刷新輸出流,使Client馬上收到該字符串
System.out.println("Server:"+line);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印該字符串
System.out.println("Client:"+ clientnum +is.readLine());
//從Client讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
line=sin.readLine();
//從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
server.close(); //關(guān)閉ServerSocket
}catch(Exception e){
System.out.println("Error:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
}
}
通過以上的學(xué)習(xí),讀者應(yīng)該對Java的面向流的網(wǎng)絡(luò)編程有了一個(gè)比較全面的認(rèn)識,這些都是基于TCP的應(yīng)用,后面我們將介紹基于UDP的Socket編程
8.3.1 Socket通訊
網(wǎng)絡(luò)上的兩個(gè)程序通過一個(gè)雙向的通訊連接實(shí)現(xiàn)數(shù)據(jù)的交換,這個(gè)雙向鏈路的一端稱為一個(gè)Socket。Socket通常用來實(shí)現(xiàn)客戶方和服務(wù)方的連接。Socket是TCP/IP協(xié)議的一個(gè)十分流行的編程界面,一個(gè)Socket由一個(gè)IP地址和一個(gè)端口號唯一確定。
在傳統(tǒng)的UNIX環(huán)境下可以操作TCP/IP協(xié)議的接口不止Socket一個(gè),Socket所支持的協(xié)議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯(lián)系的。在Java環(huán)境下,Socket編程主要是指基于TCP/IP協(xié)議的網(wǎng)絡(luò)編程。
說Socket編程是低層次網(wǎng)絡(luò)編程并不等于它功能不強(qiáng)大,恰恰相反,正因?yàn)閷哟蔚停琒ocket編程比基于URL的網(wǎng)絡(luò)編程提供了更強(qiáng)大的功能和更靈活的控制,但是卻要更復(fù)雜一些。由于Java本身的特殊性,Socket編程在Java中可能已經(jīng)是層次最低的網(wǎng)絡(luò)編程接口,在Java中要直接操作協(xié)議中更低的層次,需要使用Java的本地方法調(diào)用(JNI),在這里就不予討論了。
8.3.2 Socket通訊的一般過
前面已經(jīng)提到Socket通常用來實(shí)現(xiàn)C/S結(jié)構(gòu)。
使用Socket進(jìn)行Client/Server程序設(shè)計(jì)的一般連接過程是這樣的:Server端Listen(監(jiān)聽)某個(gè)端口是否有連接請求, Client端向Server端發(fā)出Connect(連接)請求,Server端向Client端發(fā)回Accept(接受)消息。一個(gè)連接就建立起來了。 Server端和Client端都可以通過Send,Write等方法與對方通信。
對于一個(gè)功能齊全的Socket,都要包含以下基本結(jié)構(gòu),其工作過程包含以下四個(gè)基本的步驟:
?。?) 創(chuàng)建Socket;
?。?) 打開連接到Socket的輸入/出流;
?。?) 按照一定的協(xié)議對Socket進(jìn)行讀/寫操作;
(4) 關(guān)閉Socket.
第三步是程序員用來調(diào)用Socket和實(shí)現(xiàn)程序功能的關(guān)鍵步驟,其他三步在各種程序中基本相同。
以上4個(gè)步驟是針對TCP傳輸而言的,使用UDP進(jìn)行傳輸時(shí)略有不同,在后面會有具體講解。
8.3.3 創(chuàng)建Socket
java在包java.net中提供了兩個(gè)類Socket和ServerSocket,分別用來表示雙向連接的客戶端和服務(wù)端。這是兩個(gè)封裝得非常好的類,使用很方便。其構(gòu)造方法如下:
Socket(InetAddress address, int port);
Socket(InetAddress address, int port, boolean stream);
Socket(String host, int prot);
Socket(String host, int prot, boolean stream);
Socket(SocketImpl impl)
Socket(String host, int port, InetAddress localAddr, int localPort)
Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
ServerSocket(int port);
ServerSocket(int port, int backlog);
ServerSocket(int port, int backlog, InetAddress bindAddr)
其中address、host和port分別是雙向連接中另一方的IP地址、主機(jī)名和端口號,stream指明socket是流socket還是數(shù)據(jù)報(bào) socket,localPort表示本地主機(jī)的端口號,localAddr和bindAddr是本地機(jī)器的地址(ServerSocket的主機(jī)地址),impl是socket的父類,既可以用來創(chuàng)建serverSocket又可以用來創(chuàng)建Socket。count則表示服務(wù)端所能支持的最大連接數(shù)。例如:
Socket client = new Socket("127.0.01.", 80);
ServerSocket server = new ServerSocket(80);
注意,在選擇端口時(shí),必須小心。每一個(gè)端口提供一種特定的服務(wù),只有給出正確的端口,才能獲得相應(yīng)的服務(wù)。0~1023的端口號為系統(tǒng)所保留,例如 http服務(wù)的端口號為80,telnet服務(wù)的端口號為21,ftp服務(wù)的端口號為23, 所以我們在選擇端口號時(shí),最好選擇一個(gè)大于1023的數(shù)以防止發(fā)生沖突。
在創(chuàng)建socket時(shí)如果發(fā)生錯(cuò)誤,將產(chǎn)生IOException,在程序中必須對之作出處理。所以在創(chuàng)建Socket或ServerSocket是必須捕獲或拋出例外。
8.3.4 客戶端的Socket
下面是一個(gè)典型的創(chuàng)建客戶端Socket的過程。
try{
Socket socket=new Socket("127.0.0.1",4700);
//127.0.0.1是TCP/IP協(xié)議中默認(rèn)的本機(jī)地址
}catch(IOException e){
System.out.println("Error:"+e);
}
這是最簡單的在客戶端創(chuàng)建一個(gè)Socket的一個(gè)小程序段,也是使用Socket進(jìn)行網(wǎng)絡(luò)通訊的第一步,程序相當(dāng)簡單,在這里不作過多解釋了。在后面的程序中會用到該小程序段。
8.3.5 服務(wù)器端的ServerSocket
下面是一個(gè)典型的創(chuàng)建Server端ServerSocket的過程。
ServerSocket server=null;
try {
server=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(IOException e){
System.out.println("can not listen to :"+e);
}
Socket socket=null;
try {
socket=server.accept();
//accept()是一個(gè)阻塞的方法,一旦有客戶請求,它就會返回一個(gè)Socket對象用于同客戶進(jìn)行交互
}catch(IOException e){
System.out.println("Error:"+e);
}
以上的程序是Server的典型工作模式,只不過在這里Server只能接收一個(gè)請求,接受完后Server就退出了。實(shí)際的應(yīng)用中總是讓它不停的循環(huán)接收,一旦有客戶請求,Server總是會創(chuàng)建一個(gè)服務(wù)線程來服務(wù)新來的客戶,而自己繼續(xù)監(jiān)聽。程序中accept()是一個(gè)阻塞函數(shù),所謂阻塞性方法就是說該方法被調(diào)用后,將等待客戶的請求,直到有一個(gè)客戶啟動(dòng)并請求連接到相同的端口,然后accept()返回一個(gè)對應(yīng)于客戶的socket。這時(shí),客戶方和服務(wù)方都建立了用于通信的socket,接下來就是由各個(gè)socket分別打開各自的輸入/輸出流。
8.3.6 打開輸入/出流
類Socket提供了方法getInputStream ()和getOutStream()來得到對應(yīng)的輸入/輸出流以進(jìn)行讀/寫操作,這兩個(gè)方法分別返回InputStream和OutputSteam類對象。為了便于讀/寫數(shù)據(jù),我們可以在返回的輸入/輸出流對象上建立過濾流,如DataInputStream、DataOutputStream或 PrintStream類對象,對于文本方式流對象,可以采用InputStreamReader和OutputStreamWriter、 PrintWirter等處理。
例如:
PrintStream os=new PrintStream(new BufferedOutputStreem(socket.getOutputStream()));
DataInputStream is=new DataInputStream(socket.getInputStream());
PrintWriter out=new PrintWriter(socket.getOutStream(),true);
BufferedReader in=new ButfferedReader(new InputSteramReader(Socket.getInputStream()));
輸入輸出流是網(wǎng)絡(luò)編程的實(shí)質(zhì)性部分,具體如何構(gòu)造所需要的過濾流,要根據(jù)需要而定,能否運(yùn)用自如主要看讀者對Java中輸入輸出部分掌握如何。
8.3.7 關(guān)閉Socket
每一個(gè)Socket存在時(shí),都將占用一定的資源,在Socket對象使用完畢時(shí),要其關(guān)閉。關(guān)閉Socket可以調(diào)用Socket的Close()方法。在關(guān)閉Socket之前,應(yīng)將與Socket相關(guān)的所有的輸入/輸出流全部關(guān)閉,以釋放所有的資源。而且要注意關(guān)閉的順序,與Socket相關(guān)的所有的輸入/輸出該首先關(guān)閉,然后再關(guān)閉Socket。
os.close();
is.close();
socket.close();
盡管Java有自動(dòng)回收機(jī)制,網(wǎng)絡(luò)資源最終是會被釋放的。但是為了有效的利用資源,建議讀者按照合理的順序主動(dòng)釋放資源。
8.3.8 簡單的Client/Server程序設(shè)計(jì)
下面我們給出一個(gè)用Socket實(shí)現(xiàn)的客戶和服務(wù)器交互的典型的C/S結(jié)構(gòu)的演示程序,讀者通過仔細(xì)閱讀該程序,會對前面所討論的各個(gè)概念有更深刻的認(rèn)識。程序的意義請參考注釋。
1. 客戶端程序
import java.io.*;
import java.net.*;
public class TalkClient {
public static void main(String args[]) {
try{
Socket socket=new Socket("127.0.0.1",4700);
//向本機(jī)的4700端口發(fā)出客戶請求
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
PrintWriter os=new PrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
String readline;
readline=sin.readLine(); //從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
while(!readline.equals("bye")){
//若從標(biāo)準(zhǔn)輸入讀入的字符串為 "bye"則停止循環(huán)
os.println(readline);
//將從系統(tǒng)標(biāo)準(zhǔn)輸入讀入的字符串輸出到Server
os.flush();
//刷新輸出流,使Server馬上收到該字符串
System.out.println("Client:"+readline);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印讀入的字符串
System.out.println("Server:"+is.readLine());
//從Server讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
readline=sin.readLine(); //從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
}catch(Exception e) {
System.out.println("Error"+e); //出錯(cuò),則打印出錯(cuò)信息
}
}
} 2. 服務(wù)器端程序
import java.io.*;
import java.net.*;
import java.applet.Applet;
public class TalkServer{
public static void main(String args[]) {
try{
ServerSocket server=null;
try{
server=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(Exception e) {
System.out.println("can not listen to:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
Socket socket=null;
try{
socket=server.accept();
//使用accept()阻塞等待客戶請求,有客戶
//請求到來則產(chǎn)生一個(gè)Socket對象,并繼續(xù)執(zhí)行
}catch(Exception e) {
System.out.println("Error."+e);
//出錯(cuò),打印出錯(cuò)信息
}
String line;
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
PrintWriter os=newPrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
System.out.println("Client:"+is.readLine());
//在標(biāo)準(zhǔn)輸出上打印從客戶端讀入的字符串
line=sin.readLine();
//從標(biāo)準(zhǔn)輸入讀入一字符串
while(!line.equals("bye")){
//如果該字符串為 "bye",則停止循環(huán)
os.println(line);
//向客戶端輸出該字符串
os.flush();
//刷新輸出流,使Client馬上收到該字符串
System.out.println("Server:"+line);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印讀入的字符串
System.out.println("Client:"+is.readLine());
//從Client讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
line=sin.readLine();
//從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
server.close(); //關(guān)閉ServerSocket
}catch(Exception e){
System.out.println("Error:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
}
}
從上面的兩個(gè)程序中我們可以看到,socket四個(gè)步驟的使用過程。讀者可以分別將Socket使用的四個(gè)步驟的對應(yīng)程序段選擇出來,這樣便于讀者對socket的使用有進(jìn)一步的了解。
讀者可以在單機(jī)上試驗(yàn)該程序,最好是能在真正的網(wǎng)絡(luò)環(huán)境下試驗(yàn)該程序,這樣更容易分辨輸出的內(nèi)容和客戶機(jī),服務(wù)器的對應(yīng)關(guān)系。同時(shí)也可以修改該程序,提供更為強(qiáng)大的功能,或更加滿足讀者的意圖。
8.3.9 支持多客戶的client/server程序設(shè)計(jì)
前面提供的Client/Server程序只能實(shí)現(xiàn)Server和一個(gè)客戶的對話。在實(shí)際應(yīng)用中,往往是在服務(wù)器上運(yùn)行一個(gè)永久的程序,它可以接收來自其他多個(gè)客戶端的請求,提供相應(yīng)的服務(wù)。為了實(shí)現(xiàn)在服務(wù)器方給多個(gè)客戶提供服務(wù)的功能,需要對上面的程序進(jìn)行改造,利用多線程實(shí)現(xiàn)多客戶機(jī)制。服務(wù)器總是在指定的端口上監(jiān)聽是否有客戶請求,一旦監(jiān)聽到客戶請求,服務(wù)器就會啟動(dòng)一個(gè)專門的服務(wù)線程來響應(yīng)該客戶的請求,而服務(wù)器本身在啟動(dòng)完線程之后馬上又進(jìn)入監(jiān)聽狀態(tài),等待下一個(gè)客戶的到來。
客戶端的程序和上面程序是完全一樣的,讀者如果仔細(xì)閱讀過上面的程序,可以跳過不讀,把主要精力集中在Server端的程序上。
2. 服務(wù)器端程序: MultiTalkServer.java
import java.io.*;
import java.net.*;
import ServerThread;
public class MultiTalkServer{
static int clientnum=0; //靜態(tài)成員變量,記錄當(dāng)前客戶的個(gè)數(shù)
public static void main(String args[]) throws IOException {
ServerSocket serverSocket=null;
boolean listening=true;
try{
serverSocket=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(IOException e) {
System.out.println("Could not listen on port:4700.");
//出錯(cuò),打印出錯(cuò)信息
System.exit(-1); //退出
}
while(listening){ //永遠(yuǎn)循環(huán)監(jiān)聽
new ServerThread(serverSocket.accept(),clientnum).start();
//監(jiān)聽到客戶請求,根據(jù)得到的Socket對象和
客戶計(jì)數(shù)創(chuàng)建服務(wù)線程,并啟動(dòng)之
clientnum++; //增加客戶計(jì)數(shù)
}
serverSocket.close(); //關(guān)閉ServerSocket
}
}
3. 程序ServerThread.java
import java.io.*;
import java.net.*;
public class ServerThread extends Thread{
Socket socket=null; //保存與本線程相關(guān)的Socket對象
int clientnum; //保存本進(jìn)程的客戶計(jì)數(shù)
public ServerThread(Socket socket,int num) { //構(gòu)造函數(shù)
this.socket=socket; //初始化socket變量
clientnum=num+1; //初始化clientnum變量
}
public void run() { //線程主體
try{
String line;
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
PrintWriter os=newPrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
System.out.println("Client:"+ clientnum +is.readLine());
//在標(biāo)準(zhǔn)輸出上打印從客戶端讀入的字符串
line=sin.readLine();
//從標(biāo)準(zhǔn)輸入讀入一字符串
while(!line.equals("bye")){
//如果該字符串為 "bye",則停止循環(huán)
os.println(line);
//向客戶端輸出該字符串
os.flush();
//刷新輸出流,使Client馬上收到該字符串
System.out.println("Server:"+line);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印該字符串
System.out.println("Client:"+ clientnum +is.readLine());
//從Client讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
line=sin.readLine();
//從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
server.close(); //關(guān)閉ServerSocket
}catch(Exception e){
System.out.println("Error:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
}
}
通過以上的學(xué)習(xí),讀者應(yīng)該對Java的面向流的網(wǎng)絡(luò)編程有了一個(gè)比較全面的認(rèn)識,這些都是基于TCP的應(yīng)用,后面我們將介紹基于UDP的Socket編程