Java Socket編程
對於Java Socket編程而言,有兩個概念,一個是ServerSocket,一個是Socket。服務端和客戶端之間通過Socket建立連接,之后它們就可以進行通信了。首先ServerSocket將在服務端監聽某個端口,當發現客戶端有Socket來試圖連接它時,它會accept該Socket的連接請求,同時在服務端建立一個對應的Socket與之進行通信。這樣就有兩個Socket了,客戶端和服務端各一個。
對於Socket之間的通信其實很簡單,服務端往Socket的輸出流里面寫東西,客戶端就可以通過Socket的輸入流讀取對應的內容。Socket與Socket之間是雙向連通的,所以客戶端也可以往對應的Socket輸出流里面寫東西,然后服務端對應的Socket的輸入流就可以讀出對應的內容。下面來看一些服務端與客戶端通信的例子:
1、客戶端寫服務端讀
服務端代碼
public class Server { public static void main(String args[]) throws IOException { //為了簡單起見,所有的異常信息都往外拋 int port = 8899; //定義一個ServerSocket監聽在端口8899上 ServerSocket server = new ServerSocket(port); //server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的 Socket socket = server.accept(); //跟客戶端建立好連接之后,我們就可以獲取socket的InputStream,並從中讀取客戶端發過來的信息了。 Reader reader = new InputStreamReader(socket.getInputStream()); char chars[] = new char[64]; int len; StringBuilder sb = new StringBuilder(); while ((len=reader.read(chars)) != -1) { sb.append(new String(chars, 0, len)); } System.out.println("from client: " + sb); reader.close(); socket.close(); server.close(); } }
服務端從Socket的InputStream中讀取數據的操作也是阻塞式的,如果從輸入流中沒有讀取到數據程序會一直在那里不動,直到客戶端往Socket的輸出流中寫入了數據,或關閉了Socket的輸出流。當然,對於客戶端的Socket也是同樣如此。在操作完以后,整個程序結束前記得關閉對應的資源,即關閉對應的IO流和Socket。
客戶端代碼
1 public class Client { 2 3 public static void main(String args[]) throws Exception { 4 //為了簡單起見,所有的異常都直接往外拋 5 String host = "127.0.0.1"; //要連接的服務端IP地址 6 int port = 8899; //要連接的服務端對應的監聽端口 7 //與服務端建立連接 8 Socket client = new Socket(host, port); 9 //建立連接后就可以往服務端寫數據了 10 Writer writer = new OutputStreamWriter(client.getOutputStream()); 11 writer.write("Hello Server."); 12 writer.flush();//寫完后要記得flush 13 writer.close(); 14 client.close(); 15 } 16 17 }
對於客戶端往Socket的輸出流里面寫數據傳遞給服務端要注意一點,如果寫操作之后程序不是對應着輸出流的關閉,而是進行其他阻塞式的操作(比如從輸入流里面讀數據),記住要flush一下,只有這樣服務端才能收到客戶端發送的數據,否則可能會引起兩邊無限的互相等待。在稍后講到客戶端和服務端同時讀和寫的時候會說到這個問題。
2、客戶端和服務端同時讀和寫
前面已經說了Socket之間是雙向通信的,它既可以接收數據,同時也可以發送數據。
服務端代碼
1 public class Server { 2 3 public static void main(String args[]) throws IOException { 4 //為了簡單起見,所有的異常信息都往外拋 5 int port = 8899; 6 //定義一個ServerSocket監聽在端口8899上 7 ServerSocket server = new ServerSocket(port); 8 //server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的 9 Socket socket = server.accept(); 10 //跟客戶端建立好連接之后,我們就可以獲取socket的InputStream,並從中讀取客戶端發過來的信息了。 11 Reader reader = new InputStreamReader(socket.getInputStream()); 12 char chars[] = new char[64]; 13 int len; 14 StringBuilder sb = new StringBuilder(); 15 while ((len=reader.read(chars)) != -1) { 16 sb.append(new String(chars, 0, len)); 17 } 18 System.out.println("from client: " + sb); 19 //讀完后寫一句 20 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 21 writer.write("Hello Client."); 22 writer.flush(); 23 writer.close(); 24 reader.close(); 25 socket.close(); 26 server.close(); 27 } 28 29 }
在上述代碼中首先我們從輸入流中讀取客戶端發送過來的數據,接下來我們再往輸出流里面寫入數據給客戶端,接下來關閉對應的資源文件。而實際上上述代碼可能並不會按照我們預先設想的方式運行,因為從輸入流中讀取數據是一個阻塞式操作,在上述的while循環中當讀到數據的時候就會執行循環體,否則就會阻塞,這樣后面的寫操作就永遠都執行不了了。除非客戶端對應的Socket關閉了阻塞才會停止,while循環也會跳出。針對這種可能永遠無法執行下去的情況的解決方法是while循環需要在里面有條件的跳出來,縱觀上述代碼,在不斷變化的也只有取到的長度len和讀到的數據了,len已經是不能用的了,唯一能用的就是讀到的數據了。針對這種情況,通常我們都會約定一個結束標記,當客戶端發送過來的數據包含某個結束標記時就說明當前的數據已經發送完畢了,這個時候我們就可以進行循環的跳出了。那么改進后的代碼會是這個樣子:
1 public class Server { 2 3 public static void main(String args[]) throws IOException { 4 //為了簡單起見,所有的異常信息都往外拋 5 int port = 8899; 6 //定義一個ServerSocket監聽在端口8899上 7 ServerSocket server = new ServerSocket(port); 8 //server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的 9 Socket socket = server.accept(); 10 //跟客戶端建立好連接之后,我們就可以獲取socket的InputStream,並從中讀取客戶端發過來的信息了。 11 Reader reader = new InputStreamReader(socket.getInputStream()); 12 char chars[] = new char[64]; 13 int len; 14 StringBuilder sb = new StringBuilder(); 15 String temp; 16 int index; 17 while ((len=reader.read(chars)) != -1) { 18 temp = new String(chars, 0, len); 19 if ((index = temp.indexOf("eof")) != -1) {//遇到eof時就結束接收 20 sb.append(temp.substring(0, index)); 21 break; 22 } 23 sb.append(temp); 24 } 25 System.out.println("from client: " + sb); 26 //讀完后寫一句 27 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 28 writer.write("Hello Client."); 29 writer.flush(); 30 writer.close(); 31 reader.close(); 32 socket.close(); 33 server.close(); 34 } 35 36 }
在上述代碼中,當服務端讀取到客戶端發送的結束標記,即“eof”時就會結束數據的接收,終止循環,這樣后續的代碼又可以繼續進行了。
客戶端代碼
public class Client { public static void main(String args[]) throws Exception { //為了簡單起見,所有的異常都直接往外拋 String host = "127.0.0.1"; //要連接的服務端IP地址 int port = 8899; //要連接的服務端對應的監聽端口 //與服務端建立連接 Socket client = new Socket(host, port); //建立連接后就可以往服務端寫數據了 Writer writer = new OutputStreamWriter(client.getOutputStream()); writer.write("Hello Server."); writer.flush(); //寫完以后進行讀操作 Reader reader = new InputStreamReader(client.getInputStream()); char chars[] = new char[64]; int len; StringBuffer sb = new StringBuffer(); while ((len=reader.read(chars)) != -1) { sb.append(new String(chars, 0, len)); } System.out.println("from server: " + sb); writer.close(); reader.close(); client.close(); } }
在上述代碼中我們先是給服務端發送了一段數據,之后讀取服務端返回來的數據,跟之前的服務端一樣在讀的過程中有可能導致程序一直掛在那里,永遠跳不出while循環。這段代碼配合服務端的第一段代碼就正好讓我們分析服務端永遠在那里接收數據,永遠跳不出while循環,也就沒有之后的服務端返回數據給客戶端,客戶端也就不可能接收到服務端返回的數據。解決方法如服務端第二段代碼所示,在客戶端發送數據完畢后,往輸出流里面寫入結束標記告訴服務端數據已經發送完畢了,同樣服務端返回數據完畢后也發一個標記告訴客戶端。那么修改后的客戶端代碼就應該是這個樣子:
1 public class Client { 2 3 public static void main(String args[]) throws Exception { 4 //為了簡單起見,所有的異常都直接往外拋 5 String host = "127.0.0.1"; //要連接的服務端IP地址 6 int port = 8899; //要連接的服務端對應的監聽端口 7 //與服務端建立連接 8 Socket client = new Socket(host, port); 9 //建立連接后就可以往服務端寫數據了 10 Writer writer = new OutputStreamWriter(client.getOutputStream()); 11 writer.write("Hello Server."); 12 writer.write("eof"); 13 writer.flush(); 14 //寫完以后進行讀操作 15 Reader reader = new InputStreamReader(client.getInputStream()); 16 char chars[] = new char[64]; 17 int len; 18 StringBuffer sb = new StringBuffer(); 19 String temp; 20 int index; 21 while ((len=reader.read(chars)) != -1) { 22 temp = new String(chars, 0, len); 23 if ((index = temp.indexOf("eof")) != -1) { 24 sb.append(temp.substring(0, index)); 25 break; 26 } 27 sb.append(new String(chars, 0, len)); 28 } 29 System.out.println("from server: " + sb); 30 writer.close(); 31 reader.close(); 32 client.close(); 33 } 34 35 } 36
我們日常使用的比較多的都是這種客戶端發送數據給服務端,服務端接收數據后再返回相應的結果給客戶端這種形式。只是客戶端和服務端之間不再是這種一對一的關系,而是下面要講到的多個客戶端對應同一個服務端的情況。
3、多個客戶端連接同一個服務端
像前面講的兩個例子都是服務端接收一個客戶端的請求之后就結束了,不能再接收其他客戶端的請求了,這往往是不能滿足我們的要求的。通常我們會這樣做:
1 public class Server { 2 3 public static void main(String args[]) throws IOException { 4 //為了簡單起見,所有的異常信息都往外拋 5 int port = 8899; 6 //定義一個ServerSocket監聽在端口8899上 7 ServerSocket server = new ServerSocket(port); 8 while (true) { 9 //server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的 10 Socket socket = server.accept(); 11 //跟客戶端建立好連接之后,我們就可以獲取socket的InputStream,並從中讀取客戶端發過來的信息了。 12 Reader reader = new InputStreamReader(socket.getInputStream()); 13 char chars[] = new char[64]; 14 int len; 15 StringBuilder sb = new StringBuilder(); 16 String temp; 17 int index; 18 while ((len=reader.read(chars)) != -1) { 19 temp = new String(chars, 0, len); 20 if ((index = temp.indexOf("eof")) != -1) {//遇到eof時就結束接收 21 sb.append(temp.substring(0, index)); 22 break; 23 } 24 sb.append(temp); 25 } 26 System.out.println("from client: " + sb); 27 //讀完后寫一句 28 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 29 writer.write("Hello Client."); 30 writer.flush(); 31 writer.close(); 32 reader.close(); 33 socket.close(); 34 } 35 } 36 37 }
在上面代碼中我們用了一個死循環,在循環體里面ServerSocket調用其accept方法試圖接收來自客戶端的連接請求。當沒有接收到請求的時候,程序會在這里阻塞直到接收到來自客戶端的連接請求,之后會跟當前建立好連接的客戶端進行通信,完了后會接着執行循環體再次嘗試接收新的連接請求。這樣我們的ServerSocket就能接收來自所有客戶端的連接請求了,並且與它們進行通信了。這就實現了一個簡單的一個服務端與多個客戶端進行通信的模式。
上述例子中雖然實現了一個服務端跟多個客戶端進行通信,但是還存在一個問題。在上述例子中,我們的服務端處理客戶端的連接請求是同步進行的,每次接收到來自客戶端的連接請求后,都要先跟當前的客戶端通信完之后才能再處理下一個連接請求。這在並發比較多的情況下會嚴重影響程序的性能,為此,我們可以把它改為如下這種異步處理與客戶端通信的方式:
1 public class Server { 2 3 public static void main(String args[]) throws IOException { 4 //為了簡單起見,所有的異常信息都往外拋 5 int port = 8899; 6 //定義一個ServerSocket監聽在端口8899上 7 ServerSocket server = new ServerSocket(port); 8 while (true) { 9 //server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的 10 Socket socket = server.accept(); 11 //每接收到一個Socket就建立一個新的線程來處理它 12 new Thread(new Task(socket)).start(); 13 } 14 } 15 16 /** 17 * 用來處理Socket請求的 18 */ 19 static class Task implements Runnable { 20 21 private Socket socket; 22 23 public Task(Socket socket) { 24 this.socket = socket; 25 } 26 27 public void run() { 28 29 try { 30 31 handleSocket(); 32 } catch (Exception e) { 33 e.printStackTrace(); 34 } 35 } 36 37 /** 38 * 跟客戶端Socket進行通信 39 * @throws Exception 40 */ 41 private void handleSocket() throws Exception { 42 Reader reader = new InputStreamReader(socket.getInputStream()); 43 char chars[] = new char[64]; 44 int len; 45 StringBuilder sb = new StringBuilder(); 46 String temp; 47 int index; 48 while ((len=reader.read(chars)) != -1) { 49 temp = new String(chars, 0, len); 50 if ((index = temp.indexOf("eof")) != -1) {//遇到eof時就結束接收 51 sb.append(temp.substring(0, index)); 52 break; 53 } 54 sb.append(temp); 55 } 56 System.out.println("from client: " + sb); 57 //讀完后寫一句 58 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 59 writer.write("Hello Client."); 60 writer.flush(); 61 writer.close(); 62 reader.close(); 63 socket.close(); 64 } 65 66 } 67 68 }
在上面代碼中,每次ServerSocket接收到一個新的Socket連接請求后都會新起一個線程來跟當前Socket進行通信,這樣就達到了異步處理與客戶端Socket進行通信的情況。
在從Socket的InputStream中接收數據時,像上面那樣一點點的讀就太復雜了,有時候我們就會換成使用BufferedReader來一次讀一行,如:
1 public class Server { 2 3 public static void main(String args[]) throws IOException { 4 //為了簡單起見,所有的異常信息都往外拋 5 int port = 8899; 6 //定義一個ServerSocket監聽在端口8899上 7 ServerSocket server = new ServerSocket(port); 8 while (true) { 9 //server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的 10 Socket socket = server.accept(); 11 //每接收到一個Socket就建立一個新的線程來處理它 12 new Thread(new Task(socket)).start(); 13 } 14 } 15 16 /** 17 * 用來處理Socket請求的 18 */ 19 static class Task implements Runnable { 20 21 private Socket socket; 22 23 public Task(Socket socket) { 24 this.socket = socket; 25 } 26 27 public void run() { 28 try { 29 handleSocket(); 30 } catch (Exception e) { 31 e.printStackTrace(); 32 } 33 } 34 35 /** 36 * 跟客戶端Socket進行通信 37 * @throws Exception 38 */ 39 private void handleSocket() throws Exception { 40 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); 41 StringBuilder sb = new StringBuilder(); 42 String temp; 43 int index; 44 while ((temp=br.readLine()) != null) { 45 System.out.println(temp); 46 if ((index = temp.indexOf("eof")) != -1) {//遇到eof時就結束接收 47 sb.append(temp.substring(0, index)); 48 break; 49 } 50 sb.append(temp); 51 } 52 System.out.println("from client: " + sb); 53 //讀完后寫一句 54 Writer writer = new OutputStreamWriter(socket.getOutputStream()); 55 writer.write("Hello Client."); 56 writer.write("eof\n"); 57 writer.flush(); 58 writer.close(); 59 br.close(); 60 socket.close(); 61 } 62 } 63 }
這個時候需要注意的是,BufferedReader的readLine方法是一次讀一行的,這個方法是阻塞的,直到它讀到了一行數據為止程序才會繼續往下執行,那么readLine什么時候才會讀到一行呢?直到程序遇到了換行符或者是對應流的結束符readLine方法才會認為讀到了一行,才會結束其阻塞,讓程序繼續往下執行。所以我們在使用BufferedReader的readLine讀取數據的時候一定要記得在對應的輸出流里面一定要寫入換行符(流結束之后會自動標記為結束,readLine可以識別),寫入換行符之后一定記得如果輸出流不是馬上關閉的情況下記得flush一下,這樣數據才會真正的從緩沖區里面寫入。對應上面的代碼我們的客戶端程序應該這樣寫:
1 public class Client { 2 3 public static void main(String args[]) throws Exception { 4 //為了簡單起見,所有的異常都直接往外拋 5 String host = "127.0.0.1"; //要連接的服務端IP地址 6 int port = 8899; //要連接的服務端對應的監聽端口 7 //與服務端建立連接 8 Socket client = new Socket(host, port); 9 //建立連接后就可以往服務端寫數據了 10 Writer writer = new OutputStreamWriter(client.getOutputStream()); 11 writer.write("Hello Server."); 12 writer.write("eof\n"); 13 writer.flush(); 14 //寫完以后進行讀操作 15 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); 16 StringBuffer sb = new StringBuffer(); 17 String temp; 18 int index; 19 while ((temp=br.readLine()) != null) { 20 if ((index = temp.indexOf("eof")) != -1) { 21 sb.append(temp.substring(0, index)); 22 break; 23 } 24 sb.append(temp); 25 } 26 System.out.println("from server: " + sb); 27 writer.close(); 28 br.close(); 29 client.close(); 30 } 31 }
4、設置超時時間
假設有這樣一種需求,我們的客戶端需要通過Socket從服務端獲取到XX信息,然后給用戶展示在頁面上。我們知道Socket在讀數據的時候是阻塞式的,如果沒有讀到數據程序會一直阻塞在那里。在同步請求的時候我們肯定是不能允許這樣的情況發生的,這就需要我們在請求達到一定的時間后控制阻塞的中斷,讓程序得以繼續運行。Socket為我們提供了一個setSoTimeout()方法來設置接收數據的超時時間,單位是毫秒。當設置的超時時間大於0,並且超過了這一時間Socket還沒有接收到返回的數據的話,Socket就會拋出一個SocketTimeoutException。
假設我們需要控制我們的客戶端在開始讀取數據10秒后還沒有讀到數據就中斷阻塞的話我們可以這樣做:
1 public class Client { 2 3 public static void main(String args[]) throws Exception { 4 //為了簡單起見,所有的異常都直接往外拋 5 String host = "127.0.0.1"; //要連接的服務端IP地址 6 int port = 8899; //要連接的服務端對應的監聽端口 7 //與服務端建立連接 8 Socket client = new Socket(host, port); 9 //建立連接后就可以往服務端寫數據了 10 Writer writer = new OutputStreamWriter(client.getOutputStream()); 11 writer.write("Hello Server."); 12 writer.write("eof\n"); 13 writer.flush(); 14 //寫完以后進行讀操作 15 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); 16 //設置超時間為10秒 17 client.setSoTimeout(10*1000); 18 StringBuffer sb = new StringBuffer(); 19 String temp; 20 int index; 21 try { 22 while ((temp=br.readLine()) != null) { 23 if ((index = temp.indexOf("eof")) != -1) { 24 sb.append(temp.substring(0, index)); 25 break; 26 } 27 sb.append(temp); 28 } 29 } catch (SocketTimeoutException e) { 30 System.out.println("數據讀取超時。"); 31 } 32 System.out.println("from server: " + sb); 33 writer.close(); 34 br.close(); 35 client.close(); 36 } 37 }
5、接收數據亂碼
對於這種服務端或客戶端接收中文亂碼的情況通常是因為數據發送時使用的編碼跟接收時候使用的編碼不一致。比如有下面這樣一段服務端代碼:
1 public class Server { 2 3 public static void main(String args[]) throws IOException { 4 //為了簡單起見,所有的異常信息都往外拋 5 int port = 8899; 6 //定義一個ServerSocket監聽在端口8899上 7 ServerSocket server = new ServerSocket(port); 8 while (true) { 9 //server嘗試接收其他Socket的連接請求,server的accept方法是阻塞式的 10 Socket socket = server.accept(); 11 //每接收到一個Socket就建立一個新的線程來處理它 12 new Thread(new Task(socket)).start(); 13 } 14 } 15 16 /** 17 * 用來處理Socket請求的 18 */ 19 static class Task implements Runnable { 20 21 private Socket socket; 22 23 public Task(Socket socket) { 24 this.socket = socket; 25 } 26 27 public void run() { 28 try { 29 handleSocket(); 30 } catch (Exception e) { 31 e.printStackTrace(); 32 } 33 } 34 35 /** 36 * 跟客戶端Socket進行通信 37 * @throws Exception 38 */ 39 private void handleSocket() throws Exception { 40 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "GBK")); 41 StringBuilder sb = new StringBuilder(); 42 String temp; 43 int index; 44 while ((temp=br.readLine()) != null) { 45 System.out.println(temp); 46 if ((index = temp.indexOf("eof")) != -1) {//遇到eof時就結束接收 47 sb.append(temp.substring(0, index)); 48 break; 49 } 50 sb.append(temp); 51 } 52 System.out.println("客戶端: " + sb); 53 //讀完后寫一句 54 Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8"); 55 writer.write("你好,客戶端。"); 56 writer.write("eof\n"); 57 writer.flush(); 58 writer.close(); 59 br.close(); 60 socket.close(); 61 } 62 } 63 }
這里用來測試我就弄的混亂了一點。在上面服務端代碼中我們在定義輸入流的時候明確定義了使用GBK編碼來讀取數據,而在定義輸出流的時候明確指定了將使用UTF-8編碼來發送數據。如果客戶端上送數據的時候不以GBK編碼來發送的話服務端接收的數據就很有可能會亂碼;同樣如果客戶端接收數據的時候不以服務端發送數據的編碼,即UTF-8編碼來接收數據的話也極有可能會出現數據亂碼的情況。所以,對於上述服務端代碼,為使我們的程序能夠讀取對方發送過來的數據,而不出現亂碼情況,我們的客戶端應該是這樣的:
1 public class Client { 2 3 public static void main(String args[]) throws Exception { 4 //為了簡單起見,所有的異常都直接往外拋 5 String host = "127.0.0.1"; //要連接的服務端IP地址 6 int port = 8899; //要連接的服務端對應的監聽端口 7 //與服務端建立連接 8 Socket client = new Socket(host, port); 9 //建立連接后就可以往服務端寫數據了 10 Writer writer = new OutputStreamWriter(client.getOutputStream(), "GBK"); 11 writer.write("你好,服務端。"); 12 writer.write("eof\n"); 13 writer.flush(); 14 //寫完以后進行讀操作 15 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); 16 //設置超時間為10秒 17 client.setSoTimeout(10*1000); 18 StringBuffer sb = new StringBuffer(); 19 String temp; 20 int index; 21 try { 22 while ((temp=br.readLine()) != null) { 23 if ((index = temp.indexOf("eof")) != -1) { 24 sb.append(temp.substring(0, index)); 25 break; 26 } 27 sb.append(temp); 28 } 29 } catch (SocketTimeoutException e) { 30 System.out.println("數據讀取超時。"); 31 } 32 System.out.println("服務端: " + sb); 33 writer.close(); 34 br.close(); 35 client.close(); 36 } 37 }