通過Socket實現TCP編程
Socket通信 :
1.TCP協議是面向對象連接、可靠的、有序的,以字節流的方式發送數據。
2.基於TCP協議實現網絡通信的類:
- 客戶端----Socket類
- 服務器端----ServerSocket類
一、通信過程(Socket通信模型)
Socket通信模型用下圖所示:
1、在服務端建立一個ServerSocket,綁定相應的端口,並且在指定的端口進行偵聽,等待客戶端的連接。
2、當客戶端創建連接Socket並且向服務端發送請求。
3、服務器收到請求,並且接受客戶端的請求信息。一旦接收到客戶端的連接請求后,會創建一個鏈接socket,用來與客戶端的socket進行通信。 通過相應的輸入/輸出流進行數據的交換,數據的發送接收以及數據的響應等等。
4、當客戶端和服務端通信完畢后,需要分別關閉socket,結束通信。
Socket通信實現步驟:
了解Socket通信模型后,就可以簡化出Socket通信的實現步驟:
1.創建ServerSocket和Socket
2.打開鏈接到Socket的輸入/輸出流
3.按照協議對Socket進行讀/寫操作
4.關閉輸入輸出流、關閉Socket
二、Socket和ServerSocket常用方法
ServerSocket常用方法:
- ServerSocket(int port)——創建並綁定到特定端口的服務器套接字
- accept()——偵聽並接受到此套接字的連接
- close()——關閉此套接字 getInetAddress()——得到ServerSocket對象綁定的IP地址。如果ServerSocket對象未綁定IP地址,返回0.0.0.0。
- getLocalPort()——返回此套接字在其上偵聽的端口
Socket常用方法:
- Socket(InetAddress address, int port)——創建一個套接字並將其連接到指定ip地址的指定端口號
- Socket(String host, int port)——創建一個套接字並將其連接到指定主機上的指定端口號
- close()——關閉此套接字
- getInetAddress()——返回套接字連接的地址
- getInputStream()——返回此套接字的輸入流
- getOutputStream——返回此套接字的輸出流
三、編程實現基於TCP/IP的用戶登錄小程序
通過寫一個用戶登錄的小程序,來直觀了解Socket通信的工作過程,首先我們得知道在客戶端和服務器通信時,兩者的運作流程是如何的,先從服務器入手。
服務端:
1、創建ServerSocket對象,綁定監聽端口
2、通過accept()方法監聽客戶端請求
3、連接建立后,通過輸入流讀取客戶端發送的請求信息
4、通過輸出流向客戶端發送響應信息
5、關閉相關資源
那么用戶登錄的測試案例的服務器端類可以這樣(待完善):
1 //1.創建一個服務器端的Socket,即ServerSocket,指定綁定的端口 2 ServerSocket ss=new ServerSocket(8888); 3 //2.調用accept方法開始監聽,等待客戶端的連接 4 System.out.println("服務器即將啟動,等待客戶端的連接..."); 5 Socket so=ss.accept();//accept方法返回Socket實例 6 //3.獲取一個輸入流,並讀取客戶端信息 7 InputStream is=so.getInputStream();//字節輸入流 8 InputStreamReader isr=new InputStreamReader(is);//將字節輸入流包裝成字符輸入流 9 BufferedReader br=new BufferedReader(isr);//加上緩沖流,提高效率 10 String info=null; 11 while((info=br.readLine())!=null){//循環讀取客戶端信息 12 System.out.println("我是服務器,客戶端說:"+info); 13 14 } 15 so.shutdownInput();//關閉輸入流 16 //4.關閉資源 17 br.close(); 18 isr.close(); 19 is.close(); 20 so.close(); 21 ss.close();
接着我們看一下客戶端的運作過程,其實和服務器端差不多,但是其中的區別還是要清楚的。
客戶端:
1、創建Socket對象,指明需要連接的服務器的地址和端口號
2、連接建立后,通過輸出流向服務器端發送請求信息
3、通過輸入流獲取服務器相應的信息
4、關閉相關資源。
那么用戶登錄的測試案例的客戶端類可以這樣(待完善):
1 //1.創建客戶端Socket,指定服務器地址和端口 2 Socket so=new Socket("localhost", 8888);//端口號要和服務器端相同 3 //2.獲取輸出流,向服務器端發送登錄的信息 4 OutputStream os=so.getOutputStream();//字節輸出流 5 PrintWriter pw=new PrintWriter(os);//字符輸出流 6 BufferedWriter bw=new BufferedWriter(pw);//加上緩沖流 7 bw.write("用戶名:admin;密碼:123"); 8 bw.flush(); 9 so.shutdownOutput();//關閉輸出流 10 //3.關閉資源 11 bw.close(); 12 pw.close(); 13 os.close(); 14 so.close();
這樣服務端和客戶端就能進行最簡單的通信了,目前這個還不能實現交互,下面會講兩者的交互。
運行一下,看看服務器端是否能接收到客戶端發送的信息了呢...
分別啟動服務器和客戶端,注意必須先啟動服務器再啟動客戶端,否則客戶端找不到資源報錯:
完善用戶登錄之服務器響應客戶端
在上述的例子中,服務器和客戶端僅僅只是相互可以通信,但服務器對於接收到的客戶端信息並沒有向客戶端響應,客戶端沒有接收到服務器端的任何信息,這明顯是不完善不友好的,在這里將對剛剛的例子進行完善,完成服務器對客戶端的響應。
修改后的服務器端:
1 //1.創建一個服務器端的Socket,即ServerSocket,指定綁定的端口 2 ServerSocket ss= new ServerSocket(8888); 3 //2.調用accept方法開始監聽,等待客戶端的連接 4 System.out.println("服務器即將啟動,等待客戶端的連接..."); 5 Socket so=ss.accept();//accept方法返回Socket實例 6 //3.獲取一個輸入流,並讀取客戶端信息 7 InputStream is=so.getInputStream();//字節輸入流 8 InputStreamReader isr=new InputStreamReader(is);//將字節輸入流包裝成字符輸入流 9 BufferedReader br=new BufferedReader(isr);//加上緩沖流,提高效率 10 String info=null; 11 while((info=br.readLine())!=null){//循環讀取客戶端信息 12 System.out.println("我是服務器,客戶端說:"+info); 13 14 } 15 so.shutdownInput();//關閉輸入流 16 //4.獲取一個輸出流,向客戶端輸出信息,響應客戶端的請求 17 OutputStream os=so.getOutputStream();//字節輸出流 18 PrintWriter pw=new PrintWriter(os);//字符輸出流 19 BufferedWriter bw=new BufferedWriter(pw);//緩沖輸出流 20 bw.write("歡迎您!"); 21 bw.newLine(); 22 bw.flush(); 23 24 //5.關閉資源 25 os.close(); 26 pw.close(); 27 bw.close(); 28 br.close(); 29 isr.close(); 30 is.close(); 31 so.close(); 32 ss.close();
修改后的客戶端:
1 //1.創建客戶端Socket,指定服務器地址和端口 2 Socket so=new Socket("localhost", 8888);//端口號要和服務器端相同 3 //2.獲取輸出流,向服務器端發送登錄的信息 4 OutputStream os=so.getOutputStream();//字節輸出流 5 PrintWriter pw=new PrintWriter(os);//字符輸出流 6 BufferedWriter bw=new BufferedWriter(pw);//加上緩沖流 7 bw.write("用戶名:admin;密碼:123"); 8 bw.flush(); 9 so.shutdownOutput();//關閉輸出流 10 //3.獲取輸入流,得到服務端的響應信息 11 InputStream is=so.getInputStream(); 12 InputStreamReader isr=new InputStreamReader(is); 13 BufferedReader br=new BufferedReader(isr); 14 String info=null; 15 while((info=br.readLine())!=null){ 16 System.out.println("我是客戶端,服務器說:"+info); 17 } 18 19 20 //4.關閉資源 21 bw.close(); 22 pw.close(); 23 os.close(); 24 so.close();
運行結果:
四、使用多線程實現多客戶端的通信
應用多線程來實現服務器與多客戶端之間的通信。<關於多線程更多的內容以后再總結>
多線程基本步驟:
1.服務器端創建ServerSocket,循環調用accept()等待客戶端連接。
2.客戶端創建一個socket並請求和服務器端連接。
3.服務器端接收客戶端請求,創建socket與該客戶建立專線連接。
4.建立連接的兩個socket在一個單獨的線程上對話。
5.服務器端繼續等待新的連接。
------------------------------------------------------------------------------
下面再將上述的例子修改成多線程的通信
新建一個服務器線程處理類ServerThread,該類繼承Thread類:
1 /* 2 * 服務器線程處理類 3 */ 4 public class ServerThread extends Thread { 5 // 和本線程相關的Socket 6 Socket so = null; 7 8 public ServerThread(Socket socket) {// 初始化與本線程相關的Socket 9 so = socket; 10 } 11 12 // 線程執行的操作,響應客戶端的請求 13 public void run() {// 重寫父類的run方法 14 InputStream is = null; 15 InputStreamReader isr = null; 16 BufferedReader br = null; 17 OutputStream os = null; 18 PrintWriter pw = null; 19 BufferedWriter bw = null; 20 try { 21 // 3.獲取一個輸入流,並讀取客戶端信息 22 is = so.getInputStream();// 字節輸入流 23 isr = new InputStreamReader(is);// 將字節輸入流包裝成字符輸入流 24 br = new BufferedReader(isr);// 加上緩沖流,提高效率 25 String info = null; 26 while ((info = br.readLine()) != null) {// 循環讀取客戶端信息 27 System.out.println("我是服務器,客戶端說:" + info); 28 29 } 30 so.shutdownInput();// 關閉輸入流 31 // 4.獲取一個輸出流,向客戶端輸出信息,響應客戶端的請求 32 os = so.getOutputStream();// 字節輸出流 33 pw = new PrintWriter(os);// 字符輸出流 34 bw = new BufferedWriter(pw);// 緩沖輸出流 35 bw.write("歡迎您!"); 36 bw.newLine(); 37 bw.flush(); 38 39 } catch (IOException e) { 40 // TODO Auto-generated catch block 41 e.printStackTrace(); 42 } finally { 43 // 5.關閉資源 44 try { 45 if (os != null) 46 os.close(); 47 if (pw != null) 48 pw.close(); 49 if (bw != null) 50 bw.close(); 51 if (br != null) 52 br.close(); 53 if (isr != null) 54 isr.close(); 55 if (is != null) 56 is.close(); 57 if (!so.isClosed()) 58 so.close(); 59 } catch (IOException e) { 60 // TODO Auto-generated catch block 61 e.printStackTrace(); 62 } 63 } 64 65 } 66 }
<解 惑>關閉資源為何要加一個判斷條件:不為空 ???
<回 答>這是一種正確、嚴謹的寫法。 驗證非NULL是編碼中很重要的一環。假如本來就是NULL,這是調用各自的close()方法是會報錯的。 如果在實例化這些對象時出錯導致這些對象為NULL,或是實例化沒問題但中途出了什么異常導致這些對象為NULL,都會在未經驗證非NULL前嘗試調用close()方法關閉時報錯。
<提 示>集中異常處理的快捷鍵:Alt+shift+z
服務器端:
1 try { 2 //1.創建一個服務器端的Socket,即ServerSocket,指定綁定的端口 3 ServerSocket ss= new ServerSocket(8888); 4 5 System.out.println("服務器即將啟動,等待客戶端的連接..."); 6 Socket so=null; 7 //記錄客戶端的數量 8 int count=0; 9 //循環偵聽等待客戶端的連接 10 while(true){ 11 //2.調用accept方法開始監聽,等待客戶端的連接 12 so=ss.accept();//accept方法返回Socket實例 13 //創建一個新的線程 14 ServerThread st=new ServerThread(so); 15 //啟動線程,執行與客戶端的交互 16 st.start();//注意是start不是run 17 count++; 18 System.out.println("此時客戶端數量為:"+count); 19 InetAddress add=so.getInetAddress(); 20 System.out.println("當前客戶端的ip地址為"+add.getHostAddress()); 21 } 22 } catch (IOException e) { 23 // TODO Auto-generated catch block 24 e.printStackTrace(); 25 }
多個客戶端的運行結果:
這樣一個簡單的多線程通信就完成了,這個多線程還不是並行操作的,要實現並行操作可以按照下面的思路:
主線程負責創建socket
* 一個線程用來讀取
* 一個線程用來寫入,用兩個內部類
* 要用多線程實現,send線程和recived線程同時訪問buff數據區。
* 發送完成后notify,接收線程。接收線程自身wait
* 接收的說,我先wait一下,可能緩存中還沒有數據,我會拿到空值得。
* 發送的說Ok,我先寫,寫完了我notifyall你。我就一直鎖着,這樣默契的合作了。
變成並行處理了 每個客戶端連接服務端都會產生一個新的socket。
< 具體代碼還沒寫,大家可以自行摸索。。。寫完了再更新>
---------------點擊查看更多關於Socket信息------------------