基於C/S模式的簡單聊天程序(附程序源碼)


基於C/S模式的簡單聊天程序(附程序源碼)

一、需求分析

設計要求

​ 使用Socket實現網上聊天功能。用戶可以通過客戶端連接到服務器端並進行網上聊天。聊天時可以啟動多個客戶端。服務器端啟動后,接收客戶端發來的用戶名和密碼驗證信息。驗證通過則以當前的聊天客戶列表信息進行響應;此后接收客戶端發來的聊天信息,轉發給客戶端指定的聊天客戶(即私聊)或所有其他客戶端;在客戶斷開連接后公告其退出聊天系統的信息。客戶端啟動后在GUI界面接收用戶輸入的服務器端信息、賬號和密碼等驗證客戶的身份。驗證通過則顯示當前系統在線客戶列表。客戶可以與指定對象進行私聊,也可以向系統中所有在線客戶發送信息。
​ 實現本程序需要了解網絡基礎知識,掌握C/S結構的工作特點,掌握數據結構、高級語言及網絡編程知識,可以選擇Visual C++、C或Java等語言實現。

二、設計過程與相關理論

​ 程序設計是基於TCP協議,采用C/S模式實現簡單的一對多聊天(群聊)、一對一聊天(私聊)。TCP是一種可靠的、基於連接的網絡協議,它是面向字節流的,即從一個進程到另一個進程的二進制序列。一條TCP連接需要兩個端點,這兩個端點需要分別創建各自的套接字。通常一方用於發送請求和數據(在這里為聊天的客戶端),而另一方用於監聽網絡請求和數據(在這里為服務端)。
​ 常用於TCP編程的有兩個類,均在java.net包里,這兩個類為:Socket、ServerSocket。

關於Socket

​ Socket是建立網絡連接使用的,在連接成功時,應用程序兩端都會產生一個Socket實例,操作這個實例,完成所需要的對話。

Socket類有多個構造方法:

(1)public Socket(String host, int port) 創建一個流套接字並將其連接到指定主機上。
(2)public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) 創建一個流套接字,指定了本地的地址和端口以及目的地址和端口。
(3)public Socket() 創建一個流套接字,但此套接字並未指定連接。

Socket常用的幾個工具方法,用於處理網絡會話:

(1)public InputStream GetInputStream() throws IOException 該方法返回程序中套接字所能讀取的輸入流。
(2)public OutputStream getOutputStream() throws IOException 該方法返回程序中套接字中的輸出流。
(3)public void close () throws IOException 關閉指定的套接字,套接字中的輸入流和輸出流也將被關閉。
(4)除了以上一個常用的方法外,Socket還提供了想connect(SocketAddress endpoint)用於連接到遠程服務器,getInetAddress()獲取原處服務器的地址等。

關於ServerSocket

​ ServerSocket類實現服務器套接字,等待請求通過網絡傳入,基於該請求執行某些操作,然后可能想請求者返回結果。

ServerSocket有4個構造方法:

(1)public ServerSocket() throws IOException 創建一個服務器套接字,並未指明地址和端口。
(2)public ServerSocket(int port) throws IOException創建一個服務器套接字,指明了監聽的端口,如果傳入的端口為0則可以在所有空閑的端口上創建套接字。默認接受最大連接數為50,如果客戶端鏈接數量超過50,則拒絕新接入的連接。
(3)public ServerSocket(int port, int backlog) throws IOException創建一個服務器套接字,指明了監聽的端口,如果傳入的端口port為0,則可以在所有空閑的端口上創建套接字。接受最大連接數有參數backlog設定,如果接受的連接大於這個數,多余的連接被拒絕。參數的backlog的值必須大於0,如果不大於0則采用默認值50。
(4)public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException創建一個套接字,指定了監聽的地址和端口,並設置了最大連接數。

​ ** ServerSocket常用的方法如下:**

(1)public Socket accpet() throws IOException該方法一直處於阻塞狀態,直到有新的連接接入,建立連接后,該方法會返回一個同於客戶端請求以及服務端響應。
(2)public void setSoTimeout(int timeout) throws SocketExcepotion 此方法用於設置accept()方法最大阻塞時間,如果阻塞時間超過了這個值,將會拋出java.net.SocketTimeoutException異常。
(3)Public void close() throws IOExcepiton關閉服務器套接字。

​ 這次簡單的聊天小程序可以看成是一個一對多通訊的案例,創建一個Server來管理服務器端的各類處理,其中使用多線程來實現多客戶端機制。服務器總是在指定的端口上監聽是否相應客戶的請求,而服務器本身在啟動完成后馬上進入監聽狀態,等待下一個客戶端的接入。為了方便實現消息轉發的處理,構造一個套接字處理器SocketHandler來負責處理信息。而客戶端的話也構建一個ClientHandler類實現了Runnable負責創建客戶端,而一個進程只需調用並啟動線程就可以實現客戶端的上線聊天功能。

其中系統的結構圖如下:

img

系統實現的流程圖如下:

img

三、設計結果(文字說明+截圖)

1.客戶端登錄,輸入登錄名,如果沒有輸入登錄名則會提醒重新輸入。

img

2.服務端顯示在線人數情況(下圖啟動三個客戶端)利用用戶登錄時輸入的登錄名,用Map記錄用戶的登錄名和對應的socket,因此可以直接輸入在線人數的情況。

img

img

3.群聊:如果用戶的信息沒有指定的特殊符號,則認為是群聊信息,由服務端轉發給所有的客戶端。

img

4.私聊:服務端檢查到客戶端發送的信息格式為:@[用戶名]-[聊天信息],則會將用戶的信息轉發到指定用戶名的客戶端。

img

5.退出:當服務端檢測到用戶發上來的信息中包含exit,則表示該用戶下線。更新在線用戶的信息。

img

四、設計體會

​ 通過這次的課程設計,對於TCP建立連接,釋放連接的相關過程有了更深的體會。在實現的過程中遇到一個問題就是——多個客戶端已經正常連接到服務端上了,但在發送信息時卻無法按照原先所想的效果完成。通過不斷的調試發現信息根本就沒要傳到服務端上,而所謂的建立連接成功也只是表面上的成功。系統顯示在連接成功后馬上套接字socket就被關閉了。然而我堅持所有的程序代碼,發現我自己根本就沒有關閉socket。為啥會出現這個問題呢?
​ 為了解決這個問題,我打開了debug模式調試(之前都是看代碼調試),發現客戶端在是實現登錄后就顯示socket被關閉了,雖然沒有顯式地關閉socket,但我在登錄傳輸信息給服務端后關閉的輸出流。查詢了一些資料發現,socket和它對應的輸入流、輸出流是相互關聯的,即無論關閉哪一方,另一方也會隨着被關閉,就是說我在關閉socket對應的輸出流時也相當於把socket關閉了。同樣的如果我們在關閉socket時,所有與它相關聯的流都會被關閉。

五、參考文獻

[1]徐傳運,張楊,王森.Java高級程序設計:第5章 網絡編程.清華大學出版社:第5章 網絡編程.清華大學出版社

六、程序源碼

客戶端

登錄界面:

package com.liuxingwu.client;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;

/**
 * 用戶登錄界面,完成登錄后啟動客戶端
 * @author LiuXingWu
 * @create 2021-01-23 15:42
 */
public class LoginFrame extends JFrame implements ActionListener, KeyListener, FocusListener {
    private Socket socket;    // 客戶的socket
    private String userName;    // 客戶端的名稱

    private JPanel jp;    // 面板
    private JTextField jtf;    // 文本框
    private JButton jb;    // 按鍵
    private String hintText0 = "請輸入用戶名";    // 輸入框提示內容
    private String hintText1 = "用戶名不為空或不規范,請重新輸入";    // 輸入內容為空提示

    public LoginFrame(Socket socket) {
        this.socket = socket;
        this.init();
    }

    /**
     * 初始化
     */
    public void init() {
        // 初始化組件
        jp = new JPanel();    // 面板
        jtf = new JTextField(20);    //文本框, 指定文本框大小
        // 設置提示文字
        jtf.setText(hintText0);     // 提示用戶輸入用戶名
        jtf.setForeground(Color.gray);     // 提示文字顏色
        jb = new JButton("確定");    // 按鍵
        // 將組件添加到面板上
        jp.add(jtf);
        jp.add(jb);
        // 將面板添加到窗體中
        this. add(jp);
        // 設置標題,大小, 位置, 是否可見
        this.setTitle("用戶登錄");
        this.setSize(300, 100);
        this.setLocation(700, 300);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);    // 窗口關閉 程序退出
        this.setVisible(true);     // 窗口可見

        jb.addActionListener(this);    // 給“確定”按鍵綁定一個監聽事件, 當前對象監聽
        jtf.addKeyListener(this);    // 給文本框內容添加一個鍵盤監聽事件
        jtf.addFocusListener(this);    // 給文本框添加一個焦點監聽事件
    }
    @Override
    public void actionPerformed(ActionEvent event) {
        SIGN();    //用戶登錄
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if(e.getKeyCode() == KeyEvent.VK_ENTER) {
            SIGN();    //用戶登錄
        }
    }
    @Override
    public void keyTyped(KeyEvent e) {
    }
    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void focusGained(FocusEvent e) {
        // 獲取焦點時,清空提示內容
        String temp = jtf.getText();
        if (temp.equals(hintText0) || temp.equals(hintText1)) {
            jtf.setText("");
            jtf.setForeground(Color.black);
        }
    }

    @Override
    public void focusLost(FocusEvent e) {
        // 失去焦點時,沒有輸入內容,顯示提示內容
        String temp = jtf.getText();
        if (temp.equals("")) {
            jtf.setText(hintText0);
            jtf.setForeground(Color.gray);
        }
    }

    /**
     * 用戶登錄
     */
    public void SIGN() {
        PrintStream printStream = null;
        userName = null;
        userName = jtf.getText();    // 獲取用戶輸入的名稱
        if (userName.equals("") || userName.equals(hintText0)) {
            jtf.setText(hintText1);    // 提示用戶名不能為空
            jtf.setForeground(Color.gray);     // 提示文字顏色
        } else {
            String temp = "Sign:" + userName;
            try {
                //1.獲取服務器端的輸出流
                printStream = new PrintStream(socket.getOutputStream());
                if (!userName.equals("")) {
                    printStream.println(temp);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            this.setVisible(false);    // 登錄窗口設為不可見
            // 啟動客戶端
            Thread client = new Thread(new ClientHandle(userName, socket));
            client.start();
        }
    }
}

數據處理器線程:

package com.liuxingwu.client;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/**
 * 用戶聊天界面及數據處理器
 * @author LiuXingWu
 * @create 2021-01-23 13:14
 */
public class ClientHandle extends JFrame implements ActionListener, KeyListener, Runnable{
    private JTextArea jta;    //文本域
    private JScrollPane jsp;    //滾動條
    private JPanel jp;    //面板
    private JTextField jtf;    //文本框
    private JButton jb;    //按鈕

    private PrintStream ps = null;    //輸出流

    private Socket socket;    // 用戶端口
    private String userName;    // 用戶名


    //構造方法
    public ClientHandle(String userName, Socket socket) {
        this.userName = userName;
        this.socket = socket;
        this.init();    // 初始化
    }

    /**
     * 初始化
     */
    public void init() {
        //初始化組件
        jta = new JTextArea();    //文本域,需要將文本域添加到滾動條中,實現滾動效果
        jsp = new JScrollPane(jta);    //滾動條
        jp = new JPanel();    //面板
        jtf = new JTextField(10);//文本框大小
        jb = new JButton("發送");//按鈕名稱
        // 將文本框與按鈕添加到面板中
        jp.add(jtf);
        jp.add(jb);
        // 將滾動條和面板添加到窗體中
        this.add(jsp, BorderLayout.CENTER);
        this.add(jp,BorderLayout.SOUTH);

        //設置 標題、大小、位置、關閉、是否可見
        this.setTitle(userName + " 聊天框");//標題
        this.setSize(300,300);// 寬,高
        this.setLocation(700,300);// 水平 垂直
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//窗體關閉 程序退出
        this.setVisible(true);//是否可見

        //給"發送"按鈕綁定一個監聽點擊事件
        jb.addActionListener(this);//讓當前對象監聽
        //給文本框綁定一個鍵盤點擊事件
        jtf.addKeyListener(this);

    }

    @Override
    public void run() {
        Scanner scanner = null;
        try {
            while (true) {
                scanner = new Scanner(socket.getInputStream());    // 獲取接收服務器信息的掃描器
                while (scanner.hasNext()) {
                    jta.append(scanner.next() + System.lineSeparator());    // 將受到的信息顯示出來,換行
                }
            }
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
    }
    @Override
    public void actionPerformed(ActionEvent event) {
        // 點擊發送按鈕時發送會話框中的內容
        //發送數據到socket通道中
        sendDataToServer();
    }

    @Override
    public void keyPressed(KeyEvent e) {
        // 鍵盤按下回車鍵時將會話框中的內容發送出去
        if(e.getKeyCode() == KeyEvent.VK_ENTER) {
            //發送數據到socket通道中
            sendDataToServer();
        }
    }
    @Override
    public void keyTyped(KeyEvent e) {
    }
    @Override
    public void keyReleased(KeyEvent e) {
    }

    //將數據發送給Server服務端
    private void sendDataToServer(){
        String text = jtf.getText();    // 獲取文本框中發送的內容
        jta.append("我:" + text + "\n");   // 在自己的聊天界面中顯示
        try {
            // 發送數據
            ps = new PrintStream(socket.getOutputStream());
            ps.println(text);
            ps.flush();
            // 清空自己當前的會話框
            jtf.setText("");
        } catch (IOException el) {
            el.printStackTrace();
        }
    }
}

客戶端啟動(根據需要可啟動多個,代碼一致):

package com.liuxingwu.client;

import java.io.IOException;
import java.net.Socket;

/**
 * @author LiuXingWu
 * @create 2021-01-23 9:53
 */
public class Client1 {
    public static void main(String[] args) throws IOException, InterruptedException {
        //1.客戶端連接服務器端,返回套接字Socket對象
        Socket socket = new Socket("127.0.0.1",8888);
        // 用戶登錄
        new LoginFrame(socket);
//        socket.close();    // 關閉套接字
    }
}

服務端

Socket處理器:

package com.liuxingwu.server;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 套接字處理器,用來管理聊天的客戶的的行為,比如找誰聊天,是否群發信息。上線下線
 * 一個客戶端對應一個套接字處理器
 * @author LiuXingWu
 * @create 2021-01-23 9:56
 */

class SocketHandler implements Runnable{
    // 關聯用戶的名稱和socket端口,借此統計用戶在線的情況
    private static Map<String,Socket> map = new ConcurrentHashMap<String, Socket>();
    private Socket socket;    // 對應處理的socket

    /**
     * 構造函數
     * @param socket 對應客戶端的套接字
     */
    public SocketHandler(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            Scanner scanner = new Scanner(socket.getInputStream());    // 獲取客戶端的輸入流
            String msg = null;    // 接收用戶的信息
            Server.resetText(msg);
            while(true){
                if(scanner.hasNextLine()){
                    msg = scanner.nextLine();    // 讀取客戶端傳來的數據信息
                    // 用戶登錄
                    if(msg.startsWith("Sign:")){
                        // 將用戶名保存在userName中
                        String userName = msg.split("\\:")[1];    // 獲取用戶名
                        // 注冊該用戶
                        userRegist(userName,socket);
                        continue;
                    } else if(msg.startsWith("@") && msg.contains("-")){ // 用戶選擇私聊, 私聊的格式為:@userName-私聊信息
                        // 用戶必須先注冊
                        firstStep(socket);
                        // 保存需要私聊的用戶名
                        String userName = msg.split("@")[1].split("-")[0];
                        // 保存私聊的信息
                        String str = msg.split("@")[1].split("-")[1];
                        // 發送私聊信息
                        privateChat(socket,userName,str);
                        continue;
                    } else if(msg.contains("exit")){// 用戶退出聊天, 用戶退出格式為:包含exit
                        // 用戶必須先注冊
                        firstStep(socket);
                        // 執行退出流程
                        userExit(socket);
                        break;
                    } else{// 群聊信息
                        // 用戶必須先注冊
                        firstStep(socket);
                        // 執行群聊流程
                        groupChat(socket, msg);
                        continue;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 第一步必須先注冊!
     * @param socket 當前客戶端
     */
    private void firstStep(Socket socket) throws IOException {
        Set<Map.Entry<String,Socket>> set = map.entrySet();
        for(Map.Entry<String,Socket> entry : set){
            if(entry.getValue().equals(socket)){
                if(entry.getKey() == null){
                    PrintStream printStream = new PrintStream(socket.getOutputStream());
                    printStream.println("請先進行注冊操作!");
                    printStream.println("注冊格式為:[用戶名]");
                }
            }
        }
    }

    /**
     * 注冊用戶信息
     * @param userName 用戶名
     * @param socket 用戶客戶端Socket對象
     */
    private void userRegist(String userName, Socket socket){
        map.put(userName, socket);
        Server.resetText("[用戶: " + userName + "] 上線了,他的[客戶端為: " + socket + "]!");
        Server.resetText("當前在線人數為:" + map.size() + "人");
    }

    /**
     * 群聊流程(將Map集合轉換為Set集合,從而取得每個客戶端Socket,將群聊信息發送給每個客戶端)
     * @param socket 發出群聊的客戶端
     * @param msg 群聊信息
     */
    private void groupChat(Socket socket,String msg) throws IOException {
        Set<Map.Entry<String,Socket>> set = map.entrySet();    // 將Map集合轉換為Set集合
        String userName = null;    // 遍歷Set集合找到發起群聊信息的用戶
        for(Map.Entry<String,Socket> entry : set){
            if(entry.getValue().equals(socket)){
                userName = entry.getKey();
                break;
            }
        }
        Server.resetText(userName + "群聊說:" + msg);    // 在服務器上顯示,用於調試
        // 遍歷Set集合將群聊信息發給每一個客戶端(除了自己以外)
        for(Map.Entry<String,Socket> entry : set){
            //取得客戶端的Socket對象
            if (!entry.getValue().equals(socket)) {
                Socket client = entry.getValue();
                PrintStream printStream = new PrintStream(client.getOutputStream());    //取得client客戶端的輸出流
                printStream.println(userName + "群聊說:" + msg);
            }
        }
    }
    /**
     * 私聊流程(利用userName取得客戶端的Socket對象,從而取得對應輸出流,將私聊信息發送到指定客戶端)
     * @param socket 當前客戶端
     * @param userName 私聊的用戶名
     * @param msg 私聊的信息
     */
    private void privateChat(Socket socket, String userName, String msg) throws IOException {

        String curUser = null;    // 取得當前客戶端的用戶名
        Set<Map.Entry<String,Socket>> set=map.entrySet();
        for(Map.Entry<String,Socket> entry : set){
            if(entry.getValue().equals(socket)){
                curUser=entry.getKey();
                break;
            }
        }
        Socket client = map.get(userName);    // 取得私聊用戶名對應的客戶端
        PrintStream printStream = new PrintStream(client.getOutputStream());    // 獲取私聊客戶端的輸出流,將私聊信息發送到指定客戶端
        printStream.println(curUser + "@你說:" + msg);
        Server.resetText(curUser + "私聊" + userName + "說:" + msg);    // 服務器端顯示,用於調試
    }

    /**
     * 用戶退出
     * @param socket
     */
    private void userExit(Socket socket){
        String userName = null;    //利用socket取得對應的Key值
        for(String key:map.keySet()){
            if(map.get(key).equals(socket)){
                userName=key;
                break;
            }
        }
        map.remove(userName,socket);    // 將userName,Socket元素從map集合中刪除
        // 提醒服務器該客戶端已下線
        Server.resetText("用戶:"+ userName +"已下線!");
        Server.resetText("當前在線人數為:" + map.size() + "人");
    }
}

服務器啟動:

package com.liuxingwu.server;

import javax.swing.*;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服務端管理器
 * @author LiuXingWu
 * @create 2021-01-23 9:56
 */
public class Server extends JFrame{
    private static JTextArea jta; //文本域
    private JScrollPane jsp; //滾動條
    private int serverPort;  //服務端的端口號

    //構造方法
    public Server(int serverPort) {
        this.serverPort = serverPort;
        this.init();    // 初始化
        this.excute();    // 執行
    }

    /**
     * 初始化
     */
    public void init() {
        jta = new JTextArea();    //文本域 注意:需要將文本域添加到滾動條中,實現滾動效果
        jsp = new JScrollPane(jta);    //滾動條

        //將滾動條和面板添加到窗體中
        this.add(jsp);
        //設置 標題、大小、位置、關閉、是否可見
        this.setTitle("聊天小程序服務端");    //標題
        this.setSize(500,300);    // 寬 高
        this.setLocation(0,300);    // 水平 垂直
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);    //窗體關閉 程序退出
        this.setVisible(true);    //設為可見
    }

    /**
     * 執行
     */
    public void excute() {
        ServerSocket serverSocket = null;    // 創建服務端套接字
        try {
            serverSocket = new ServerSocket(serverPort);
            // 創建線程池,從而可以處理多個客戶端
            ExecutorService executorService= Executors.newFixedThreadPool(20);
            resetText("聊天室小程序已啟動");    // 輸出提示信息
            while (true) {
                Socket socket = serverSocket.accept();    // 監聽客戶端的情況,等待客戶端連接
                resetText("有新的朋友加入");
                executorService.execute(new SocketHandler(socket));    // 對每一個客戶端,創建一個套接字處理器線程
            }
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();    // 關閉serverSocket通道
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void resetText(String info) {
       jta.append(info + "\n");
    }
    public static void main(String[] args){
        Server server = new Server(8888);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM