相對於C和C++來說,Java中的socket編程是比較簡單的,比較多的細節都已經被封裝好了,每次創建socket連接只需要知道地址和端口即可。
在了解socket編程之前,我們先來了解一下讀寫數據的數據流類中一些需要注意的東西。
BufferedReader與DataInputStream的區別:
通常我們常用到的字節輸入輸出流有BufferedReader與PrintWriter,DataInputStream和DataOutputStream這兩對。這些類都屬於java.io包。
那么兩者之間有什么區別呢?
區別就是前者有個緩沖區,假如我們人為設置為100k(不設置亦可,有默認值),當這個緩沖區存儲的內容達到100k的時候,類對象才會進行讀入或寫入操作。
而Stream的兩個對象是沒有緩沖區的,它們是收到什么數據就即刻進行讀出和寫入。
所以在進行socket編程的時候,這兩對最好不要交替使用,因為當有數據存到前面提到的緩存里的時候,stream對象沒有辦法讀到緩存里的東西,所以會造成數據的丟失。
在這里我們另外說一說PrintWriter類,先看看比較常用的兩個構造方法:
在第二個構造方法中,參數2指明該對象是否自動將緩沖區里的數據流自動刷出,一般來說我們可以采用第二種構造方法,將參數2設為true。
否則,在每次用PrintWriter對象調用printXXX方法的時候,后面就要緊接着使用flush方法。
比如:
PrintWriter pw = new PrintWriter(socket.getOutputStream);
pw.println(“寫出數據”);
pw.flush();
如果你不這么做的話,pw對象可能會因為你要寫出的數據並未到達緩沖區指定大小而不作任何操作。這個時候你的線程就會阻塞!!所以關於這一點務必小心。
Java里的Socket工作模式:
在socket編程中我們基本上需要用到這些類:
SocketServer、Socket、BufferedReader與PrintWriter(或者DataInputStream與DataOutputStream)。
在服務器中,首先新建一個服務器socket對象:
ServerSocket srvSocket = new ServerSocket(nPort);
一旦接收到請求,則生成一個socket對象:
Socket socket = srvSocket.accept();
然后創建流對象:
BufferedReader bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter pw = new PrintWriter(socket.getOutputStream(),true);
客戶端里直接進行連接然后創建流對象即可:
Socket socket = new Socket(hostAddr, nPort);
Socket編程基礎Java實現:
其實在實現過程中遇到挺多很細節但是又很讓人蛋疼的問題,比如前面提到的PrintWriter對象要么初始化的時候就設定為自動刷出緩存區內容,要么就每次寫操作后面調用flush方法。下面給出實現方法:
客戶端實現:
客戶端實現的功能是這樣的,輸入一些特定的字符串,比如:DATE,BYE,DOY,DOM,DOW什么的,然后讓服務器判斷輸入的是什么命令,然后服務器調用Calendar類返回對應的日期和時間信息。
我希望能從控制台讀取用戶輸入的信息,所以設計了如下代碼:
BufferedReader inSys = new BufferedReader(new InputStreamReader(System.in)); while((string = inSys.readLine()) != null && string.length() != 0) { System.out.println("客戶端這邊輸入的命令是"+string); pw.println(string); System.out.println("服務器返還的數據是"+bf.readLine()); //ctrl+z or Enter to terminate the loop }
這樣的話,但凡是用戶按了ctrl+z或者是Enter鍵,則結束循環。
下面給出客戶端完整實現代碼:

import java.io.*; import java.net.*; import java.util.*; public class NeroSocketClient { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub System.out.println("新客戶端開啟"); //三個變量要放在try-catch塊前聲明,不然的話在finall塊中,try里面聲明和定義的內容是不可見的,屬於不同的作用域,生命周期不同 Socket socket = null; BufferedReader bf = null; PrintWriter pw = null; try { socket = new Socket("127.0.0.1", 8888); bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); pw = new PrintWriter(socket.getOutputStream(),true); //參數2:自動flush緩沖區內容 BufferedReader inSys = new BufferedReader(new InputStreamReader(System.in)); String string; System.out.println("支持的命令如下:"); System.out.println("BYE:結束連接"); System.out.println("DATE:日期和時間"); System.out.println("DOW:day_of_week"); System.out.println("DOM:day_of_month"); System.out.println("DOY:day_of_year"); System.out.println("PAUSE:暫停"); System.out.println("舉個例子:客戶端這邊輸入的是DATE"); pw.println("DATE"); pw.flush(); System.out.println ("服務器返還的數據是"+bf.readLine ()); System.out.println("請輸入:"); while((string = inSys.readLine()) != null && string.length() != 0) { System.out.println("客戶端這邊輸入的命令是"+string); pw.println(string); System.out.println("服務器返還的數據是"+bf.readLine()); //ctrl+z or Enter to terminate the loop } } catch (IOException e) { // TODO: handle exception System.out.println (e.toString ()); }finally{ //關閉連接 try { if (bf != null) { bf.close(); } if (pw != null) { pw.close(); } if (socket != null) { socket.close(); } } catch (IOException e2) { // TODO: handle exception } } } }
服務器實現:
設計服務器的時候,對進行數據讀寫操作的類應用Runnable接口,這樣即可實現多線程,因為服務器沒可能只對一個客戶端提供服務的,所以寫練習程序的時候直接寫多線程的即可,從最基本的練起沒必要,進度太慢。
在服務器的主方法里面,我們通過一個無限循環來不斷地接受新發現的連接請求:
ServerSocket srvSocket = new ServerSocket(8888); while(true) //服務器是需要一直運行的,這樣可以不斷地監聽和接收新的socket連接 { Socket socket = srvSocket.accept(); //收到新的請求 System.out.println("收到新的socket連接請求"); ServerThread sThread = new ServerThread(socket); Thread thread = new Thread(sThread); thread.start(); //上面的三行代碼,不妨直接寫成: //new Thread(new ServerThread(socket)).start(); }
這樣的話,服務器即可一直運行。
具體操作數據的方法寫在從接口繼承來的run方法即可,這個方法是必須被重載的。
下面給出服務器實現代碼:

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.*; import java.util.*; public class NeroSocketServer { public static void main(String[] args) throws IOException{ // TODO Auto-generated method stub System.out.println("新服務器開啟"); ServerSocket srvSocket = new ServerSocket(8888); while(true) //服務器是需要一直運行的,這樣可以不斷地監聽和接收新的socket連接 { Socket socket = srvSocket.accept(); //收到新的請求 System.out.println("收到新的socket連接請求"); ServerThread sThread = new ServerThread(socket); Thread thread = new Thread(sThread); thread.start(); //上面的三行代碼,不妨直接寫成: //new Thread(new ServerThread(socket)).start(); } } } //應用這個接口,在run方法里面定義具體操作 class ServerThread implements Runnable { private Socket socket; //構造函數 public ServerThread(Socket s) { this.socket = s; } @Override public void run() { // TODO Auto-generated method stub System.out.println("線程開始"); BufferedReader bf = null; PrintWriter pw = null; try { bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); pw = new PrintWriter(socket.getOutputStream(),true); //如果參數2不設為true,則每次都需要執行flush操作 //程序實現功能:通過客戶端請求,返回日期、時間等信息 //靜態方法,直接調用 Calendar calendar = Calendar.getInstance(); System.out.println("准備進入循環"); while(true) //一直循環,直到用戶請求完畢 { String s_request = bf.readLine(); //將所有命令轉換為大寫 s_request = s_request.toUpperCase(); System.out.println("當前接受到的命令是"+s_request); if (s_request.startsWith("BYE")) { //結束 break; } if (s_request.startsWith("DATE") || s_request.startsWith("TIME")) { System.out.println("輸出日期和時間"); pw.println(calendar.getTime().toString()); System.out.println("輸出完畢"); } if (s_request.startsWith("DOM")) { pw.println(""+calendar.get(Calendar.DAY_OF_MONTH)); //以字符形式寫入 } if (s_request.startsWith("DOW")) { switch (calendar.get(Calendar.DAY_OF_WEEK)) { case Calendar.SUNDAY: pw.println("SUNDAY"); break; case Calendar.MONDAY: pw.println("MONDAY"); break; case Calendar.TUESDAY: pw.println("TUESDAY"); break; case Calendar.WEDNESDAY: pw.println("WEDNESDAY"); break; case Calendar.THURSDAY: pw.println("THURSDAY"); break; case Calendar.FRIDAY: pw.println("FRIDAY"); break; case Calendar.SATURDAY: pw.println("SATURDAY"); break; default: break; } } if (s_request.startsWith("DOY")) { pw.println(""+calendar.get(Calendar.DAY_OF_YEAR)); } if (s_request.startsWith("PAUSE")) { try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO: handle exception System.out.println(e.toString()); } } } } catch (Exception e) { // TODO: handle exception System.out.println(e.toString()); } finally{ //關閉連接 System.out.println("當前客戶端斷開連接"); try { if (bf != null) { bf.close(); } if (pw != null) { pw.close(); } if (socket != null) { socket.close(); } } catch (Exception e2) { // TODO: handle exception } } } }
最后我們看看客戶端和服務器的運行情況,先運行服務器,然后運行客戶端1,在客戶端1輸入一些測試命令以后,我們運行客戶端2。
因為開啟兩個客戶端是在同一個eclipse中開啟,所以測試有點不准確,不過也懶得去開多個編譯器了,就是那么一回事。對於這個問題呢,我們可以開多幾個eclipse來運行(工作空間必須是不同的)。
也可以直接在控制台(cmd)里進行編譯“javac 主類名.java”,生成.class字節碼文件以后,用“java 類名”的方式運行客戶端,在此不作演示。
客戶端(左邊)的截圖只記錄了第二個客戶端開啟以后輸入的信息。