摘 要:本文介紹了Java語言的Socket編程,包括服務端和用戶端的編程方法,並提供了若干執行個體。 關鍵詞:Java, Socket, Server, Client, Internet 一、什麼是Socket Socket 介面是訪問 Internet 使用得最廣泛的方法。 如果你有一台剛配好TCP/IP協議的主機,其IP地址是202.120.127.201, 此時在另一台主機或同一台主機上執行ftp 202.120.127.201,顯然無法建立串連。因為“202.120.127.201” 這台主機沒有運行FTP服務軟體。同樣, 在另一台或同一台主機上運行瀏覽軟體如Netscape,輸入“http://202.120.127.201”,也無法建立串連。現在,如果在這台主機上運行一個FTP服務軟體(該軟體將開啟一個Socket,並將其綁定到21連接埠),再在這台主機上運行一個Web 服務軟體(該軟體將開啟另一個Socket,並將其綁定到80連接埠)。這樣,在另一台主機或同一台主機上執行ftp 202.120.127.201,FTP客戶軟體將通過21連接埠來呼叫主機上由FTP 服務軟體提供的Socket,與其建立串連並對話。而在netscape中輸入“http://202.120.127.201”時,將通過80連接埠來呼叫主機上由Web服務軟體提供的Socket,與其建立串連並對話。 在Internet上有很多這樣的主機,這些主機一般運行了多個服務軟體,同時提供幾種服務。每種服務都開啟一個Socket,並綁定到一個連接埠上,不同的連接埠對應於不同的服務。Socket正如其英文原意那樣,象一個多孔插座。一台主機猶如布滿各種插座的房間,每個插座有一個編號,有的插座提供220伏交流電, 有的提供110伏交流電,有的則提供有線電視節目。 客戶軟體將插頭插到不同編號的插座,就可以得到不同的服務。 在Java語言中,提供了相應的Socket編程方法。用Java既可以編寫服務端的程式,又可以編寫用戶端的程式。 二、編寫服務端的程式 Java中的ServerSocket類提供了服務端的Socket介面。為了使大家對編寫服務端程式有一個感性的認識,這裡提供一個類比FTP伺服器的服務軟體。 為了簡潔起見,該程式只提供了最簡單的建立FTP串連的功能。 該程式如下: import java.io.*; import java.net.*; public class ftpserver{ public static void main(String args[]) { try{ ServerSocket ftpserver = new ServerSocket(21); Socket fs=ftpserver.accept(); PrintStream fs_out=new PrintStream(fs.getOutputStream()); DataInputStream fs_in=new DataInputStream(fs.getInputStream()); fs_out.println("Welcome to the test server"); System.out.println("got follow infor from client:"+fs_in.readLine()); fs_out.println("331 Please send Password"); System.out.println("got follow infor from client:"+fs_in.readLine()); fs_out.println("230 Login OK"); System.out.println("got follow infor from client:"+fs_in.readLine()); } catch(Exception e) { System.out.println(e); } } } 為了測試該程式,可以在一台安裝了Windows 95並配置了TCP/IP協議的微機上進行(不一定要連入Internet)。在該微機上安裝Java編譯軟體如JDK1.01 或JDK1.02(可在ftp://ftp.javasoft.com/pub/JDK-102-win32-x86.exe 下載),將上述程式存入檔案ftpserver.java,執行“javac ftpserver.java”將其編譯為位元組碼檔案ftpserver.class。這樣,只要在該微機上執行“java ftpserver.class”以運行該Java程式,該微機便成為一個類比的FTP伺服器。 測試該類比FTP伺服器,既可以在另一台連網的微機上進行, 也可以直接在該類比FTP伺服器上另開一個DOS視窗進行。運行命令列形式的FTP客戶軟體, 如在Windows 95的DOS視窗執行:ftp 202.120.127.201(如果你的Windows 95中配置TCP/IP協議時用的IP地址是其他值,需將這裡的“202.120.127.201 ”改為相應的值),便可以進行對話。是對話過程,其中帶底線的部分為使用者的輸入。 用戶端 C:/xyx/java/sock/bak/ftp>ftp 202.120.127.201 Connected to 202.120.127.201. Welcome to the test server User (202.120.127.201:(none)): anonymous 331 Please send Password Password:xyx@yc.shu.edu.cn 230 Login OK ftp> bye 類比FTP伺服器 C:/xyx/java/sock/bak/ftp>java ftpserver got follow infor from client:USER anonymous got follow infor from client:PASS xyx@yc.shu.edu.cn got follow infor from client:QUIT 下面我們來看一看該類比FTP伺服器的編程方法。在上面的程式中, 關鍵區段是下面四句: 1. ServerSocket ftpserver = new ServerSocket(21); 2. Socket fs=ftpserver.accept(); 3. PrintStream fs_out=new PrintStream(fs.getOutputStream()); 4. DataInputStream fs_in=new DataInputStream(fs.getInputStream()); 其中,第一句建立了一個服務端的Socket,並將其綁定到21連接埠。這樣,服務端的Socket將一直等待用戶端建立串連。這裡的21連接埠是FTP服務慣用的連接埠,你也可以使用其他連接埠來提供自己的服務。第二句利用Java提供的方法accept()接收用戶端的串連。第三句和第四句則為分別建立的串連開啟一個輸出和輸入資料流。這四句可以作為編寫服務端程式的一個範式,接下去的操作就是按照約定的協議對輸出和輸入資料流進行讀寫操作了。 在上面的程式中,對輸出資料流fs_out用方法println("...")向用戶端發送字串,對輸入資料流fs_in用方法readLine()獲得用戶端向服務端發送的字串, 並用System.out.println("...")在伺服器上顯示出來。 向用戶端發送資訊和讀取用戶端發送來的資訊必須按協議約定進行,這樣,服務端和用戶端之間才能順利通訊。在上面的程式中,資訊發送順序是這樣的: 1. 用戶端串連後,服務端向用戶端發送歡迎資訊。這由程式中如下一行完成: fs_out.println("Welcome to the test server"); 2. 用戶端顯示服務端發送的資訊,並提示使用者輸入帳號, 發送給服務端。在本例中,這由FTP客戶軟體完成。 3. 服務端接收用戶端提供的帳號,向用戶端發送結果碼331,並提示需要口令。這由程式中如下兩行完成:: System.out.println("got follow infor from client:"+fs_in.readLine()); fs_out.println("331 Please send Password"); 4. 用戶端提示使用者輸入口令,並將口令發送給服務端。在本例中,這由FTP客戶軟體完成。 5. 服務端接收用戶端提供的口令,向用戶端發送結果碼230,並提示註冊成功。讀取用戶端發送命令。這由程式中如下兩行完成: fs_out.println("230 Login OK"); System.out.println("got follow infor from client:"+fs_in.readLine()); 從以上我們可以看出用戶端和服務端對話的簡單過程,在這裡,我們省略了服務端對使用者及口令的檢驗以及根據用戶端輸入的不同命令執行各種操作。事實上,在上面的例子中既可以看到服務端如何向用戶端發送資訊,又可以看到服務端如何接收用戶端的資訊。因此,只要搞清楚雙方對話的協議,便不難作出相應的編程。 三、編寫用戶端的程式 在上面的程式中,我們借用了Windows 95本身提供的FTP 客戶軟體來測試我們的類比FTP服務程式。現在,我們要自己編寫一個用戶端的程式。 我們先編寫一個簡單的服務端程式和用戶端程式,以理解服務端與用戶端的通訊及其編程。 為簡明起見, 我們使用一個自己定義的簡單協議:伺服器使用一個閒置連接埠8886,用戶端串連後:1. 服務端向用戶端發送一個資訊;2. 用戶端讀取服務端的資訊並顯示,再向服務端發送一個反饋資訊;3.服務端讀取用戶端的反饋資訊並顯示。 對應於此協議,服務端的程式可如下: import java.io.*; import java.net.*; public class server{ public static void main(String args[]) { try { Server Socket server_1 = new Server Socket(8886); Socket socket_s=server_1.accept(); Print Stream server_out=new Print Stream(socket_s.get Output Stream()); Data Input Stream server_in=new Data Input Stream(socket_s. getInputStream()); server_out.println("This is infor sent by server /r"); String s1=server_in.readLine(); System.out.println("Got follow infor from client:"+s1); } catch(Exception e) { System.out.println(e); } } } 該例子與前面的類比FTP伺服器類似,不同的只是服務提供者使用的是 8886連接埠,此外由於使用的協議不同,對輸入和輸出資料流的操作不同。相應的用戶端程式可如下: import java.io.*; import java.net.*; public class client { public static void main(String args[]) { try { Socket sock_1 = new Socket("202.120.127.201", 8886); DataInputStream client_in = new DataInputStream(sock_1.getInputStream()); DataOutputStream cl_out= new DataOutputStream(sock_1.getOutputStream()); PrintStream client_out=new PrintStream(cl_out); String s1=client_in.readLine(); System.out.println("Got follow infor from server:"+s1); client_out.println("This is infor sent by client /r"); } catch(Exception e) { System.out.println(e); } } } 這是一個簡單的用戶端程式的例子,其關鍵區段是下面四句: 1. Socket sock_1 = new Socket("202.120.127.201", 8886); 2. DataInputStream client_in = new DataInputStream(sock_1.getInputStream()); 3. DataOutputStream cl_out= new DataOutputStream(sock_1.getOutputStream()); 4. PrintStream client_out=new PrintStream(cl_out); 其中,第一句建立了一個用戶端的Socket,從而與202.120.127.201主機建立一個串連。其中的8886為連接埠號碼,與服務端的Socket所綁定到的連接埠號碼相對應。第二至四句為Socket建立輸入和輸出資料流。這四句可以作為編寫用戶端程式的一個範式。接下去的操作同樣是按照約定的協議對輸出和輸入資料流進行操作。上一程式中同樣對輸入資料流client_in用方法readLine()讀取服務端發送的字串,對輸出資料流client_out用方法println("...")向服務端發送字串。 上面兩個程式編譯後執行效果如下: 用戶端 C:/xyx/java/sock/bak/c-both-s>java client Got follow infor from server:This is infor sent by server 服務端 C:/xyx/java/sock/bak/c-both-s>java server Got follow infor from client:This is infor sent by client 測試時既可以在同一台微機上開兩個DOS視窗,也可以在兩台連網的微機上進行。在上面的程式基礎上,我們可以為前面的類比FTP服務程式編寫一個用戶端程式: import java.io.*; import java.net.*; public class ftpc { public static void main(String[] args) { try { Socket sock_1 = new Socket("202.120.127.201", 21); DataInputStream client_in = new DataInputStream(sock_1.getInputStream()); DataOutputStream cl_out= new DataOutputStream(sock_1.getOutputStream()); PrintStream client_out=new PrintStream(cl_out); StringBuffer buf = new StringBuffer(50); int c; String fromServer,usertyped; while ((fromServer = client_in.readLine()) != null) { System.out.println("Server: " + fromServer); while ((c = System.in.read()) != '/n') { buf.append((char)c); } usertyped=buf.toString(); client_out.println(usertyped); client_out.flush(); buf.setLength(0); } } catch (Exception e) { System.out.println(e); } } } 該程式與前面的程式類似,不同之處在於該程式使用迴圈: while ((fromServer = client_in.readLine()) != null) { ...} 反覆讀取服務端的輸入,並用: while ((c = System.in.read()) != '/n') { buf.append((char)c); } usertyped=buf.toString(); client_out.println(usertyped); 語句讀取使用者的鍵盤輸入,發送至服務端。其對話如下所示: 用戶端 C:/xyx/java/sock/bak/ftp>java ftpc Server: Welcome to the test server anonymous Server: 331 Please send Password xyx@yc.shu.edu.cn Server: 230 Login OK bye 服務端 C:/xyx/java/sock/bak/ftp>java ftpserver got follow infor from client:anonymous got follow infor from client:xyx@yc.shu.edu.cn got follow infor from client:bye 值得一提的是,該客戶軟體不僅可以和前面的類比FTP伺服器進行通訊,而且可以和真正的FTP伺服器通訊。如將該客戶軟體中IP地址“202.120.127.201”改為某FTP伺服器的IP地址:“202.120.127.218”,則可作如下的通訊: C:/xyx/java/sock/bak/ftp>javac ftpc Server: 220 sun1000E-1 FTP server (UNIX(r) System V Release 4.0) ready. USER anonymous Server: 331 Guest login ok, send ident as password. PASS xyx@yc.shu.edu.cn Server: 230 Guest login ok, access restrictions apply. QUIT Server: 221 Goodbye. 其中,USER、PASS、QUIT分別為協議規定的使用者帳號、口令及退出的命令。 四、處理用戶端請求 以上的例子均只在服務端與用戶端相互傳送資訊,在實用中,服務端應能對用戶端不同的輸入作出不同的響應。本節給出一個服務端處理用戶端請求的例子,協議如下:客戶串連後,服務端發送“Welcome to Time server”資訊,用戶端讀取使用者輸入發送給服務端,如果用戶端輸入為Hours,則發送當前小時數至用戶端;如果用戶端輸入為Minutes、Years、Month、Day、Time、Date、down,則分別發送分鐘數、年份、月份、日期、時分秒、年月日至用戶端;用戶端輸入down則結束會話。 其用戶端仍採用上一節編寫的類比FTP伺服器的客戶程式,但需將程式中的連接埠21改為8885,以便與下面的服務端程式對話。服務端的程式修改如下: import java.net.*; import java.io.*; import java.util.Date; class server { public static void main(String args[]) { try { ServerSocket server_Socket = new ServerSocket(8885); Socket client_Socket = server_Socket.accept(); DataInputStream server_in = new DataInputStream(client_Socket.getInputStream()); PrintStream server_out = new PrintStream(client_Socket.getOutputStream()); String inputLine, outputLine; server_out.println("Welcome to Time server"); server_out.flush(); Date t=new Date(); while ((inputLine = server_in.readLine()) != null) { System.out.println("got"+inputLine); String hours = String.valueOf(t.getHours()); String minutes = String.valueOf(t.getMinutes()); String seconds = String.valueOf(t.getSeconds()); String years = String.valueOf(t.getYear()); String month = String.valueOf(t.getMonth()); String day = String.valueOf(t.getDay()); if(inputLine.equalsIgnoreCase("Down")) break; else if(inputLine.equalsIgnoreCase("Hours")) server_out.println("Current Hours is:"+hours); else if(inputLine.equalsIgnoreCase("Minutes")) server_out.println("Current Minutes is:"+minutes); else if(inputLine.equalsIgnoreCase("Years")) server_out.println("Current Years is:"+years); else if(inputLine.equalsIgnoreCase("Month")) server_out.println("Current Month is:"+month); else if(inputLine.equalsIgnoreCase("Day")) server_out.println("Current Day is:"+day); else if(inputLine.equalsIgnoreCase("Time")) server_out.println("Current Times is:"+hours+":"+minutes+":"+seconds); else if(inputLine.equalsIgnoreCase("Date")) server_out.println("Current Date is:"+years+"."+month+"."+day); else server_out.println("I don't know"); server_out.flush(); } } catch(Exception e){ System.out.println(e); } } } 在該程式中,使用類似前面用戶端的方法,用一個迴圈 while ((inputLine = server_in.readLine()) != null) { ...} 反覆讀取用戶端的資訊。在迴圈中根據用戶端傳來的不同資訊作不同的處理。 五、程式的最佳化 為了使程式更最佳化,可從以下方面入手: 1. 進行出錯處理 如可對每句使用try{...}catch(...){...}的形式處理常式中的例外情況,恰當地返回出錯資訊或進行出錯處理等。 2. 關閉開啟的Socket和流 結束對話時將所開啟的Socket和流都關閉,Java中的SeverScoket、Socket、DataInputStream及DataOutputStream類都提供了方法close()來實現此功能。 3. 支援多次串連 前面的服務端程式在結束一次對話後都將自動結束,如果再有用戶端要建立串連需要重新執行服務端的程式。為了使服務端支援多次串連,只要用一個迴圈即可。如對前面所有的服務端程式,都可以將執行“accept()”的語句至“}catch(Exception e)”語句的前一行包含在while(true){...}的迴圈體中而使其支援多次串連。 4. 使用線程 服務端程式一般使用線程,以便在等待用戶端串連時可以處理其他事情。此外,通過為每個用戶端的請求分配一個新的線程,可以使服務端能夠同時支援多個串連,平行處理用戶端的請求。 〖參考資料〗 1. Mary Campione and Kathy Walrath, "The Java Tutorial", last updated 4 Mar 96. ftp://ftp.javasoft.com/docs/tutorial.html.zip 2. Laura Lemay, Charles L. Perkins, "Teach Yourself JAVA in 21 Days" 3. "The Java Language Tutorial" ftp://java.sun.com/docs/progGuide.html.zip 4. elharo@sunsite.unc.edu, "Brewing Java: A Tutorial", Last-modified: 1996/9/20, http://sunsite.unc.edu/javafaq/javatutorial.html |