zoukankan      html  css  js  c++  java
  • 基于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 + "
    ");   // 在自己的聊天界面中显示
            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 + "
    ");
        }
        public static void main(String[] args){
            Server server = new Server(8888);
        }
    }
    
    

    扫地生码云仓库对应源码链接

    向大神看齐
  • 相关阅读:
    使用 WebSphere Adapter for SAP Software V7.5 配置 SAP 系统和客户端之间的安全网络通信 (SNC)
    在 ubuntu 12.04 上安装 redmine
    配置nat稳定网络防病毒
    利用 Replication Handler 备份索引
    .NET 4.5对Base Class Library做出改善
    redmine 和 gitolite 的整合
    IBM Power7 服务器 Hypervisor 内存使用情况研究
    Word域代码的显示
    转载:深入分析MFC文档视图
    VIM常用指令
  • 原文地址:https://www.cnblogs.com/Liu-xing-wu/p/14320376.html
Copyright © 2011-2022 走看看