Android學習筆記49:Socket編程實現簡易聊天室


  在之前的博文中,我們學習了在Android開發中,如何使用標准Java接口HttpURLConnection和Apache接口HttpClient進行HTTP通信。

  本篇博文將主要對Socket進行介紹,並通過Socket編程實現一個簡易聊天室的案例。

 

1.Socket基礎知識

  Socket(套接字)用於描述IP地址和端口,是通信鏈的句柄,應用程序可以通過Socket向網絡發出請求或者應答網絡請求。

  Socket是支持TCP/IP協議的網絡通信的基本操作單元,是對網絡通信過程中端點的抽象表示,包含了進行網絡通信所必需的5種信息:連接所使用的協議、本地主機的IP地址、本地進程的協議端口、遠地主機的IP地址以及遠地進程的協議端口。

1.1 Socket的傳輸模式

  Socket有兩種主要的操作方式:面向連接的和無連接的。

  面向連接的Socket操作就像一部電話,Socket必須在發送數據之前與目的地的Socket取得連接,一旦連接建立了,Socket就可以使用一個流接口進行打開、讀寫以及關閉操作。並且,所有發送的數據在另一端都會以相同的順序被接收。

  無連接的Socket操作就像一個郵件投遞,每一個數據報都是一個獨立的單元,它包含了這次投遞的所有信息(目的地址和要發送的內容)。在這個模式下的Socket不需要連接目的地Socket,它只是簡單的投出數據報。

  由此可見,無連接的操作是快速高效的,但是數據安全性不佳;面向連接的操作效率較低,但數據的安全性較好。

  本文主要介紹的是面向連接的Socket操作。

1.2 Socket的構造方法

  Java在包java.net中提供了兩個類Socket和ServerSocket,分別用來表示雙向連接的Socket客戶端和服務器端。

  Socket的構造方法如下:

  (1)Socket(InetAddress address, int port);

  (2)Socket(InetAddress address, int port, boolean stream);

  (3)Socket(String host, int port);

  (4)Socket(String host, int port, boolean stream);

  (5)Socket(SocketImpl impl);

  (6)Socket(String host, int port, InetAddress localAddr, int localPort);

  (7)Socket(InetAddress address, int port, InetAddrss localAddr, int localPort);

  ServerSocket的構造方法如下:

  (1)ServerSocket(int port);

  (2)ServerSocket(int port, int backlog);

  (3)ServerSocket(int port, int backlog, InetAddress bindAddr);

  其中,參數address、host和port分別是雙向連接中另一方的IP地址、主機名和端口號;參數stream表示Socket是流Socket還是數據報Socket;參數localAddr和localPort表示本地主機的IP地址和端口號;SocketImpl是Socket的父類,既可以用來創建ServerSocket,也可以用來創建Socket。

  如下的代碼在服務器端創建了一個ServerSocket:

1   try {
2   ServerSocket serverSocket = new ServerSocket(50000);    //創建一個ServerSocket,用於監聽客戶端Socket的連接請求
3       while(true) {
4           Socket socket = serverSocket.accept();        //每當接收到客戶端的Socket請求,服務器端也相應的創建一個Socket
5       //todo開始進行Socket通信
6       }
7   }catch (IOException e) {
8       e.printStackTrace();
9   }

  其中,50000是我們自己選擇的用來進行Socket通信的端口號,在創建Socket時,如果該端口號已經被別的服務占用,將會拋出異常。

  通過以上的代碼,我們創建了一個ServerSocket在端口50000監聽客戶端的請求。accept()是一個阻塞函數,就是說該方法被調用后就會一直等待客戶端的請求,直到有一個客戶端啟動並請求連接到相同的端口,然后accept()返回一個對應於該客戶端的Socket。

  那么,如何在客戶端創建並啟動一個Socket呢?

1   try {
2       socket = new Socket("192.168.1.101", 50000);    //192.168.1.101是服務器的IP地址,50000是端口號
3     //todo開始進行Socket通信
4   } catch (IOException e) {
5       e.printStackTrace();
6 }

   至此,客戶端和服務器端都建立了用於通信的Socket,接下來就可以由各自的Socket分別打開各自的輸入流和輸出流進行通信了。

1.3輸入流和輸出流

  Socket提供了方法getInputStream()和getOutPutStream()來獲得對應的輸入流和輸出流,以便對Socket進行讀寫操作,這兩個方法的返回值分別是InputStream和OutPutStream對象。

  為了便於讀寫數據,我們可以在返回的輸入輸出流對象上建立過濾流,如PrintStream、InputStreamReader和OutputStreamWriter等。

1.4關閉Socket

  可以通過調用Socket的close()方法來關閉Socket。在關閉Socket之前,應該先關閉與Socket有關的所有輸入輸出流,然后再關閉Socket。

 

2.簡易聊天室

  下面就來說說如何通過Socket編程實現一個簡易聊天室。客戶端完成后的運行效果如圖1所示。

  圖1 運行效果

  在該客戶端的界面中,使用了一個TextView控件來顯示聊天記錄。為了方便查看,將兩個用戶也放到了一個界面中,實際上應該啟動兩個模擬器,分別作為兩個用戶的客戶端,此處是為了方便操作才這么做的。

2.1服務器端ServerSocket的實現

  在該實例中,我們在MyEclipse中新建了一個Java工程作為服務器端。在該Java工程中,我們應該完成以下的操作。

  (1)指定端口實例化一個ServerSocket,並調用ServerSocket的accept()方法在等待客戶端連接期間造成阻塞。

  (2)每當接收到客戶端的Socket請求時,服務器端也相應的創建一個Socket,並將該Socket存入ArrayList中。與此同時,啟動一個ServerThread線程來為該客戶端Socket服務。

  以上兩步操作,可以通過以下的代碼來實現:

 1   /*
 2    * Class    :   MyServer類,用於監聽客戶端Socket連接請求
 3    * Author   :   博客園-依舊淡然
 4    */
 5   public class MyServer {
 6       
 7       //定義ServerSocket的端口號
 8       private static final int SOCKET_PORT = 50000;
 9       //使用ArrayList存儲所有的Socket
10       public static ArrayList<Socket> socketList = new ArrayList<Socket>();
11   
12       public void initMyServer() {
13           try {
14         //創建一個ServerSocket,用於監聽客戶端Socket的連接請求
15               ServerSocket serverSocket = new ServerSocket(SOCKET_PORT);
16               while(true) {
17                   //每當接收到客戶端的Socket請求,服務器端也相應的創建一個Socket
18                   Socket socket = serverSocket.accept();
19                   socketList.add(socket);
20                   //每連接一個客戶端,啟動一個ServerThread線程為該客戶端服務
21                   new Thread(new ServerThread(socket)).start();
22               }
23           }catch (IOException e) {
24               e.printStackTrace();
25           }
26       }
27       
28       public static void main(String[] args) {
29           MyServer myServer = new MyServer();
30           myServer.initMyServer();
31       }
32   }

   (3)在啟動的ServerThread線程中,我們需要將讀到的客戶端內容(也就是某一個客戶端Socket發送給服務器端的數據),發送給其他的所有客戶端Socket,實現信息的廣播。ServerThread類的具體實現如下:

 1   public class ServerThread implements Runnable {
 2   
 3       //定義當前線程所處理的Socket
 4       private Socket socket = null;
 5       //該線程所處理的Socket對應的輸入流
 6       private BufferedReader bufferedReader = null;
 7       
 8       /*
 9        * Function  :    ServerThread的構造方法
10        * Author    :    博客園-依舊淡然
11        */
12       public ServerThread(Socket socket) throws IOException {
13           this.socket = socket;
14           //獲取該socket對應的輸入流
15           bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
16       }
17       
18       /*
19        * Function  :    實現run()方法,將讀到的客戶端內容進行廣播
20        * Author    :    博客園-依舊淡然
21        */
22       public void run() {
23           try {
24               String content = null;
25               //采用循環不斷地從Socket中讀取客戶端發送過來的數據
26               while((content = bufferedReader.readLine()) != null) {
27                   //將讀到的內容向每個Socket發送一次
28                   for(Socket socket : MyServer.socketList) {
29                       //獲取該socket對應的輸出流
30                       PrintStream printStream = new PrintStream(socket.getOutputStream());
31                       //向該輸出流中寫入要廣播的內容
32                       printStream.println(packMessage(content));
33                       
34                   }
35               }
36           } catch(IOException e) {
37               e.printStackTrace();
38           }
39       }
40       
41       /*
42        * Function  :    對要廣播的數據進行包裝
43        * Author    :    博客園-依舊淡然
44        */
45       private String packMessage(String content) {
46           String result = null;
47           SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");    //設置日期格式
48           if(content.startsWith("USER_ONE")) {
49               String message = content.substring(8);        //獲取用戶發送的真實的信息
50               result = "\n" + "往事如風  " + df.format(new Date()) + "\n" + message;
51           }
52           if(content.startsWith("USER_TWO")) {
53               String message = content.substring(8);        //獲取用戶發送的真實的信息
54               result = "\n" + "依舊淡然  " + df.format(new Date()) + "\n" + message;
55           }
56           return result;
57       }
58   
59   }

   其中,在packMessage()方法中,我們對要廣播的數據進行了包裝。因為要分辨出服務器接收到的消息是來自哪一個客戶端Socket的,我們對客戶端Socket發送的消息也進行了包裝,方法是在消息的頭部加上"USER_ONE"來代表用戶"往事如風",在消息的頭部加上"USER_TWO"來代表用戶"依舊淡然"。 

  至此,服務器端的ServerSocket便算是創建好了。

2.2客戶端Socket的實現

  接下來,我們便可以在Android工程中,分別為用戶"往事如風"和"依舊淡然"創建一個客戶端Socket,並啟動一個客戶端線程ClientThread來監聽服務器發來的數據。

  這一過程的具體實現如下:

 1     /*
 2      * Function   :   初始化Socket
 3      * Author     :   博客園-依舊淡然
 4      */
 5     private void initSocket() {
 6         try {
 7             socketUser1 = new Socket(URL_PATH, SOCKET_PORT);            //用戶1的客戶端Socket
 8             socketUser2 = new Socket(URL_PATH, SOCKET_PORT);            //用戶2的客戶端Socket
 9             clientThread = new ClientThread();        //客戶端啟動ClientThread線程,讀取來自服務器的數據
10             clientThread.start();
11         } catch (IOException e) {
12             e.printStackTrace();
13         }        
14     }

   ClientThread的具體實現和服務器端的ServerThread線程相似,唯一的區別是,在ClientThread線程中接收到服務器端發來的數據后,我們不可以直接在ClientThread線程中進行刷新UI的操作,而是應該將數據封裝到Message中,再調用MyHandler對象的sendMessage()方法將Message發送出去。這一過程的具體實現如下:

 1     /*
 2      * Function   :   run()方法,用於讀取來自服務器的數據
 3      * Author     :   博客園-依舊淡然
 4      */
 5   public void run() {
 6   try {
 7           String content = null;
 8           while((content = bufferedReader .readLine()) != null) {
 9               Bundle bundle = new Bundle();
10               bundle.putString(KEY_CONTENT, content);
11               Message msg = new Message();
12               msg.setData(bundle);            //將數據封裝到Message對象中
13               myHandler.sendMessage(msg);
14           }
15       } catch (Exception e) {
16           e.printStackTrace();
17       }
18   }

   最后,我們在UI主線程中創建一個內部類MyHandler,讓它繼承Handler類,並實現handleMessage()方法,用來接收Message消息並處理(刷新UI)。MyContent是一個用來保存聊天記錄的類,提供了get和set接口,其中,set接口設置的本條聊天記錄,而get接口獲得的是全部的聊天記錄。具體的實現如下:

 1     /*
 2      * Class      :   內部類MyHandler,用於接收消息並處理
 3      * Author     :   博客園-依舊淡然
 4      */
 5     private class MyHandler extends Handler {
 6         public void handleMessage(Message msg) {
 7             Bundle bundle = msg.getData();            //獲取Message中發送過來的數據
 8             String content = bundle.getString(KEY_CONTENT);
 9             MyContent.setContent(content);            //保存聊天記錄
10             mTextView.setText(MyContent.getContent());
11         }
12     }

   至此,客戶端的Socket也編寫完成了。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM