人的一生總要去書寫許多不知結局的故事。有的故事可以順利完成,而有的故事或許將永遠都是一種殘缺。完美的結局是我們搏擊風雨的見證,也是我們永久的期待,然而生活並不像我們所想像的那樣美好,既然有年輕,也必然有衰老。
簡易聊天室,什麼是聊天室呢,簡單一點說就是一些人可以共同聊天,別人能夠看見你發布的消息,你也可以看到別人的消息,大家的消息是公開的。
功能分析:
1、聊天功能,聊天是一個長時間的相互交互的過程,要實現長時間連接Socket是一個比較不錯的選擇。
2、一些人相互聊天功能,要想實現相互聊天,就要將消息轉發給所有建立連接的人,這裡就要進行消息轉發。
實現思路:
利用Socket實現客戶端和服務端長連接,每次將連接進來的Socket保存到一個集合當中,當其中一個Socket有接收到數據的時候,將接收到的數據轉發給其他的Socket,這樣既能實現聊天功能又能實現轉發。
對Socket的操作應放在子線程當中,每一次有新的Socket連接進來之後就新開一個子線程。
如果對Socket不是很了解,推薦【JavaEE】Socket簡單體驗, TCP和UDP這篇文章進行了解。
服務端搭建
首先要實現監聽客戶端連接,需要利用ServerSocket的accept方法進行監聽,但是該方法會將程序阻塞,所以該監聽過程要放在子線程中完成。這裡定義的線程為ServerListener。
其次每監聽到一個Socket,Socket無論是獲取還是傳輸過程都是對字節流進行操作,這是一個比較耗時的操作,所以也要放在子線程中操作。這裡定義的線程為ChatSocket。
最後還要定義一個Socket的子線程管理類,方便對所有連接進來的Socket的子線程進行管理,例如對數據的轉發。這裡定義的管理類為ChatManager。
在程序最開始運行的時候,要開啟對Socket連接的監聽,所以在main方法中開始監聽線程。
public static void main(String[] args) { // 開啟服務端監聽 new ServerListener().start(); }
開啟監聽線程之後,當監聽到有Socket進行連接的時候,不僅要新建一個子線程實現該Socket的操作,而且要把該Socket添加到Socket線程管理類ChatManager當中,方便後期管理。
public class ServerListener extends Thread { @Override public void run() { try { // 1、創建ServerScoket,設置埠 ServerSocket serverSocket = new ServerSocket(12345); while (true) { // 2、accept方法將導致程序阻塞 Socket socket = serverSocket.accept(); JOptionPane.showMessageDialog(null, "有客戶端連接到本機的12345埠"); // 3、將socket傳遞給新線程 ChatSocket cs = new ChatSocket(socket); cs.start(); // 4、使用Chatmanager進行管理 ChatManager.getInstance().add(cs); } } catch (IOException e) {e.printStackTrace();} } }
在ChatSocket新線程中,要實現對接收到的Socket進行處理,例如獲取Socket客戶端發送過來的數據進行轉發等。而轉發過程,是轉發給所有連接進來的Socket,所以轉發過程的實現將由ChatManager完成。
ChatSocket在一開啟線程的時候,就要進行客戶端數據獲取,並轉發。
ChatSocket要能夠完成服務端想客戶端發送數據的過程。
public class ChatSocket extends Thread { private Socket socket; public ChatSocket(Socket socket) { this.socket = socket; } // 服務端傳值給客戶端 public void out(String out) { try { if (socket.isConnected() && !socket.isClosed()) { // 獲取當前Socket輸出流,輸出數據 socket.getOutputStream().write(out.getBytes("gbk")); System.out.println("轉發數據**********" + out); } else { // 連結已關閉 ChatManager.getInstance().remove(this); } } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { try { if (socket.isConnected() && !socket.isClosed()) { // 接受客戶端數據 BufferedReader br = new BufferedReader(new InputStreamReader( socket.getInputStream(), "gbk")); // 讀取數據 String line = null; while ((line = br.readLine()) != null) { // 轉發數據 ChatManager.getInstance().publish(this, line); System.out.println("接受數據******" + line); } br.close(); } else { // 連結已關閉 ChatManager.getInstance().remove(this); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
最後來說說Socket線程管理類ChatManager,一個聊天室只能有一個Socket線程管理類,所以該類要使用單例模式。
private static ChatManager cm; // 單例 public static ChatManager getInstance() { if (cm == null) { cm = new ChatManager(); } return cm; }
提供Socket子線程存放集合,能夠實現Socket子線程添加和移除功能。
// ChatSocket集合 Vector vector = new Vector(); // 添加ChatSocket public void add(ChatSocket chatSocket) { vector.add(chatSocket); } // 移除ChatSocket public void remove(ChatSocket chatSocket) { vector.remove(chatSocket); }
最後是藉助Socket子線程存放集合,實現數據轉發功能。
// 發送消息 public void publish(ChatSocket cs, String out) { for (int i = 0; i
Android端搭建
對於Android客戶端而言就顯得稍微簡單一些,只需要實現數據的接受和傳遞即可。
首先是界面的構建:
客戶端界面當點擊連接的時候,客戶端會向服務端發送連接請求,請求成功之後便可發送消息,聊天內容將會在界面中間部分顯示。
1、初始化信息,在Activity中完成對控制項的初始化和點擊事件監聽。
// 定義控制項全局變量 private TextView ipTv, contentTv; private Button linkBtn, sendBtn; private EditText sendEd; // 初始化控制項 private void initView() { ipTv = (TextView) findViewById(R.id.tv_ip); contentTv = (TextView) findViewById(R.id.tv_content); linkBtn = (Button) findViewById(R.id.btn_link); linkBtn.setOnClickListener(this); sendBtn = (Button) findViewById(R.id.btn_send); sendBtn.setOnClickListener(this); sendEd = (EditText) findViewById(R.id.ed_send); } // 點擊事件監聽 @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_link:// 連結 connect(); break; case R.id.btn_send:// 發送 send(); break; } }
2、連接服務端,並獲取服務端傳遞數據。
// 定義三個全局變量 private Socket socket; private BufferedReader reader; private BufferedWriter writer; // 連接Socket服務端 private void connect() { final String ip = ipTv.getText().toString().trim(); final int port = 12345; // 異步執行 AsyncTask asyncTask = new AsyncTask() { @Override protected Void doInBackground(Void... params) { try { // 實例化Socket socket = new Socket(ip, port); reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); publishProgress("success"); // 讀取傳遞過來的數據 String line; while ((line = reader.readLine()) != null) { publishProgress(line); } } catch (IOException e) { publishProgress("failed"); e.printStackTrace(); } return null; } @Override protected void onProgressUpdate(String... values) { if ("success".equals(values[0])) Toast.makeText(MainActivity.this, "連接成功", Toast.LENGTH_LONG).show(); else if ("failed".equals(values[0])) Toast.makeText(MainActivity.this, "無法建立連接", Toast.LENGTH_LONG).show(); else contentTv.append("他說:" + values[0] + "\n"); super.onProgressUpdate(values); } }; asyncTask.execute(); }
注意連接服務端和都去服務端數據是一個耗時的操作,所以應放在子線程中完成,這裡是採用異步AsyncTask的方式來進行處理。
3、發送數據
// 向Socket服務端發送 private void send() { String out = sendEd.getText().toString().trim(); // 發送數據 try { writer.write(out + "\n"); writer.flush(); sendEd.setText(""); contentTv.append("我說:" + out + "\n"); } catch (IOException e) { e.printStackTrace(); } }
到這裡就已經全部完成了,包括服務端和Android客戶端的實現,在實際開發當中往往是通過構建一些比較成熟的框架來實現這一過程,實現原理大致相同,實現過程有所不同。
Github後臺代碼地址[https://github.com/zrunker/SocketChatRoomDemo]
Github安卓客戶端代碼地址[https://github.com/zrunker/MySocketClient]