我用草圖畫了一下及基本流程為:
原理與思路:
使用TCP面向連接的套接字來建立服務端和客戶端兩個IP地址端點之間的會畫。
服務器端主要完成用戶信息的存儲、客戶端命令的響應與接收、信息的轉發等功能。
客戶端主要完成登陸、聊天信息的接收以及發送信息等功能。客戶端必須在服務器啟動后才能連接成功,用戶登陸以后聊天室的客戶端會顯示用戶已經登陸,可以與服務器進行即使聊天。
服務器端設計
服務器端是整個聊天室的核心部分,它涵蓋了客戶端的加入、客戶端請求如何處理等一系列功能,客戶發送的信息也是通過服務器端發送給其他用戶的。
系統的安全性
服務器的安全包括兩個部分,一是服務器本身軟件和硬件上的安全性,比如防止安全漏洞;而是客戶和服務器通訊協議的安全性設計,防止通過協議本身非法攻擊服務器。
系統並發服務能力
多線程並發處理可以使CPU效率達到最高,舉個例子來說,網絡的數據傳送速率遠遠低於CPU的處理能力,本地文件系統資源的讀寫速度也遠遠低於CPU的處理能力,在傳統的單線程環境中,在一個線程程序中如果出現阻塞則整個程序都可以能停止運行,而在一個多線程的程序中就不會出現這樣的問題。當一個線程阻塞時,別的線程依然可以大大地提高CPU的效率。
服務器端主要類的定義
SerFrame類是服務器端程序的入口,其中包括一個主窗體、若干個容器、按鈕和文本框。Init()方法是使整個圖形界面初始化。同時還有ServerStart() 方法和ServerStop()兩個方法,當運行ServerStart()方法時就會創建一個ServerSocket()對象並設定1001端口號,關閉聊天界面會自動斷開連接。
serConnect類集成了Thread線程類,並重寫了父類的main()方法。方法中的run()方法一直處於監聽狀態,直到返回值為false時,也就是服務器關閉時。控制輸入輸出流來顯示在線信息,並發送到用戶管理的設計。
服務器端用戶管理的設計
通過定義UesrInfoList和Node這兩個類來實現服務器管理用戶功能,當服務器接收到一個客戶端的請求,服務器就會初始化一個Node節點作為客戶端,Node類中包括輸入輸出、用戶名等必要的屬性,還定義了其它自身引用NodeNext,其作用是將客戶端設計成鏈接表,通過這個屬性讓他們互相連接,這樣設計的優點是不需要再定義Map或者List來存放客戶端結點,只要在Node中增加一個屬性就能夠實現,從而提高性能。UserInfoList中定義了對Node結點查找、增加、刪除等方法,無論是發送信息、上線、下線和私聊都是調用UserInfoList中的方法來確定對哪個客戶端進行操作的。Node對象是存放在內存中的,當ServerSocket關閉的時候會釋放資源,Node結點失效。
服務器端消息顯示設計
服務器端是所有消息中轉戰和系統消息發出站。在客戶端Node類定義兩個屬性,分別是ObjectOutputStream和ObjectIntputStream,這是java語言的輸出輸入流,應用所有所有信息的傳遞。首先ServerListenerThread會捕獲到客戶端的請求,然后引用ChatRoom類並調用ObjectOutStream方法發送消息。如果是群聊,則調用SendMessage中的SendMsgToAll發送到每一個客戶端,並調用UserInfoList來獲取所有客戶。客戶端獲得消息后調用服務器中定義的Node類中的OutputStream將接受的顯示到圖形界面中的文本輸出框。若果是私聊,UserInfoList會查找到是哪個用戶,再調用SendMessage方法將消息發送到指定客戶端。
客服端設計
客戶端中實現了客戶界面的顯示以及與服務器端的數據交換,包括接收和發送信息。它繼承了Thread線程類,因而多個客戶端可以並發執行。
客戶端設計准則
客戶端作為面對普通的群體,其作用是非常重要的,它的權限沒有服務器高。在客戶端的設計中只要加入客戶端啟動界面和發送、接收信息的線程以及一些對基本字符串的簡單功能,其他復雜的功能都由服務器來完成。
客戶端主要類的定義
ChatClient類中定義了客戶端的主要函數,login方法是用於用於登陸的 ,不需要事先注冊,用戶名不為空即可登陸。另外,還要logout下線以及發送信息的方法。客戶端的主界面的初始化寫在inti方法中,和服務器端界面類似,它由一個panel和若干個按鈕和textArea組成,這些Swing組件與服務器端的監聽器相連。
客戶端登陸設計
程序登陸需要驗證,若果用戶不是已經存在的用戶名,則基本的那個路成功。登陸功能中首先要實例化一個Socket,傳入本機的IP地址和連接服務器的端口號,然后congratulationsocket對象獲取輸入流,實例化用戶線程並啟用,若果服務器未啟動則拋出異常,並在主界面中顯示錯誤信息。
客戶端發端發送信息的設計
SendMessage方法實現發送信息,該方法首先獲得要發送的對象和內容,然后把提示信息、聊天方法、聊天對象、聊天內容和表情依次通過輸出流發送給服務器端,發送一條並及時清空一次緩存,服務器會根據接收到的信息作相應的處理。
客戶端顯示收到的消息的設計
ReiMessage方法實現接收消息,該方法中定義了一個while循環,只要用戶沒有斷開與服務器的連接或者下線,則消息一直處於接收狀態。當輸入流接收服務器發送的內容后,用了條件語句判斷是和何種類型的信息,並對其加上相對應的類型提示信息的處理。最后送交主界面進行顯示。
創建服務器主類:
package com.home.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; public class MyServer { // 定義保存所有Socket的集合 public static ArrayList<Socket> socketList = new ArrayList<Socket>(); public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(20000); System.out.println("服務器創建成功!"); System.out.println("等待客戶端的連接。。。"); while (true) { // 此行代碼會阻塞,等待用戶的連接 Socket socket = ss.accept(); System.out.println("有客戶端連接進來!"); socketList.add(socket); // 每當客戶端連接后啟動一條ServerThread線程為該客戶端服務 new Thread(new ServerThread(socket)).start(); } } }
Server:
package com.home.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; public class MyServer { // 定義保存所有Socket的集合 public static ArrayList<Socket> socketList = new ArrayList<Socket>(); public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(20000); System.out.println("服務器創建成功!"); System.out.println("等待客戶端的連接。。。"); while (true) { // 此行代碼會阻塞,等待用戶的連接 Socket socket = ss.accept(); System.out.println("有客戶端連接進來!"); socketList.add(socket); // 每當客戶端連接后啟動一條ServerThread線程為該客戶端服務 new Thread(new ServerThread(socket)).start(); } } }
服務器的線程:
package com.home.server; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; public class ServerThread implements Runnable { // 定義當前線程所處理的Socket private Socket socket = null; // 該線程所處理的Socket所對應的輸入流 BufferedReader br = null; public ServerThread(Socket socket) throws IOException { this.socket = socket; // 初始化該Socket對應的輸入流 br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8")); } @Override public void run() { try { String content = null; // 采用循環不斷從Socket中讀取客戶端發送過來的數據 while ((content = readFromClient()) != null) { // 遍歷socketList中的每個Socket,將讀到的內容向每個Socket發送一次 for (Socket s : MyServer.socketList) { OutputStream os = s.getOutputStream(); os.write((content + "\n").getBytes("utf-8")); } } } catch (Exception e) { e.printStackTrace(); } }
定義讀取客戶端數據的方法:
private String readFromClient() { try { return br.readLine(); } // 如果捕捉到異常,表明該Socket對應的客戶端已經關閉 catch (Exception e) { // 刪除該Socket MyServer.socketList.remove(socket); e.printStackTrace(); } return null; } }
客戶端主體類Activity:
package com.home.activity; import java.io.OutputStream; import java.net.Socket; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import com.home.R; import com.home.util.ClientThread; public class MultiThreadClient extends Activity { private EditText input, show; private Button sendBtn; private OutputStream os; private Handler handler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); input = (EditText) findViewById(R.id.main_et_input); show = (EditText) findViewById(R.id.main_et_show); sendBtn = (Button) findViewById(R.id.main_btn_send); handler = new Handler() { @Override public void handleMessage(Message msg) { // 如果消息來自子線程 if (msg.what == 0x234) { // 將讀取的內容追加顯示在文本框中 show.append("\n" + msg.obj.toString()); } } }; Socket socket; try { socket = new Socket("192.168.0.101", 20000); // 客戶端啟動ClientThread線程不斷讀取來自服務器的數據 new Thread(new ClientThread(socket, handler)).start(); os = socket.getOutputStream(); } catch (Exception e) { e.printStackTrace(); } sendBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { try { // 將用戶在文本框內輸入的內容寫入網絡 os.write((input.getText().toString() + "\r\n").getBytes()); // 清空input文本框數據 input.setText(""); } catch (Exception e) { e.printStackTrace(); } } }); } }