1.Socket基礎知識
Socket(套接字)用于描述IP位址和端口,是通信鍊的句柄,應用程式可以通過Socket向網絡送出請求或者應答網絡請求。
Socket是支援TCP/IP協定的網絡通信的基本操作單元,是對網絡通信過程中端點的抽象表示,包含了進行網絡通信所必需的5種資訊:連接配接所使用的協定、本地主機的IP位址、本地程序的協定端口、遠地主機的IP位址以及遠地程序的協定端口。
1.1 Socket的傳輸模式
Socket有兩種主要的操作方式:面向連接配接的和無連接配接的。(TCP/UDP)
面向連接配接的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也編寫完成了。