Socket编程实现简易聊天室
?
1.Socket基礎(chǔ)知識(shí)
Socket(套接字)用于描述IP地址和端口,是通信鏈的句柄,應(yīng)用程序可以通過(guò)Socket向網(wǎng)絡(luò)發(fā)出請(qǐng)求或者應(yīng)答網(wǎng)絡(luò)請(qǐng)求。
Socket是支持TCP/IP協(xié)議的網(wǎng)絡(luò)通信的基本操作單元,是對(duì)網(wǎng)絡(luò)通信過(guò)程中端點(diǎn)的抽象表示,包含了進(jìn)行網(wǎng)絡(luò)通信所必需的5種信息:連接所使用的協(xié)議、本地主機(jī)的IP地址、本地進(jìn)程的協(xié)議端口、遠(yuǎn)地主機(jī)的IP地址以及遠(yuǎn)地進(jìn)程的協(xié)議端口。
1.1?Socket的傳輸模式
Socket有兩種主要的操作方式:面向連接的和無(wú)連接的。(TCP/UDP)
面向連接的Socket操作就像一部電話,Socket必須在發(fā)送數(shù)據(jù)之前與目的地的Socket取得連接,一旦連接建立了,Socket就可以使用一個(gè)流接口進(jìn)行打開(kāi)、讀寫(xiě)以及關(guān)閉操作。并且,所有發(fā)送的數(shù)據(jù)在另一端都會(huì)以相同的順序被接收。
無(wú)連接的Socket操作就像一個(gè)郵件投遞,每一個(gè)數(shù)據(jù)報(bào)都是一個(gè)獨(dú)立的單元,它包含了這次投遞的所有信息(目的地址和要發(fā)送的內(nèi)容)。在這個(gè)模式下的Socket不需要連接目的地Socket,它只是簡(jiǎn)單的投出數(shù)據(jù)報(bào)。
由此可見(jiàn),無(wú)連接的操作是快速高效的,但是數(shù)據(jù)安全性不佳;面向連接的操作效率較低,但數(shù)據(jù)的安全性較好。
本文主要介紹的是面向連接的Socket操作。
1.2?Socket的構(gòu)造方法
Java在包java.net中提供了兩個(gè)類Socket和ServerSocket,分別用來(lái)表示雙向連接的Socket客戶端和服務(wù)器端。
Socket的構(gòu)造方法如下:
(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的構(gòu)造方法如下:
(1)ServerSocket(int?port);
(2)ServerSocket(int?port,?int?backlog);
(3)ServerSocket(int?port,?int?backlog,?InetAddress?bindAddr);
其中,參數(shù)address、host和port分別是雙向連接中另一方的IP地址、主機(jī)名和端口號(hào);參數(shù)stream表示Socket是流Socket還是數(shù)據(jù)報(bào)Socket;參數(shù)localAddr和localPort表示本地主機(jī)的IP地址和端口號(hào);SocketImpl是Socket的父類,既可以用來(lái)創(chuàng)建ServerSocket,也可以用來(lái)創(chuàng)建Socket。
如下的代碼在服務(wù)器端創(chuàng)建了一個(gè)ServerSocket:
1 try { 2 ServerSocket serverSocket = new ServerSocket(50000); //創(chuàng)建一個(gè)ServerSocket,用于監(jiān)聽(tīng)客戶端Socket的連接請(qǐng)求 3 while(true) { 4 Socket socket = serverSocket.accept(); //每當(dāng)接收到客戶端的Socket請(qǐng)求,服務(wù)器端也相應(yīng)的創(chuàng)建一個(gè)Socket 5 //todo開(kāi)始進(jìn)行Socket通信 6 } 7 }catch (IOException e) { 8 e.printStackTrace(); 9 }其中,50000是我們自己選擇的用來(lái)進(jìn)行Socket通信的端口號(hào),在創(chuàng)建Socket時(shí),如果該端口號(hào)已經(jīng)被別的服務(wù)占用,將會(huì)拋出異常。
通過(guò)以上的代碼,我們創(chuàng)建了一個(gè)ServerSocket在端口50000監(jiān)聽(tīng)客戶端的請(qǐng)求。accept()是一個(gè)阻塞函數(shù),就是說(shuō)該方法被調(diào)用后就會(huì)一直等待客戶端的請(qǐng)求,直到有一個(gè)客戶端啟動(dòng)并請(qǐng)求連接到相同的端口,然后accept()返回一個(gè)對(duì)應(yīng)于該客戶端的Socket。
那么,如何在客戶端創(chuàng)建并啟動(dòng)一個(gè)Socket呢?
1 try { 2 socket = new Socket("192.168.1.101", 50000); //192.168.1.101是服務(wù)器的IP地址,50000是端口號(hào) 3 //todo開(kāi)始進(jìn)行Socket通信 4 } catch (IOException e) { 5 e.printStackTrace(); 6 }? 至此,客戶端和服務(wù)器端都建立了用于通信的Socket,接下來(lái)就可以由各自的Socket分別打開(kāi)各自的輸入流和輸出流進(jìn)行通信了。
1.3輸入流和輸出流
Socket提供了方法getInputStream()和getOutPutStream()來(lái)獲得對(duì)應(yīng)的輸入流和輸出流,以便對(duì)Socket進(jìn)行讀寫(xiě)操作,這兩個(gè)方法的返回值分別是InputStream和OutPutStream對(duì)象。
為了便于讀寫(xiě)數(shù)據(jù),我們可以在返回的輸入輸出流對(duì)象上建立過(guò)濾流,如PrintStream、InputStreamReader和OutputStreamWriter等。
1.4關(guān)閉Socket
可以通過(guò)調(diào)用Socket的close()方法來(lái)關(guān)閉Socket。在關(guān)閉Socket之前,應(yīng)該先關(guān)閉與Socket有關(guān)的所有輸入輸出流,然后再關(guān)閉Socket。
?
2.簡(jiǎn)易聊天室
下面就來(lái)說(shuō)說(shuō)如何通過(guò)Socket編程實(shí)現(xiàn)一個(gè)簡(jiǎn)易聊天室。客戶端完成后的運(yùn)行效果如圖1所示。
圖1?運(yùn)行效果
在該客戶端的界面中,使用了一個(gè)TextView控件來(lái)顯示聊天記錄。為了方便查看,將兩個(gè)用戶也放到了一個(gè)界面中,實(shí)際上應(yīng)該啟動(dòng)兩個(gè)模擬器,分別作為兩個(gè)用戶的客戶端,此處是為了方便操作才這么做的。
2.1服務(wù)器端ServerSocket的實(shí)現(xiàn)
在該實(shí)例中,我們?cè)贛yEclipse中新建了一個(gè)Java工程作為服務(wù)器端。在該Java工程中,我們應(yīng)該完成以下的操作。
(1)指定端口實(shí)例化一個(gè)ServerSocket,并調(diào)用ServerSocket的accept()方法在等待客戶端連接期間造成阻塞。
(2)每當(dāng)接收到客戶端的Socket請(qǐng)求時(shí),服務(wù)器端也相應(yīng)的創(chuàng)建一個(gè)Socket,并將該Socket存入ArrayList中。與此同時(shí),啟動(dòng)一個(gè)ServerThread線程來(lái)為該客戶端Socket服務(wù)。
以上兩步操作,可以通過(guò)以下的代碼來(lái)實(shí)現(xiàn):
1 /* 2 * Class : MyServer類,用于監(jiān)聽(tīng)客戶端Socket連接請(qǐng)求 3 * Author : 博客園-依舊淡然 4 */ 5 public class MyServer { 6 7 //定義ServerSocket的端口號(hào) 8 private static final int SOCKET_PORT = 50000; 9 //使用ArrayList存儲(chǔ)所有的Socket 10 public static ArrayList<Socket> socketList = new ArrayList<Socket>(); 11 12 public void initMyServer() { 13 try { 14 //創(chuàng)建一個(gè)ServerSocket,用于監(jiān)聽(tīng)客戶端Socket的連接請(qǐng)求 15 ServerSocket serverSocket = new ServerSocket(SOCKET_PORT); 16 while(true) { 17 //每當(dāng)接收到客戶端的Socket請(qǐng)求,服務(wù)器端也相應(yīng)的創(chuàng)建一個(gè)Socket 18 Socket socket = serverSocket.accept(); 19 socketList.add(socket); 20 //每連接一個(gè)客戶端,啟動(dòng)一個(gè)ServerThread線程為該客戶端服務(wù) 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)在啟動(dòng)的ServerThread線程中,我們需要將讀到的客戶端內(nèi)容(也就是某一個(gè)客戶端Socket發(fā)送給服務(wù)器端的數(shù)據(jù)),發(fā)送給其他的所有客戶端Socket,實(shí)現(xiàn)信息的廣播。ServerThread類的具體實(shí)現(xiàn)如下:
1 public class ServerThread implements Runnable { 2 3 //定義當(dāng)前線程所處理的Socket 4 private Socket socket = null; 5 //該線程所處理的Socket對(duì)應(yīng)的輸入流 6 private BufferedReader bufferedReader = null; 7 8 /* 9 * Function : ServerThread的構(gòu)造方法 10 * Author : 博客園-依舊淡然 11 */ 12 public ServerThread(Socket socket) throws IOException { 13 this.socket = socket; 14 //獲取該socket對(duì)應(yīng)的輸入流 15 bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); 16 } 17 18 /* 19 * Function : 實(shí)現(xiàn)run()方法,將讀到的客戶端內(nèi)容進(jìn)行廣播 20 * Author : 博客園-依舊淡然 21 */ 22 public void run() { 23 try { 24 String content = null; 25 //采用循環(huán)不斷地從Socket中讀取客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù) 26 while((content = bufferedReader.readLine()) != null) { 27 //將讀到的內(nèi)容向每個(gè)Socket發(fā)送一次 28 for(Socket socket : MyServer.socketList) { 29 //獲取該socket對(duì)應(yīng)的輸出流 30 PrintStream printStream = new PrintStream(socket.getOutputStream()); 31 //向該輸出流中寫(xiě)入要廣播的內(nèi)容 32 printStream.println(packMessage(content)); 33 34 } 35 } 36 } catch(IOException e) { 37 e.printStackTrace(); 38 } 39 } 40 41 /* 42 * Function : 對(duì)要廣播的數(shù)據(jù)進(jìn)行包裝 43 * Author : 博客園-依舊淡然 44 */ 45 private String packMessage(String content) { 46 String result = null; 47 SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss"); //設(shè)置日期格式 48 if(content.startsWith("USER_ONE")) { 49 String message = content.substring(8); //獲取用戶發(fā)送的真實(shí)的信息 50 result = "\n" + "往事如風(fēng) " + df.format(new Date()) + "\n" + message; 51 } 52 if(content.startsWith("USER_TWO")) { 53 String message = content.substring(8); //獲取用戶發(fā)送的真實(shí)的信息 54 result = "\n" + "依舊淡然 " + df.format(new Date()) + "\n" + message; 55 } 56 return result; 57 } 58 59 }? 其中,在packMessage()方法中,我們對(duì)要廣播的數(shù)據(jù)進(jìn)行了包裝。因?yàn)橐直娉龇?wù)器接收到的消息是來(lái)自哪一個(gè)客戶端Socket的,我們對(duì)客戶端Socket發(fā)送的消息也進(jìn)行了包裝,方法是在消息的頭部加上"USER_ONE"來(lái)代表用戶"往事如風(fēng)",在消息的頭部加上"USER_TWO"來(lái)代表用戶"依舊淡然"。?
至此,服務(wù)器端的ServerSocket便算是創(chuàng)建好了。
2.2客戶端Socket的實(shí)現(xiàn)
接下來(lái),我們便可以在Android工程中,分別為用戶"往事如風(fēng)"和"依舊淡然"創(chuàng)建一個(gè)客戶端Socket,并啟動(dòng)一個(gè)客戶端線程ClientThread來(lái)監(jiān)聽(tīng)服務(wù)器發(fā)來(lái)的數(shù)據(jù)。
這一過(guò)程的具體實(shí)現(xiàn)如下:
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(); //客戶端啟動(dòng)ClientThread線程,讀取來(lái)自服務(wù)器的數(shù)據(jù) 10 clientThread.start(); 11 } catch (IOException e) { 12 e.printStackTrace(); 13 } 14 }? ClientThread的具體實(shí)現(xiàn)和服務(wù)器端的ServerThread線程相似,唯一的區(qū)別是,在ClientThread線程中接收到服務(wù)器端發(fā)來(lái)的數(shù)據(jù)后,我們不可以直接在ClientThread線程中進(jìn)行刷新UI的操作,而是應(yīng)該將數(shù)據(jù)封裝到Message中,再調(diào)用MyHandler對(duì)象的sendMessage()方法將Message發(fā)送出去。這一過(guò)程的具體實(shí)現(xiàn)如下:
1 /* 2 * Function : run()方法,用于讀取來(lái)自服務(wù)器的數(shù)據(jù) 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); //將數(shù)據(jù)封裝到Message對(duì)象中 13 myHandler.sendMessage(msg); 14 } 15 } catch (Exception e) { 16 e.printStackTrace(); 17 } 18 }? 最后,我們?cè)赨I主線程中創(chuàng)建一個(gè)內(nèi)部類MyHandler,讓它繼承Handler類,并實(shí)現(xiàn)handleMessage()方法,用來(lái)接收Message消息并處理(刷新UI)。MyContent是一個(gè)用來(lái)保存聊天記錄的類,提供了get和set接口,其中,set接口設(shè)置的本條聊天記錄,而get接口獲得的是全部的聊天記錄。具體的實(shí)現(xiàn)如下:
1 /* 2 * Class : 內(nèi)部類MyHandler,用于接收消息并處理 3 * Author : 博客園-依舊淡然 4 */ 5 private class MyHandler extends Handler { 6 public void handleMessage(Message msg) { 7 Bundle bundle = msg.getData(); //獲取Message中發(fā)送過(guò)來(lái)的數(shù)據(jù) 8 String content = bundle.getString(KEY_CONTENT); 9 MyContent.setContent(content); //保存聊天記錄 10 mTextView.setText(MyContent.getContent()); 11 } 12 }? 至此,客戶端的Socket也編寫(xiě)完成了。
轉(zhuǎn)載于:https://www.cnblogs.com/berylqliu/p/6261505.html
總結(jié)
以上是生活随笔為你收集整理的Socket编程实现简易聊天室的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: linq分页扩展
- 下一篇: 四则运算之Right-BICEP测试