Java Socket編程編程
對於Java Socket編程而言,有兩個概念,一個是ServerSocket,一個是Socket。服務端和客戶端之間經過Socket創建鏈接,以後它們就能夠進行通訊了。首先ServerSocket將在服務端監聽某個端口,當發現客戶端有Socket來試圖鏈接它時,它會accept該Socket的鏈接請求,同時在服務端創建一個對應的Socket與之進行通訊。這樣就有兩個Socket了,客戶端和服務端各一個。併發
對於Socket之間的通訊其實很簡單,服務端往Socket的輸出流裏面寫東西,客戶端就能夠經過Socket的輸入流讀取對應的內容。Socket與Socket之間是雙向連通的,因此客戶端也能夠往對應的Socket輸出流裏面寫東西,而後服務端對應的Socket的輸入流就能夠讀出對應的內容。下面來看一些服務端與客戶端通訊的例子:app
服務端代碼異步
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。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一下,只有這樣服務端才能收到客戶端發送的數據,不然可能會引發兩邊無限的互相等待。在稍後講到客戶端和服務端同時讀和寫的時候會說到這個問題。測試
前面已經說了Socket之間是雙向通訊的,它既能夠接收數據,同時也能夠發送數據。ui
服務端代碼this
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
咱們平常使用的比較多的都是這種客戶端發送數據給服務端,服務端接收數據後再返回相應的結果給客戶端這種形式。只是客戶端和服務端之間再也不是這種一對一的關係,而是下面要講到的多個客戶端對應同一個服務端的狀況。
像前面講的兩個例子都是服務端接收一個客戶端的請求以後就結束了,不能再接收其餘客戶端的請求了,這每每是不能知足咱們的要求的。一般咱們會這樣作:
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 }
假設有這樣一種需求,咱們的客戶端須要經過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 }
對於這種服務端或客戶端接收中文亂碼的狀況一般是由於數據發送時使用的編碼跟接收時候使用的編碼不一致。好比有下面這樣一段服務端代碼:
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 }