標籤:

類QQ聊天程序設計-伺服器篇

一、總體目標分析:

  1. 總的目標是要設計一個類似qq一樣的聊天程序。
  2. 伺服器端界面:可以控制埠的開啟與關閉,可以看到連接伺服器的客戶機的一些詳細信息,包括用戶名、密碼以及登錄地址等。
  3. 伺服器端與客戶端通信:伺服器端要能與客戶端實現一對一的通信,還能實現一對多的「群聊」,還要能實現「踢人」功能。
  4. 客戶端包括登錄界面和聊天界面,在登錄界面需要先註冊才能進行登錄,聊天界面中,可以看到在線好友,可以實現客戶端與客戶端的一對一同信,也可以實現所有客戶端之間的「群聊」,以及與伺服器的通信。
  5. 可以傳輸文件以及圖片。

二、過程分析:

在整個伺服器端的設計中,主要分為四個類:

  • ServerUI.java
  • Server.java
  • ServerThread.java
  • User.java

(1)、ServerUI.java:

這個類主要是用來設計伺服器端的界面。這個類中包括一些基本的界面屬性,如窗體、輸入框、按鈕等。這裡要說明的是含有一個整型的port屬性,這是要從輸入框中獲取,然後傳入到Server.java類中去綁定伺服器;有一個Server類對象server,這個對象在點擊「開啟」按鈕後創建;有三個JTextArea類對象,用於顯示文字,由於在其他類中也可能用到,而且這些對象是唯一的,因此將他們申明為static類型,便於在其他類中直接用類名來調用。

這個類的方法主要是兩個監聽器裡面的方法。在這裡,為按鈕添加監聽器使用了簡寫的形式

//給按鈕添加監聽器,開啟後馬上獲取埠號,開啟相應伺服器jb_start.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { //調用開啟伺服器的方法 startAction(e); }});

  • 在開啟按鈕的監聽事件中,首先要改變按鈕中的文字,要實現既能開啟也能關閉,然後就是開啟對應埠的伺服器,這個過程如下:

首先獲取埠號:

this.port = Integer.parseInt(jt_port.getText());

然後將這個埠號通過創建Server對象的方式傳入Server類中去開啟對應埠的伺服器

server = new Server(port); //創建伺服器類對象server.createServer(); //調用相應的方法創建相應伺服器server.start();

在Server類是一個繼承了線程的類,因此用到了啟動線程的方法這裡放於Server類中講。到這一步,如果不出意外,一個對應埠的伺服器就創建好了,只等著客戶機的連接了。

  • 在發送按鈕的監聽事件中,首先要判斷伺服器是否開啟,只有伺服器開啟才能發送消息。若伺服器開啟,下一步就是要獲取輸入框中的字元串(發送消息當然要知道發送什麼),得到字元串後就要調用方法了,這一步困難了,發送消息方法在處理客戶機連接的線程類中定義(考慮到每個客戶端都要通信),怎麼樣才能調用這個方法能,注意到,每當有客戶機連接上時,就創建一個線程對象去處理,而該線程對象就被放進一個隊列中(該隊列在Server類中定義),因此只要將該隊列定義為static類型即可解決此問題。

for(int i = 0;i<Server.stList.size();i++){ //給每個客戶端都發送該消息Server.stList.get(i).sendMessage(server_input);}

(2)、Server.java:

這個類主要負責創建伺服器、等待客戶端的連接、創建處理客戶端連接的線程對象、將該線程對象加入到隊列中。

在創建伺服器之前要先預存進10個用戶名

static { for (int i = 0; i < 10; i++) { User u = new User("poe" + i, "poe" + i, null); System.out.println("生成用戶名:" + u.getName()); userDB.put(u.getName(), u); // 將生成的用戶名加入資料庫中 }}

這是一段靜態塊代碼,會在程序運行時先執行,且只執行一次,不需要對象來調用

對於伺服器的創建,其實很簡單,只要知道埠只用一個語句即可

// 創建伺服器的方法public void createServer() { try { // 創建對應埠的伺服器 server = new ServerSocket(port); System.out.println("伺服器" + port + "創建成功,等待客戶機的連接"); } catch (IOException e) { e.printStackTrace(); }}

然後就是等待客戶端的連接請求,當程序運行到這裡的時候,系統就會被阻塞在這個地方,直到有客戶機請求成功。當然我們不希望我們的伺服器只連上一個客戶機就再也不能被其他客戶機連接了,所以我們將這個連接請求放在一個死循環中,每當一個客戶機連接上後,就創建一個線程處理對象去處理該客戶機的連接,系統又再次進入等待狀態。死循環這又是一個要考慮的問題,因為我們不能讓我們的程序陷入死循環中,因此只能將其放入線程中。

public void run() {// 讓伺服器進入循環等待 while (true) { try { Socket client = server.accept(); // 當程序運行到這裡是就將進入阻塞狀態,直到有客戶機連上,就再次進入等待 System.out.println("客戶機連接上,等待驗證用戶名和密碼"); // 每當有客戶機連接上後,就開啟一個線程,將對客戶機的處理交由線程去處理 ServerThread st = new ServerThread(client); st.start(); // 每創建一個線程對象,就將該線程對象加入隊列中 if (client.isBound()) { stList.add(st); // 當客戶端與伺服器連接上時,就將他加入隊列 } ServerUI.area_num.setText("" + stList.size()); } catch (Exception e) { e.printStackTrace(); } } }}

(3)、ServerThread.java

這個類主要使用來處理客戶機的連接,包括收發消息,因此這個類的一個對象就相當於一個客戶機。在這個類中,接收消息的方法也存在延遲,就好像系統會一直停在那,直到客戶機發來消息。同樣將這個方法放於一個線程中,置於死循環轉態。

發送消息主要用到輸出流的write()方法

// 伺服器發送消息的方法 public void sendMessage(String mesg) { mesg += "
"; System.out.println("伺服器要發送的消息是:" + mesg); byte[] data = mesg.getBytes(); // 獲取要發送字元串的位元組數組 try { ous.write(data);// 發送位元組數組 } catch (IOException e) { e.printStackTrace(); } }

接收消息用到readLine()方法

br = new BufferedReader(new InputStreamReader(ins));String recieve = br.readLine();// 獲得收到的消息

若要進行賬號和密碼的判斷,就可以在這個方法中進行,在這個程序中,我預先存入了10個用戶,暫時只能用這10個用戶名和密碼登錄。在這裡可以先設置一個整型的計數器count(全局變數),用它來記錄伺服器收到的消息的條數。因為使用的是readLine()方法接收消息,因此伺服器相當於每次接受的是一條消息,沒接受到一條消息count自動加1,利用消息發來的序號來判斷該消息是用戶名還是密碼或是聊天消息。

if (count == 1) { // 第一條為用戶名 user.setName(recieve); System.out.println("客戶機輸入的用戶名是:" + recieve);}if (count == 2) { // 第二條為密碼 user.setPass(recieve); System.out.println("客戶機輸入的密碼是:" + recieve); if(!User.checkUser(user)){ closeClient(); //用戶名不合法就關閉與伺服器的連接 return; } Server.userDB.get(user.getName()).setOnline(true); //設置標誌為true,表示該用戶已上線,禁止其他人再次使用該用戶登錄 ServerUI.area_recive.append(user.getName()+"上線
"
);}if (count >= 3) { //第二條以後的是聊天信息,顯示在聊天板上 recieve = user.getName() +":"+ recieve + "
"
; ServerUI.area_recive.append(recieve); // 將接收到的消息加入到消息顯示框中 }

(4)、User.java

這是一個用戶類,這個類比較簡單,主要是將用戶的信息打包,包括用戶名、密碼、登錄地址等一些信息。在這個方法中還定義了一個判斷用戶是否合法的static類型的方法,該方法用於客戶端登錄時,對客戶端發來的用戶名和密碼進行驗證,只有用戶名和密碼都正確才能進行聊天。

// 判斷用戶名是否合法public static boolean checkUser(User u) { if (Server.userDB.containsKey(u.getName())) {// 用戶資料庫中包含該用戶名的用戶 if (!Server.userDB.get(u.getName()).isOnline()) { //該用戶名沒有被登錄 System.out.println("該用戶名還沒有上線"); if (u.getPass() .equals(Server.userDB.get(u.getName()).getPass())) // 密碼正確 return true; } } return false; }

到此,整個伺服器端設計的類結構已經分析完了。

三、總結

在這個簡單的伺服器的設計中,我用了大約10天的時間,從最開始的一行代碼一步步走到了現在的幾百行代碼;從最開只能寫出始極簡單的伺服器,到現在的功能還算全面的伺服器;在這個過程中也是付出了一定的時間和精力,也遇到了一些困惑和疑問,幸運的是,在我的身邊聚集了一群熱情、勇於探索的盟友,是他們(更確切的說,是我們)幫我走出困境。

  • 迷惑一:伺服器和客戶端到底是怎麼進行通信的

創建一個伺服器不就是三步么,創建ServerSocket對象,等待連接獲取Socket對象,獲取輸入輸出流對象。當然,一開始的那十幾行代碼還是直白明了好理解的,我知道了是用read()和write()方法進行通信。然而就在幾天後,隨著代碼的一步步增多,功能的增加,我就搞迷糊了,到底是如何通信的,加之我們最開始是用的是windows命令行客戶端(Windows自帶的一個非常原始的客戶端),我總以為read()方法的作用就是從我的命令行界面上讀取字元,而write()方法呢就是將一個字元串顯示在我的界面上。一旦有了這個思想,後面就無法進行下去了,因為搞不懂伺服器要發送消息是為什麼要用到write()(不是直接客戶端自己顯示在界面上就可可以了么),而伺服器要讀取消息時(這個更惱火),從哪裡讀取。

現在的理解:write()的作用不是將字元串在界面上顯示,而是將字元串發送到位元組流上,等待對方接受;read()方法的作用不是從界面上讀取字元,而是從發送過來的字元流上讀取字元,只有當對方發送了,這邊用read()方法才能讀取,否則將一直是處於等待讀取的狀態。另外,伺服器的發送和接受消息時互不影響的,就好像一個人在聽別人講話的同時自己也可以說話。

  • 迷惑二:我知道客戶機連接進來後要交由一個線程去處理,包括收發消息,那麼是要把收發消息放到一個類中么。

當然不是的,重寫線程中的run()方法中是一個死循環,發消息不應該放於這中間,只有接收消息的方法放這中間,因為接收消息有延遲等待的現象,要讓他一直處於循環等待狀態;而發送消息不存在延遲,只有要發送時才調用他。

最後的界面如下:

客戶端設計篇然我們等待下一篇。

package t_server1021_較完整伺服器;import java.awt.Color;import java.awt.Dimension;import java.awt.FlowLayout;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import javax.swing.JButton;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JTextArea;import javax.swing.JTextField;/** * 伺服器界面,包括開啟關閉伺服器,伺服器埠號等 * @author Sam * */public class ServerUI { private JFrame jf_server; //伺服器界面 private JLabel jl_port,jl_num; //埠顯示區,在線人數 private JTextField jt_port; //埠輸入端 private JButton jb_start,jb_send; //開始,發送按鈕 private String station = "開啟"; //開啟,關閉狀態字元串 private int port; //埠號 private Server server; //伺服器對象 public static JTextArea area_recive = new JTextArea(8,33); public static JTextArea area_send = new JTextArea(7,33); public static JTextArea area_num = new JTextArea(); private String server_input; //伺服器要發送的字元串 public static void main(String[] args) { new ServerUI(); } //構造函數 public ServerUI(){ showUI(); } //顯示界面的函數 public void showUI(){ jf_server = new JFrame("伺服器"); jf_server.setSize(400, 400); jf_server.setDefaultCloseOperation(3); jf_server.setLocationRelativeTo(null); jf_server.setLayout(new FlowLayout()); //開放埠 jl_port = new JLabel("開放埠"); jt_port = new JTextField(5); jb_start = new JButton(station); //給按鈕添加監聽器,開啟後馬上獲取埠號,開啟相應伺服器 jb_start.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { //調用開啟伺服器的方法 startAction(e); } }); jb_send = new JButton("發送"); //給發送按鈕添加監聽器 jb_send.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { sendAction(e); } }); area_send.setBackground(Color.pink); area_recive.setBackground(new Color(160,130,50)); jf_server.add(jl_port); jf_server.add(jt_port); jf_server.add(jb_start); jl_num = new JLabel("在線人數"); jf_server.add(jl_num); area_num.setPreferredSize(new Dimension(50,20)); jf_server.add(area_num); jf_server.add(area_recive); jf_server.add(area_send); jf_server.add(jb_send); jf_server.setVisible(true); } //開啟伺服器的方法,在監聽器中調用,先要將開啟按鈕改為關閉,獲取埠號,開啟相應伺服器 public void startAction(ActionEvent e){ //獲取按鈕上的字元串,如果是開啟則開啟伺服器,若為關閉則關閉伺服器 String str_button = e.getActionCommand(); if(str_button.equals("開啟")){//點擊前為開啟,點擊後變為關閉並開啟伺服器 station = "關閉"; jb_start.setText(station); area_num.setText("0"); //獲取埠號,傳給創建伺服器的類,創建對應埠的伺服器 this.port = Integer.parseInt(jt_port.getText()); server = new Server(port); //創建伺服器類對象 server.createServer(); //調用相應的方法創建相應伺服器 server.start(); }else if(str_button.equals("關閉")){//點擊前為關閉,點擊後變為開啟並關閉伺服器 jb_start.setText("開啟"); } } //發送監聽方法 public void sendAction(ActionEvent e){ if(station.equals("關閉")){ //表示服伺服器開啟 server_input = area_send.getText(); //獲取伺服器要發送的字元串 server_input = "伺服器:"+server_input; //獲取到要發送的字元串後,就要調用相應的發送方法,將字元串發送給客戶端 for(int i = 0;i<Server.stList.size();i++){ //給每個客戶端都發送該消息 Server.stList.get(i).sendMessage(server_input); } area_recive.append(server_input+"n"); //將發送的消息添加到顯示區 area_send.setText(null); //清空 } }}package t_server1021_較完整伺服器;import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;import java.util.ArrayList;import java.util.HashMap;import javax.swing.JTextArea;/** * 創建伺服器的類 * * @author Sam * */public class Server extends Thread { public ServerSocket server; // 伺服器類對象,全局變數 public int port; // 要開啟的埠號 // 用於存放處理客戶機的線程隊列 public static ArrayList<ServerThread> stList = new ArrayList<ServerThread>(); // 用戶資料庫 public static HashMap<String, User> userDB = new HashMap<>(); // 預先存進10個用戶名 static { for (int i = 0; i < 10; i++) { User u = new User("poe" + i, "poe" + i, null); System.out.println("生成用戶名:" + u.getName()); userDB.put(u.getName(), u); // 將生成的用戶名加入資料庫中 } } // 構造函數,從窗體界面調用,傳入一個埠號 public Server(int port) { this.port = port; } // 創建伺服器的方法 public void createServer() { try { // 創建對應埠的伺服器 server = new ServerSocket(port); System.out.println("伺服器" + port + "創建成功,等待客戶機的連接"); } catch (IOException e) { e.printStackTrace(); } } // public void run() { // 讓伺服器進入循環等待 while (true) { try { Socket client = server.accept(); // 當程序運行到這裡是就將進入阻塞狀態,直到有客戶機連上,就再次進入等待 System.out.println("客戶機連接上,等待驗證用戶名和密碼"); // 每當有客戶機連接上後,就開啟一個線程,將對客戶機的處理交由線程去處理 ServerThread st = new ServerThread(client); st.start(); // 每創建一個線程對象,就將該線程對象加入隊列中 if (client.isBound()) { stList.add(st); // 當客戶端與伺服器連接上時,就將他加入隊列 } ServerUI.area_num.setText("" + stList.size()); } catch (Exception e) { e.printStackTrace(); } } }}package t_server1021_較完整伺服器;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.net.Socket;import javax.swing.JTextArea;/** * 繼承線程的類,在這個類中主要完成對客戶機連接後的處理,及包括收發消息 * * @author Sam * */public class ServerThread extends Thread { public Socket client; // 客戶機全局變數,由創建伺服器方法中傳入 private InputStream ins; // 輸入流對象 private OutputStream ous; // 輸出流對象 private BufferedReader br; private User user; // 該線程對應的用戶 private int count; // 計數器 public User getUser() { return user; } public void setUser(User user) { this.user = user; } // 構造函數 public Socket getClient() { return client; } // 構造函數,傳入一個客戶端client public ServerThread(Socket client) { this.client = client; user = new User(null, null, null); try { ins = client.getInputStream(); // 獲取輸入輸出流對象 ous = client.getOutputStream(); } catch (Exception e) { e.printStackTrace(); } } // 重寫線程中的run方法,這個方法主要實現對收發消息的處理 public void run() { while (true) { recieveMessage();// 調用該方法會阻塞,放於線程中 if (client.isClosed()) { Server.stList.remove(this); System.out.println("移出隊列"); ServerUI.area_num.setText("" + Server.stList.size()); return; // 當伺服器斷開與客戶端連接時,就跳出循環 } } } // 伺服器發送消息的方法 public void sendMessage(String mesg) { mesg += "
"; System.out.println("伺服器要發送的消息是:" + mesg); byte[] data = mesg.getBytes(); // 獲取要發送字元串的位元組數組 try { ous.write(data);// 發送位元組數組 } catch (IOException e) { e.printStackTrace(); } } // 伺服器接收消息的方法 public void recieveMessage() { br = new BufferedReader(new InputStreamReader(ins)); try { String recieve = br.readLine();// 獲得收到的消息 count++; System.out.println("伺服器收到的第" + count + "條消息是:" + recieve); if (recieve.equals("bye")) { closeClient(); } if (count == 1) { // 第一條為用戶名 user.setName(recieve); System.out.println("客戶機輸入的用戶名是:" + recieve); } if (count == 2) { // 第二條為密碼 user.setPass(recieve); System.out.println("客戶機輸入的密碼是:" + recieve); if(!User.checkUser(user)){ closeClient(); //用戶名不合法就關閉與伺服器的連接 return; } Server.userDB.get(user.getName()).setOnline(true); //設置標誌為true,表示該用戶已上線,禁止其他人再次使用該用戶登錄 ServerUI.area_recive.append(user.getName()+"上線
"); } if (count >= 3) { //第二條以後的是聊天信息,顯示在聊天板上 recieve = user.getName() +":"+ recieve + "
"; ServerUI.area_recive.append(recieve); // 將接收到的消息加入到消息顯示框中 } } catch (IOException e) { e.printStackTrace(); } } // 關閉客戶端與伺服器的連接 public void closeClient() { try { client.close(); System.out.println("已關閉一個客戶端與伺服器的連接"); } catch (IOException e) { e.printStackTrace(); } }}package t_server1021_較完整伺服器;/** * 用戶類,將用戶的一些消息打包 * * @author Sam * */public class User { private String name; // 用戶名 private String pass; // 密碼 private String sex; // 性別 private boolean online = false; // 該用戶是否上線 public boolean isOnline() { return online; } public void setOnline(boolean online) { this.online = online; } // 構造函數 public User(String name, String pass, String sex) { this.name = name; this.pass = pass; this.sex = sex; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPass() { return pass; } public void setPass(String pass) { this.pass = pass; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } // 判斷用戶名是否合法 public static boolean checkUser(User u) { if (Server.userDB.containsKey(u.getName())) {// 用戶資料庫中包含該用戶名的用戶 if (!Server.userDB.get(u.getName()).isOnline()) { //該用戶名沒有被登錄 System.out.println("該用戶名還沒有上線"); if (u.getPass() .equals(Server.userDB.get(u.getName()).getPass())) // 密碼正確 return true; } } return false; }}

推薦閱讀:

怎麼識別web伺服器的開發埠及伺服器的版本?
SSR - 收藏集 - 掘金
低價穩定的VPS推薦
三步搭建伺服器
node.js構建靜態伺服器

TAG:伺服器 |