socket就是指两个应用程序之间通信的抽象对象,我们可以使用socket实现网络应用程序。例如一个多人聊天室。

目录

先从服务端开始

创建一个窗口类

创建一些方法,用于管理服务端链接,或者进行消息的发送。

编写一个多线程类,用于监听用户的消息输入

回到服务端窗口类,添加一些变量

编写addbutton方法

编写runnable方法

服务端窗体类添加一个构造方法

SendMessageToHost方法

dispose方法

服务端预览图

客户端的制作

addbutton方法

dispose方法

connect方法

登录窗口类

addbutton方法

addlistener方法

构造方法

预览

客户端的构造方法 

预览一下客户端

 最终效果图


先从服务端开始

创建一个窗口类

我习惯在主包下创建内部类,大家也可以另外自己创建一个包或者类。

public class Main
{
    public static void main(String[] args)
    {
        new serverFrame();
    }

    public static class serverFrame extends JFrame
    {

    }
}

创建一些方法,用于管理服务端链接,或者进行消息的发送。

//等会调用这个给窗口添加组件
private void addButton(){}

//等会调用这个方法来唤起多线程
private void runnable(){}

//在监听客户端发送信息的线程中调用此方法,向服务端输出接收到的信息,同时向其他客户端输出。
public void SendMessageToHost(String message,Socket s){}

@override
public void dispose(){super.dispose();}

编写一个多线程类,用于监听用户的消息输入

当用户链接至服务器时,首先单独创建一个多线程,用于监听该用户的字节流,如果有数据,则调用服务端的SendMessageToHost方法。

//监听客户端向服务端发送信息的线程
public class MessageListener extends Thread {

    //客户端的字节流
    public BufferedReader reader;
    //如果客户端发送消息,通过此对象调用SendMessageToHost方法,向服务器发送信息。
    public Main.serverFrame server;
    //代表客户端自己的socket
    public Socket self;

    public MessageListener(BufferedReader r, Main.serverFrame s, Socket self)
    {
        reader = r;
        server = s;
        this.self = self;
    }

    @Override
    public void run() {
        while (true)
        {
            try {
                String message = reader.readLine();
                server.SendMessageToHost(message,self);
                if (message.equals("exit"))
                {
                    self.close();
                    break;
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
                break;
            }
        }
    }
}

回到服务端窗口类,添加一些变量

用于表示我们一会将会用到的一些数据。

private ServerSocket server;

        //用来记录所有用户的信息输出流
        private Map<Socket, MessageListener> users=new HashMap<>();

        //用来记录所有用户的信息输入流
        private Map<Socket, PrintWriter> socketInputStream = new HashMap<>();

        //用来记录所有用户的链接socket
        private List<Socket> sockets = new ArrayList<>();

        //客户端向服务端发送信息的显示的地方
        private JTextArea conMessage=new JTextArea();

        private JScrollPane panel=new JScrollPane(conMessage);

        //服务端向客户端发送信息的地方
        private JTextField inputArea = new JTextField();

以上是我们服务端窗口类内的全部变量,一会我们都会用得到的~

server就代表我们自己的socket;

users用来根据用户的socket获取相应的监听线程对象,当发生意外或任何情况时,可以进行一个关闭之类的管理操作;

socketInputStream用来获取客户端的输入流,方便服务端发送消息。


接下来我们开始逐步编写窗口类的方法体,首先

编写addbutton方法

,对窗体的一些控件进行布置。

private void addButton()
        {
            conMessage.setFont(new Font(null,Font.PLAIN,30));
            conMessage.setEditable(false);
            getContentPane().add(panel,BorderLayout.CENTER);
            inputArea.setFont(new Font(null,Font.PLAIN,30));
            //添加文本域回车事件(发送信息)
            inputArea.addActionListener(new ActionListener()
            {
                @Override
                public void actionPerformed(ActionEvent e)
                {
                    if (!inputArea.getText().equals(""))
                    {
                        conMessage.append("你:" + inputArea.getText() + "\n");

                        if (users.size() != 0)
                        {
                            //向所有客户端的信息输入流传入信息
                            for (Socket s : socketInputStream.keySet())
                            {
                                socketInputStream.get(s).println("主机:" + inputArea.getText());
                            }
                        }
                        inputArea.setText("");
                    }
                }
            });
            getContentPane().add(inputArea,"South");
        }

接下来再

编写runnable方法

,在里边写一个继承thread的匿名内部类,持续监听客户端的接入。

private void runnable()
        {
            //监听客户端链接线程
            new Thread()
            {
                @Override
                public void run()
                {
                    try {
                        while (true)
                        {
                            Socket socket = server.accept();
                            if (!users.containsKey(socket))
                            {
                                //客户端链接数量阈值
                                if (users.size() < 5)
                                {
                                    BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                                    MessageListener listener = new MessageListener(reader,serverFrame.this,socket);
                                    listener.start();
                                    users.put(socket,listener);
                                    socketInputStream.put(socket,new PrintWriter(socket.getOutputStream(),true));
                                    sockets.add(socket);
                                    conMessage.append("客户端连接,ip:" + socket.getInetAddress() + "\n");
                                    conMessage.append("现在聊天室人数:" + (1 + users.size()) + "\n");
                                    for (Socket s : socketInputStream.keySet())
                                    {
                                        socketInputStream.get(s).println("客户端连接,ip:" + socket.getInetAddress());
                                        socketInputStream.get(s).println("现在聊天室人数:" + (1 + users.size()));
                                    }
                                }
                                else
                                {
                                    System.out.println("客户端接入数量已满!已拒绝一名接入!");
                                    conMessage.append("客户端接入数量已满!已拒绝一名接入!");
                                    socket.close();
                                }
                            }
                        }
                    }
                    catch (IOException ex)
                    {
                        JOptionPane.showMessageDialog(serverFrame.this,"错误!" + ex.getMessage());
                        ex.printStackTrace();
                        for (Socket s : socketInputStream.keySet())
                        {
                            socketInputStream.get(s).println("服务器出现错误,连接终止。");
                            socketInputStream.remove(s);
                            sockets.remove(s);
                            users.remove(s);
                            try {
                                s.close();
                            }
                            catch (IOException ex2)
                            {
                                ex2.printStackTrace();
                            }
                        }
                        dispose();

                    }
                }
            }.start();
        }

这两个方法写完之后,给

服务端窗体类添加一个构造方法

,在初始化的时候调用这两个方法,顺便对窗体的一些属性进行设置

public serverFrame()
        {
            setTitle("socket服务器");
            setDefaultCloseOperation(DISPOSE_ON_CLOSE);
            setBounds(500,400,800,600);
            addButton();
            try {
                server=new ServerSocket(8390);
            }
            catch (IOException ex)
            {
                JOptionPane.showMessageDialog(this,"错误!" + ex.getMessage());
                ex.printStackTrace();
            }
            runnable();
            setVisible(true);
        }

然后再来编写一下我们的

SendMessageToHost方法

,这个方法的用处前面已经说过了~

//在监听客户端发送信息的线程中调用此方法,向服务端输出接收到的信息,同时向其他客户端输出。
        public void SendMessageToHost(String message,Socket s)
        {
            //如果信息是exit则客户端退出
            if (message.equals("exit"))
            {
                users.remove(s);
                sockets.remove(s);
                socketInputStream.remove(s);
                conMessage.append("客户端:" + s.getInetAddress() + " 已退出");
                conMessage.append("现在聊天室人数:" + (1 + users.size()));
                if (users.size() != 0)
                {
                    for (Socket s_2 : socketInputStream.keySet())
                    {
                        socketInputStream.get(s_2).println("客户端:" + s.getInetAddress() + " 已退出");
                        socketInputStream.get(s_2).println("现在聊天室人数:" + (1 + users.size()));
                    }
                }
                return;
            }
            else
            {
                conMessage.append("\n" + message);
                for (Socket s_2 : socketInputStream.keySet())
                {
                    if (!s_2.equals(s))
                    {
                        socketInputStream.get(s_2).println(message);
                    }
                }
            }
        }

最后再来编写一下

dispose方法

,保证服务端窗口关闭后,与服务端链接的socket也能正常关闭。

@Override
        public void dispose()
        {
            try {
                server.close();
                for (Socket s : sockets)
                {
                    users.remove(s);
                    socketInputStream.remove(s);
                    sockets.remove(s);
                    s.close();
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
            super.dispose();
        }

至此,整个服务端程序已经编写完成,

服务端预览图

打开后是这个样子的:

java多对一聊天 java一对一聊天室代码_服务端

做的比较简便,大家可以根据自己的喜好或者想法去修改这个窗体的布局


下面就可以进入到

客户端的制作

了,客户端只做起来相对比较简单。

同样我还是选择在主包进行编写,把客户端窗口类写成一个主包的内部类

public class Main
{
    public static void main(String[] args)
    {
        new ClientFrame();
    }

    public static class ClientFrame extends JFame
    {

    }
}

老样子,先往客户端窗口类里添加一些一会将会用到的数据。

//在聊天室中展示的名字
        public String userName="";
        //服务器套接字
        private Socket server;
        //客户端输出流
        private PrintWriter serverOutputStream;
        //客户端输入流
        private BufferedReader serverInputStream;
        //显示信息的地方
        private JTextArea conMessage=new JTextArea();
        private JScrollPane panel=new JScrollPane(conMessage);
        private JTextField inputArea = new JTextField();

serverOutputStream用来等会向服务器发送字节流。

serverInputStream用来接收服务器的字节流。

inputArea用来输入客户端向服务端发送的消息。


然后我们给客户端添加一个

addbutton方法

,像服务端那样,addbutton方法用于对窗体进行布局

private void addbutton()
        {
            conMessage.setEditable(false);
            conMessage.setFont(new Font(null,Font.PLAIN,30));
            getContentPane().add(panel,BorderLayout.CENTER);
            inputArea.setFont(new Font(null,Font.PLAIN,30));
            //添加文本输入框监听事件
            inputArea.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    //如果没连接上服务器
                    if (server == null)
                    {
                        int value = JOptionPane.showConfirmDialog(ClientFrame.this,"服务器未能连接成功,是否重新连接?","提示",JOptionPane.YES_NO_OPTION);
                        if (value == 0)
                        {
                            new ConnectDialog(ClientFrame.this);
                        }
                    }
                    //否则向服务器发送信息
                    else
                    {
                        serverOutputStream.println(userName + ":" + inputArea.getText());
                        conMessage.append("我:" +  inputArea.getText() + "\n");
                        inputArea.setText("");
                    }

                }
            });
            getContentPane().add(inputArea,"South");
        }

然后编写

dispose方法

用来保证程序关闭的同时与服务端断开连接

@Override
        public void dispose() {
            if (server != null)
            {
                serverOutputStream.println("exit");
                try {
                    server.close();
                    serverInputStream.close();
                    serverOutputStream.close();
                }
                catch (IOException ex)
                {
                    ex.printStackTrace();
                }
            }

            super.dispose();
        }

现在添加

connect方法

,调用此方法向服务端聊天室发起连接。

//调用此方法向服务器发起链接
        public void connect(String name, String ip,String port)
        {
            this.userName = name;
            try {
                server = new Socket(ip,Integer.parseInt(port));
                serverOutputStream = new PrintWriter(server.getOutputStream(),true);
            }
            catch (UnknownHostException ex1)
            {
                JOptionPane.showMessageDialog(this,"连接错误!" + ex1.getMessage());
                ex1.printStackTrace();
            }
            catch (IOException ex)
            {
                JOptionPane.showMessageDialog(this,"连接错误!" + ex.getMessage());
                ex.printStackTrace();
            }

            if (server!=null)
            {
                try {
                    serverInputStream = new BufferedReader(new InputStreamReader(server.getInputStream()));
                    new Thread()
                    {
                        @Override
                        public void run() {
                            //持续监听服务端消息
                            while (true)
                            {
                                try {
                                    String message = serverInputStream.readLine();
                                    conMessage.append(message + "\n");
                                }
                                catch (IOException ex)
                                {
                                    JOptionPane.showMessageDialog(ClientFrame.this,"读取信息错误!" + ex.getMessage());
                                    ex.printStackTrace();
                                }
                            }
                        }
                    }.start();
                }
                catch (IOException ex)
                {
                    JOptionPane.showMessageDialog(this,"创建输入流错误!" + ex.getMessage());
                    ex.printStackTrace();
                }
            }
        }

客户端有了连接方法,但是我们还需要在指定的地方调用他,所以创建一个

登录窗口类

//链接服务器窗口
    private static class ConnectDialog extends JDialog
    {

    }

添加按钮和文本输入,还有一些变量

//通过这个对象调用链接方法
        private ClientFrame main;
        private JLabel nameLabel=new JLabel("昵称:",SwingConstants.CENTER);
        private JTextField nameTextField=new JTextField();
        private JLabel serveripAddress=new JLabel("主机:",SwingConstants.CENTER);
        private JTextField AddressTextfield=new JTextField();
        private JLabel AddressPort=new JLabel("端口:",SwingConstants.CENTER);
        private JTextField portTextfield=new JTextField();

        private JButton JbuttonConnect = new JButton("连接");
        private JButton jbuttonCancel = new JButton("取消");

addbutton方法

用来添加按钮布局

private void addbutton()
        {
            nameLabel.setBounds(20,30,100,40);
            nameLabel.setFont(new Font(null,Font.PLAIN,30));
            add(nameLabel);
            nameTextField.setBounds(110,30,230,50);
            nameTextField.setFont(new Font(null,Font.PLAIN,27));
            add(nameTextField);
            serveripAddress.setBounds(20,100,100,40);
            serveripAddress.setFont(new Font(null,Font.PLAIN,30));
            add(serveripAddress);
            AddressTextfield.setBounds(110,100,230,50);
            AddressTextfield.setFont(new Font(null,Font.PLAIN,27));
            add(AddressTextfield);
            AddressPort.setBounds(20,170,100,40);
            AddressPort.setFont(new Font(null,Font.PLAIN,30));
            add(AddressPort);
            portTextfield.setBounds(110,170,230,50);
            portTextfield.setFont(new Font(null,Font.PLAIN,27));
            add(portTextfield);
            JbuttonConnect.setBounds(80,240,100,40);
            JbuttonConnect.setFont(new Font(null,Font.PLAIN,30));
            add(JbuttonConnect);

            jbuttonCancel.setBounds(215,240,100,40);
            jbuttonCancel.setFont(new Font(null,Font.PLAIN,30));
            add(jbuttonCancel);
        }

addlistener方法

用于添加按钮监听以及输入的信息格式检查,这里我随便弄了下,检查不是很严格。

private void addListener()
        {
            //简单的进行一些数据检验
            JbuttonConnect.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (nameTextField.getText().length() > 8)
                    {
                        JOptionPane.showMessageDialog(ConnectDialog.this,"你的名字太长了!");
                        return;
                    }
                    if (AddressTextfield.getText().length() > 15)
                    {
                        JOptionPane.showMessageDialog(ConnectDialog.this,"ip地址不合法!");
                        return;
                    }
                    if (portTextfield.getText().length() > 5)
                    {
                        JOptionPane.showMessageDialog(ConnectDialog.this,"端口数值不合法!");
                        return;
                    }
                    //检查通过,调用链接方法向服务端发起链接
                    main.connect(nameTextField.getText(),AddressTextfield.getText(),portTextfield.getText());
                    dispose();
                }
            });

            jbuttonCancel.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    dispose();
                }
            });
        }

构造方法

public ConnectDialog(JFrame frame)
        {
            super(frame,"连接窗口",true);
            setDefaultCloseOperation(DISPOSE_ON_CLOSE);
            this.main = (ClientFrame) frame;
            setBounds(700,600,400,350);
            setLayout(null);
            addbutton();
            addListener();
            setVisible(true);
        }

至此登录窗口已经设计完成,可以打开

预览

java多对一聊天 java一对一聊天室代码_客户端_02


最后回到

客户端的构造方法 

public ClientFrame()
        {
            setTitle("socket连接端");
            setDefaultCloseOperation(DISPOSE_ON_CLOSE);
            setBounds(500,400,800,600);

            addbutton();

            setVisible(true);

            new ConnectDialog(this);
        }

至此,,客户端也已经完成制作,一起来

预览一下客户端

吧。

java多对一聊天 java一对一聊天室代码_服务端_03

其实我设计的样式是跟服务端一样的~~

现在打包这两个程序出来,测试一下吧!


 最终效果图

java多对一聊天 java一对一聊天室代码_客户端_04