Bootstrap

利用Java GUI,结合Java Socket,Java多线程,服务器,制作一个简单的具有界面的多用户实时聊天程序(从GUI,到Socket,到多线程,到服务器,项目级别详细教程)

目录

规划

Java GUI设计

Java Socket

Java 服务器


利用 IntelliJ IDEA软件为例

        首先,我们应当了解,像运行在两台电脑或者手机终端这样的程序一般是没有办法直接相互发送信息的,一般是需要第三方服务器提供服务,就像QQ或者微信那样,来为这两个终端程序提供转发服务,

        因此,我们设计的程序包括服务端(Server)和客户端(Client),两个客户端程序信息交流过程实际上就是各自和服务器信息交流的过程,服务器提供转发服务

规划

        我们应当先规划程序的逻辑,在所有关键点中,程序的可靠性是最重要的!如果一个程序由于各种bug,造成连接断开,或者程序崩溃,或者甚至不知道对方是否在线,那么这常常让人很不舒服

        因此我计划将客户端和服务器的每次交流分为两行内容,第一行为发送信息的类型(消息,状态,异常),第二行为具体内容(消息内容,状态信息,异常信息).同样,服务器或者客户端在接受消息也是每次接受两行的内容对两行内容分别处理

其次应当考虑的逻辑是,要设计一个多用户的系统,使能够多个用户同时使用一台服务器进行信息交流,我计划是这样的,每一个用户自行分配一个ID,ID在0~1023之间,每一对奇偶数构成一对连接(0和1,2和3,4和5......)并将此ID发送服务器,如果两个客户端为一对奇偶数,那么能够相互发送和接受消息,同一个ID只能被用一次,除非占用该ID的客户端断开连接

Java GUI设计

先无需管他逻辑怎样实现,先把UI界面做完整

把主方法填完整

package MyProject;
public class Client {
    public static void main(String []args){
        
    }
}

创建ClientFrame界面类和ClientPanel面板类,使其分别继承与JFrame和JPanel

class ClientFrame extends JFrame {
    public ClientPanel cp;//声明ClientPanel的一个变量
    
    public ClientFrame(){
        setSize(700,700);//设置界面大小
        setLocationRelativeTo(null);//使界面默认显示在屏幕中央
        setResizable(false);//使界面大小不能被改变,否则可能会使布局错乱
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置在关闭窗口时的操作,即关闭窗口是退出程序
        cp=new ClientPanel(this);//将ClientPanel实例化,并将自身传递给ClientPanel对象,供改变内容时调用
        add(cp);//将面板对象添加至此窗口对象
        setVisible(true);//默认是不显示窗口的,需要手动去显示,一定要防止最后面,否则会导致面板的内容无法显示
    }
}
class ClientPanel extends JPanel{
    public ClientFrame cf;//用来保存ClientFrame对象 
    
    public ClientPanel(ClientFrame cf){
        this.cf=cf;//赋值传过来的对象
    }
}

除去具体的控件,界面已经大致完整

在mian方法中实例化一个ClientFrame对象,就可以看到界面了

public class Client {
    public static void main(String []args){
        ClientFrame cf=new ClientFrame();//实例化ClientFrame对象
        cf.setTitle("聊天程序-未连接至服务器");//设置标题
    }
}

效果如下

 然后我们为ClientPanel类添加控件,形成最终的交互界面

class ClientPanel extends JPanel implements ActionListener {
    public ClientFrame cf;//用来保存ClientFrame对象
    public Connection cc;

    public JLabel l1=new JLabel("服务器地址:");
    public JLabel l2=new JLabel("服务器端口:");
    public JLabel l3=new JLabel("为自己设置ID(0-1023)");//供身份识别,进行一对一的信息交流

    public JTextField t1=new JTextField("");//输入ip地址
    public JTextField t2=new JTextField("11999");//输入端口号
    public JTextField t3=new JTextField("");//输入ID

    public JButton b1=new JButton("连接");

    public JTextArea ta1=new JTextArea("");//消息文本框
    public JTextArea ta2=new JTextArea("");//消息编辑界面
    public JTextArea ta3=new JTextArea("");//程序运行状态日志框

    public JScrollPane s1=new JScrollPane();//定义可滚动面板
    public JScrollPane s2=new JScrollPane();
    public JScrollPane s3=new JScrollPane();

    public JButton b2=new JButton("发送");

    public ClientPanel(ClientFrame cf){
        this.cf=cf;//赋值传过来的对象
        cc=new Connection(this);

        setLayout(null);//设置空布局,使用xy坐标确定绝对位置
        l1.setBounds(5,5,70,20);//设置文本框的位置x,y和大小width,height
        add(l1);//将此控件加入此面板
        t1.setBounds(80,5,100,20);
        add(t1);
        l2.setBounds(190,5,70,20);
        add(l2);
        t2.setBounds(270,5,50,20);
        add(t2);
        l3.setBounds(330,5,130,20);
        add(l3);
        t3.setBounds(470,5,50,20);
        add(t3);
        b1.setBounds(560,5,100,20);
        add(b1);

        s1.setBounds(5,30,670,500);//设置可滚动面板的位置和大小
        add(s1);//将此面板加入
        s1.getViewport().add(ta1);//将文本框附加在滚动面板上
        ta1.setEditable(false);//设置消息框不可被编辑

        s2.setBounds(5,540,400,110);
        add(s2);
        s2.getViewport().add(ta2);

        b2.setBounds(410,540,100,110);
        add(b2);

        s3.setBounds(515,540,155,110);
        add(s3);
        s3.getViewport().add(ta3);
        ta3.setEditable(false);//设置消息框不可被编辑

        b1.addActionListener(this);//将按钮添加ActionListener监听器接口,这样,点击按钮的时候,触发事件会通过接口方法actionPerformed执行
        b2.addActionListener(this);
    }
}

展现的界面如下

 然后我们为按钮添加事件,要对按钮添加事件,需要实现ActionListener接口,这样按钮所触发的事件会通过接口的actionPerformed方法传递过来,通过比较传过来的参数e,获取e.getSource(),和按钮比较,就能知道是哪个按钮被按下了,然后我们调用相关的方法即可

class ClientPanel extends JPanel implements ActionListener {
//*******************重复代码省略*******************
    @Override
    public void actionPerformed(ActionEvent e) {
        if(e.getSource()==b1){//判断是哪个按钮被按下,就和那个按钮比较一下
            Connect();//连接按钮被按下,就执行连接程序
        }else if(e.getSource()==b2){//发送按钮被按下,就执行发送程序
            SendMessage();
        }
    }
    public void Connect(){
        try {
            String address = t1.getText();//获取输入框的服务器IP地址
            int port = Integer.parseInt(t2.getText());//将String类型端口号转换为int类型
            if(port<0||port>65535){//判断端口是否在正常的范围内
                throw new InputMismatchException();
            }
            String ID = t3.getText();//获取ID码
            if(Integer.parseInt(ID)<0||Integer.parseInt(ID)>1023){//检测正常范围
                throw new InputMismatchException();
            }
            cc.connect(address,port,ID);//如果上面有任何异常,就会被下面捕获,而不会执行到此处的代码,因此,此处执行不会有其他异常
        }
        catch (NumberFormatException e){
            SendLogs("端口格式错误!");
        }
        catch (InputMismatchException e){
            SendLogs("端口不在0~65535正常范围内或者ID不在0~1023范围内");
        }
    }
    public void SendMessage(){
        String str=ta2.getText().replace("\n","\\n");
        //由于客户端和服务器每次交流消息分为两行内容,第一行为发送类型(消息,状态码,异常),第二行为内容,为避免用户输入多行信息造成的程序识别信息混乱,这里将用户输入的回车符转义为\n
        cc.SendData(Connection.Message,str);//将转义后的信息发送出去
        InsertMessage(true,ta2.getText());//并在聊天框中显示自己发送的内容
        ta2.setText("");//清除输入框的内容
    }
    public void AcceptMessage(String s){
        String S=s.replace("\\n","\n");//将发送信息时转义过的字符串\n重新转换为回车符
        InsertMessage(false,S);//在聊天框中显示对方发送的信息
    }
    public void InsertMessage(boolean IsMe,String s){//向消息面板插入新的消息
        if(!IsMe) {
            ta1.setText(ta1.getText() + "\n" + "对方:" + s);//如果是对方发送的消息,就在消息的前面标注是对方发送的
        }
        else{
            ta1.setText(ta1.getText() + "\n" + "我:" + s);//如果是自己发送的消息,就在消息的前面标注是自己发送的
        }
    }
    public void SendLogs(String s){//发送日志到面板的方法
        ta3.setText(ta3.getText()+"\n"+s);//设置日志框的内容为之前的内容,换行,再加上新的内容
    }
}

至此,UI界面及其方法接口完成

Java Socket

然后我们需要为这个程序添加具体的逻辑

我们需要新建两个类,一个是connection类,负责整个逻辑,一个是接收消息线程类,这样逻辑层次关系清晰,分工明确


class Connection{
    Socket sk;//定义Socket变量sk
    ClientPanel cp;//储存cp变量,用于调用方法

    InputStream is;//储存输入流
    OutputStream os;//储存输出流
    PrintWriter pw;//储存PrintWriter对象,用于发送文字信息
    BufferedReader br;//储存BufferedReader对象,用于接收文字信息

    AcceptThread at;//定义接收消息线程类

    public static final String Message = "M";//定义消息类型M,即下一行内容为发送的消息
    public static final String Status =  "S";//定义状态码,即下一行传输的内容不是消息,而是连接状态码
    public static final String Exception = "E";//定义异常码,即下一行返回服务器遇到的异常

    public static final String StatusID= "ID";//ID标志,其后附加自身的ID一并发送过去
    public static final String StatusSUCCESS="SU";//服务器返回的状态码,连接成功的标志
    public static final String StatusPARTYDISCONNECT="PD";//服务器返回的状态码,对方断开连接的标志
    public static final String StatusPARTYCONNECT="PC";//服务器返回的状态码,对方成功连接并处在同一频道内的标志


    public Connection(ClientPanel cp){
        this.cp=cp;//接收并储存对象
    }
    public void connect(String address,int port,String ID){//连接按钮所执行的操作
        try{
            sk=new Socket(address,port);//建立一个新的连接
            is=sk.getInputStream();//获取输入流
            os=sk.getOutputStream();//获取输出流
            pw=new PrintWriter(os);//获取PrintWriter对象
            br=new BufferedReader(new InputStreamReader(is));//获取BufferedReader对象
            SendData(Status,StatusID+ID);//向服务器发送自己设定的ID码
            at=new AcceptThread(this);//开始新的线程接收消息
        }
        catch (Exception e){
            String s=e.getMessage();
            if (s.equals("Connection refused: connect")) {
                cp.SendLogs("无法连接至服务器,请确认服务器是否开启,请稍后再试");
            }else if(s.equals("Connection timed out: connect")){
                cp.SendLogs("无法连接至服务器,请确认服务器是否开启,请稍后再试");
            } else{
                cp.SendLogs("其他错误原因:"+e);
            }
        }
    }
    public void SendData(String Type, String Content){
        pw.println(Type);//发送第一行内容为类型
        pw.println(Content);//第二行为内容
        pw.flush();//发送信息
    }
    public void AcceptMessage(String s){
        cp.AcceptMessage(s);
    }
    public void AcceptStatus(String s){//将状态信息显示在窗口标题以及日志栏中
        switch (s) {
            case StatusSUCCESS:
                cp.SendLogs("连接服务器成功");
                cp.cf.setTitle("聊天程序-已连接至服务器-对方未在线");
                break;
            case StatusPARTYDISCONNECT:
                cp.SendLogs("对方已断开连接");
                cp.cf.setTitle("聊天程序-已连接至服务器-对方未在线");
                break;
            case StatusPARTYCONNECT:
                cp.SendLogs("对方已连接");
                cp.cf.setTitle("聊天程序-已连接至服务器-对方在线");
                break;
        }
    }
    public void AcceptException(String s){
        cp.SendLogs("服务器异常:"+s);
    }
}
class AcceptThread extends Thread{
    Connection cc;//记录Connection对象,用于调用方法

    public AcceptThread(Connection cc){
        this.cc=cc;//接受并赋值Connection对象
        start();//开始一个新的线程,从run()开始执行
    }
    @Override
    public void run() {
        while(true){//无限循环接收消息
            if(cc.sk!=null) {//如果未连接服务器,那么忽略接收消息的代码
                try {
                    String s = cc.br.readLine();//读取第一行信息
                    if (s != null) {//避免读取为空产生异常
                        switch (s) {
                            case Connection.Status: {//如果是状态码
                                String ss = cc.br.readLine();//读取下一行
                                while (ss == null) {//保证读取的有效行
                                    ss = cc.br.readLine();
                                }
                                cc.AcceptStatus(ss);//处理状态码
                                break;
                            }
                            case Connection.Message: {//如果接收的是消息类型
                                String ss = cc.br.readLine();//读取下一行
                                while (ss == null) {
                                    ss = cc.br.readLine();
                                }
                                cc.AcceptMessage(ss);//接收消息
                                break;
                            }
                            case Connection.Exception: {//如果接收的是异常
                                String ss = cc.br.readLine();
                                while (ss == null) {
                                    ss = cc.br.readLine();
                                }
                                cc.AcceptException(ss);//那么将服务器的异常信息打印在日志框内
                                break;
                            }
                        }
                    }
                } catch (IOException e) {
                    if(e.getMessage().equals("Connection reset")){
                        cc.cp.SendLogs("服务器连接已断开,可能原因:服务器已关闭");
                    }
                    else{
                        System.out.println("IO错误:" + e);
                    }
                    break;
                }
            }
        }
    }
}

至此,该聊天系统的客户端程序已经做完成

Java 服务器

        如果我们需要两个客户端程序能够交流,必须需要服务器(Server),服务器分为公网服务器,和私网服务器,如果我们需要在世界各地都能用这个程序交流,那么必须有一台公网服务器执行服务程序,否则,就只能在一个小范围的网络中使用,当然,我们的电脑就可以作为一个私网服务器

完整的服务器代码如下(修改后):

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class Server{
    public static Connection []Rec=new Connection[1024];//记录1024个连接对象
    public static void main(String []args) throws IOException {
        ServerSocket ss=new ServerSocket(11999);//监听11999端口
        while(true){//无限循环
            Socket sk=ss.accept();//一直监听并获取Socket对象的连接
            DealThread dt=new DealThread(sk);//建立一个新的线程去执行逻辑任务,主线程仍然监听
        }
    }
    public static void Destroy(int ID){//当用户断开连接时,销毁储存在表中的对象
        Rec[ID]=null;
    }
}
class DealThread extends Thread{
    Socket sk;//储存Socket对象,供随时调用
    Connection cc;//存储Connection对象,供随时调用
    public DealThread(Socket sk){
        this.sk=sk;//传入并保存对象
        start();//执行新的线程
    }

    @Override
    public void run() {
        cc=new Connection(sk,this);//新建线程开始执行,利用Connection的构造方法实例化一个对象
    }
    public void Insert(int ID){//将新连接完成的Connect对象储存在Server.Rec的表中
        System.out.println("已接受ID:"+ID);//将连接成功的客户端的ID显示出来
        if(Server.Rec[ID]!=null){//如果这个ID在数据表中已经被占用,那么就发出异常信息,提示客户端该ID已经占用
            cc.SendData(Connection.Exception,"This ID has been occupied by others. The connection will be disconnected. Please change the ID and try again");
        }else{
            if(Server.Rec[GetOther(ID)]==null){//如果互为奇偶数的另一个数没有被占用
                cc.SendData(Connection.Status,Connection.StatusSUCCESS);//那么发送状态码连接成功
                System.out.println("连接成功");//显示
                Server.Rec[ID]=cc;//储存在Rec表中
                cc.ID=ID;//将自身的ID储存在Connection里,以供备用
            }else{
                cc.SendData(Connection.Status,Connection.StatusSUCCESS);//如果互为奇偶数的另一个数被占用
                Server.Rec[ID]=cc;//储存在Rec表中
                Server.Rec[GetOther(ID)].SetParty(cc);//两个Connect对象相互传递,使之能够利用对方的Connection向对方传输信息
                cc.SetParty(Server.Rec[GetOther(ID)]);//两个Connect对象相互传递,使之能够利用对方的Connection向对方传输信息
                cc.ID=ID;//将自身的ID储存在Connection里,以供备用
            }
        }
    }
    public int GetOther(int ID){//获取和这个数互为奇偶数的数
        if(ID%2==0){
            return ID+1;
        }
        else{
            return ID-1;
        }
    }
}
class Connection{
    Socket sk;//定义Socket变量sk
    Connection party;
    DealThread dt;
    int ID;

    InputStream is;//储存输入流
    OutputStream os;//储存输出流
    BufferedWriter bw;//储存PrintWriter对象,用于发送文字信息
    BufferedReader br;//储存BufferedReader对象,用于接收文字信息

    AcceptThread at;//定义接收消息线程类

    public static final String Message = "M";//和客户端相同
    public static final String Status =  "S";
    public static final String Exception = "E";

    public static final String StatusID= "ID";
    public static final String StatusSUCCESS="SU";
    public static final String StatusPARTYDISCONNECT="PD";
    public static final String StatusPARTYCONNECT="PC";

    public Connection(Socket sk,DealThread dt){
        this.sk=sk;//传入并储存对象
        this.dt=dt;//传入并储存对象
        try{
            is=sk.getInputStream();//获取输入流
            os=sk.getOutputStream();//获取输出流
            bw =new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));//获取PrintWriter以供发送信息
            br=new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));//获取BufferedReader以供接受信息
        }
        catch (Exception e){//捕获可能存在的错误
            System.out.println("服务程序错误!原因:"+e);
        }
        at=new AcceptThread(this);//开始新的线程监听消息
    }
    public void SetParty(Connection cc){//设置对方的Connection,通过调用对方的Connection的SendData()向对方发送消息
        this.party=cc;//设置
        SendData(Status,StatusPARTYCONNECT);//发送状态码,对方已经在线
    }
    public void SendData(String Type, String Content){
        try {
            bw.write(Type + "\n");//向客户端发送信息类型
            bw.write(Content + "\n");//向客户端发送信息内容
            bw.flush();//发送
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }
    public void AcceptMessage(String s){
        if(party!=null){
            party.SendData(Message,s);//如果用户发送信息,检测party是否被赋值,如果不为空,说明对方在线
        }
        else{
            SendData(Exception,"对方不在线!");//如果为空,说明对方不在线
        }
    }
    public void AcceptStatus(String s){//检测ID状态码
        if(s.startsWith(StatusID)){
            dt.Insert(Integer.parseInt(s.substring(2)));//将附加在ID后面的ID码截取出来
        }
    }
}
class AcceptThread extends Thread{
    Connection cc;//储存Connection,供调用

    public AcceptThread(Connection cc){
        this.cc=cc;//传入Connection对象
        start();
    }
    @Override
    public void run() {
        while(true){
            if(cc.sk!=null) {
                try {
                    String s = cc.br.readLine();
                    if (s != null) {
                        if (s.equals(Connection.Status)) {//检测获取的信息类型
                            String ss = cc.br.readLine();
                            while (ss == null) {
                                ss = cc.br.readLine();
                            }
                            cc.AcceptStatus(ss);//处理状态码
                        } else if (s.equals(Connection.Message)) {//检测获取的信息类型
                            String ss = cc.br.readLine();
                            while (ss == null) {
                                ss = cc.br.readLine();
                            }
                            cc.AcceptMessage(ss);//接受信息,并利用对方的Connection向对方发送信息
                        }
                    }
                } catch (IOException e) {
                    if(e.getMessage().equals("Connection reset")){
                        System.out.println(cc.ID+"连接已断开");
                        if(cc.party!=null){//如果产生异常(Connection reset),说明连接断开,向对方发送状态码说明该用户已经不在线
                            cc.party.SendData(Connection.Status,Connection.StatusPARTYDISCONNECT);//发送状态码
                        }
                        Server.Destroy(cc.ID);//销毁储存的ID
                        break;//终止无限循环
                    }
                    else{
                        System.out.println("IO错误:" + e);//显示其他可能存在的异常
                    }
                }
            }
        }
    }
}

至此,项目完成

完整的客户端代码如下(修改后):

package MyProject;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.InputMismatchException;

public class Client {
    public static void main(String []args){
        ClientFrame cf=new ClientFrame();//实例化ClientFrame对象
        cf.setTitle("聊天程序-未连接至服务器");//设置标题
    }
}
class ClientFrame extends JFrame {
    public ClientPanel cp;//声明ClientPanel的一个变量

    public ClientFrame(){
        setSize(700,700);//设置界面大小
        setLocationRelativeTo(null);//使界面默认显示在屏幕中央
        setResizable(false);//使界面大小不能被改变,否则可能会使布局错乱
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置在关闭窗口时的操作,即关闭窗口是退出程序
        cp=new ClientPanel(this);//将ClientPanel实例化,并将自身传递给ClientPanel对象,供改变内容时调用
        add(cp);//将面板对象添加至此窗口对象
        setVisible(true);//默认是不显示窗口的,需要手动去显示,一定要防止最后面,否则会导致面板的内容无法显示
    }
}
class ClientPanel extends JPanel implements ActionListener {
    public ClientFrame cf;//用来保存ClientFrame对象
    public Connection cc;

    public JLabel l1=new JLabel("服务器地址:");
    public JLabel l2=new JLabel("服务器端口:");
    public JLabel l3=new JLabel("为自己设置ID(0-1023)");//供身份识别,进行一对一的信息交流

    public JTextField t1=new JTextField("");//输入ip地址
    public JTextField t2=new JTextField("11999");//输入端口号
    public JTextField t3=new JTextField("");//输入ID

    public JButton b1=new JButton("连接");

    public JTextArea ta1=new JTextArea("");//消息文本框
    public JTextArea ta2=new JTextArea("");//消息编辑界面
    public JTextArea ta3=new JTextArea("");//程序运行状态日志框

    public JScrollPane s1=new JScrollPane();//定义可滚动面板
    public JScrollPane s2=new JScrollPane();
    public JScrollPane s3=new JScrollPane();

    public JButton b2=new JButton("发送");

    public ClientPanel(ClientFrame cf){
        this.cf=cf;//赋值传过来的对象
        cc=new Connection(this);

        setLayout(null);//设置空布局,使用xy坐标确定绝对位置
        l1.setBounds(5,5,70,20);//设置文本框的位置x,y和大小width,height
        add(l1);//将此控件加入此面板
        t1.setBounds(80,5,100,20);
        add(t1);
        l2.setBounds(190,5,70,20);
        add(l2);
        t2.setBounds(270,5,50,20);
        add(t2);
        l3.setBounds(330,5,130,20);
        add(l3);
        t3.setBounds(470,5,50,20);
        add(t3);
        b1.setBounds(560,5,100,20);
        add(b1);

        s1.setBounds(5,30,670,500);//设置可滚动面板的位置和大小
        add(s1);//将此面板加入
        s1.getViewport().add(ta1);//将文本框附加在滚动面板上
        ta1.setEditable(false);//设置消息框不可被编辑

        s2.setBounds(5,540,400,110);
        add(s2);
        s2.getViewport().add(ta2);

        b2.setBounds(410,540,100,110);
        add(b2);

        s3.setBounds(515,540,155,110);
        add(s3);
        s3.getViewport().add(ta3);
        ta3.setEditable(false);//设置消息框不可被编辑

        b1.addActionListener(this);//将按钮添加ActionListener监听器接口,这样,点击按钮的时候,触发事件会通过接口方法actionPerformed执行
        b2.addActionListener(this);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if(e.getSource()==b1){
            Connect();
        }else if(e.getSource()==b2){
            SendMessage();
        }
    }
    public void Connect(){
        try {
            String address = t1.getText();
            int port = Integer.parseInt(t2.getText());//将String类型端口号转换为int类型
            if(port<0||port>65535){//判断端口是否在正常的范围内
                throw new InputMismatchException();
            }
            String ID = t3.getText();//获取ID码
            if(Integer.parseInt(ID)<0||Integer.parseInt(ID)>1023){//检测正常范围
                throw new InputMismatchException();
            }
            cc.connect(address,port,ID);//如果上面有任何异常,就会被下面捕获,而不会执行到此处的代码,因此,此处执行不会有其他异常
        }
        catch (NumberFormatException e){
            SendLogs("端口格式错误!");
        }
        catch (InputMismatchException e){
            SendLogs("端口不在0~65535正常范围内或者ID不在0~1023范围内");
        }
    }
    public void SendMessage(){
        String str=ta2.getText().replace("\n","\\n");
        //由于客户端和服务器每次交流消息分为两行内容,第一行为发送类型(消息,状态码,异常),第二行为内容,为避免用户输入多行信息造成的程序识别信息混乱,这里将用户输入的回车符转义为\n
        cc.SendData(Connection.Message,str);//将转义后的信息发送出去
        InsertMessage(true,ta2.getText());//并在聊天框中显示自己发送的内容
        ta2.setText("");//清除输入框的内容
    }
    public void AcceptMessage(String s){
        String S=s.replace("\\n","\n");//将发送信息时转义过的字符串\n重新转换为回车符
        InsertMessage(false,S);//在聊天框中显示对方发送的信息
    }
    public void InsertMessage(boolean IsMe,String s){//向消息面板插入新的消息
        if(!IsMe) {
            ta1.setText(ta1.getText() + "\n" + "对方:" + s);//如果是对方发送的消息,就在消息的前面标注是对方发送的
        }
        else{
            ta1.setText(ta1.getText() + "\n" + "我:" + s);//如果是自己发送的消息,就在消息的前面标注是自己发送的
        }
    }
    public void SendLogs(String s){//发送日志到面板的方法
        ta3.setText(ta3.getText()+"\n"+s);//设置日志框的内容为之前的内容,换行,再加上新的内容
    }
}
class Connection{
    Socket sk;//定义Socket变量sk
    ClientPanel cp;//储存cp变量,用于调用方法

    InputStream is;//储存输入流
    OutputStream os;//储存输出流
    BufferedWriter bw;//储存PrintWriter对象,用于发送文字信息
    BufferedReader br;//储存BufferedReader对象,用于接收文字信息

    AcceptThread at;//定义接收消息线程类

    public static final String Message = "M";//定义消息类型M,即下一行内容为发送的消息
    public static final String Status =  "S";//定义状态码,即下一行传输的内容不是消息,而是连接状态码
    public static final String Exception = "E";//定义异常码,即下一行返回服务器遇到的异常

    public static final String StatusID= "ID";//ID标志,其后附加自身的ID一并发送过去
    public static final String StatusSUCCESS="SU";//服务器返回的状态码,连接成功的标志
    public static final String StatusPARTYDISCONNECT="PD";//服务器返回的状态码,对方断开连接的标志
    public static final String StatusPARTYCONNECT="PC";//服务器返回的状态码,对方成功连接并处在同一频道内的标志


    public Connection(ClientPanel cp){
        this.cp=cp;//接收并储存对象
    }
    public void connect(String address,int port,String ID){//连接按钮所执行的操作
        try{
            sk=new Socket(address,port);//建立一个新的连接
            is=sk.getInputStream();//获取输入流
            os=sk.getOutputStream();//获取输出流
            bw =new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));//获取PrintWriter对象
            br=new BufferedReader(new InputStreamReader(is,  StandardCharsets.UTF_8));//获取BufferedReader对象
            SendData(Status,StatusID+ID);//向服务器发送自己设定的ID码
            at=new AcceptThread(this);//开始新的线程接收消息
        }
        catch (Exception e){
            String s=e.getMessage();
            if (s.equals("Connection refused: connect")) {
                cp.SendLogs("无法连接至服务器,请确认服务器是否开启,请稍后再试");
            }else if(s.equals("Connection timed out: connect")){
                cp.SendLogs("无法连接至服务器,请确认服务器是否开启,请稍后再试");
            } else{
                cp.SendLogs("其他错误原因:"+e);
            }
        }
    }
    public void SendData(String Type, String Content){
        try {
            bw.write(Type + "\n");//发送第一行内容为类型
            bw.write(Content + "\n");//第二行为内容
            bw.flush();//发送信息
        }catch (Exception e){
            cp.SendLogs("发生错误:"+e.getMessage());
        }
    }
    public void AcceptMessage(String s){
        cp.AcceptMessage(s);
    }
    public void AcceptStatus(String s){//将状态信息显示在窗口标题以及日志栏中
        switch (s) {
            case StatusSUCCESS:
                cp.SendLogs("连接服务器成功");
                cp.cf.setTitle("聊天程序-已连接至服务器-对方未在线");
                break;
            case StatusPARTYDISCONNECT:
                cp.SendLogs("对方已断开连接");
                cp.cf.setTitle("聊天程序-已连接至服务器-对方未在线");
                break;
            case StatusPARTYCONNECT:
                cp.SendLogs("对方已连接");
                cp.cf.setTitle("聊天程序-已连接至服务器-对方在线");
                break;
        }
    }
    public void AcceptException(String s){
        cp.SendLogs("服务器异常:"+s);
    }
}
class AcceptThread extends Thread{
    Connection cc;//记录Connection对象,用于调用方法

    public AcceptThread(Connection cc){
        this.cc=cc;//接受并赋值Connection对象
        start();//开始一个新的线程,从run()开始执行
    }
    @Override
    public void run() {
        while(true){//无限循环接收消息
            if(cc.sk!=null) {//如果未连接服务器,那么忽略接收消息的代码
                try {
                    String s = cc.br.readLine();//读取第一行信息
                    if (s != null) {//避免读取为空产生异常
                        switch (s) {
                            case Connection.Status: {//如果是状态码
                                String ss = cc.br.readLine();//读取下一行
                                while (ss == null) {//保证读取的有效行
                                    ss = cc.br.readLine();
                                }
                                cc.AcceptStatus(ss);//处理状态码
                                break;
                            }
                            case Connection.Message: {//如果接收的是消息类型
                                String ss = cc.br.readLine();//读取下一行
                                while (ss == null) {
                                    ss = cc.br.readLine();
                                }
                                cc.AcceptMessage(ss);//接收消息
                                break;
                            }
                            case Connection.Exception: {//如果接收的是异常
                                String ss = cc.br.readLine();
                                while (ss == null) {
                                    ss = cc.br.readLine();
                                }
                                cc.AcceptException(ss);//那么将服务器的异常信息打印在日志框内
                                break;
                            }
                        }
                    }
                } catch (IOException e) {
                    if(e.getMessage().equals("Connection reset")){
                        cc.cp.SendLogs("服务器连接已断开,可能原因:服务器已关闭");
                    }
                    else{
                        System.out.println("IO错误:" + e);
                    }
                    break;
                }
            }
        }
    }
}

程序完成截图1:利用本地电脑作为服务器,在同一台电脑上运行两个客户端

 (左侧用户已登录)

 (当右侧用户登录时,两个用户马上各自受到对方在线的消息)

 (发送信息)

 (发送多行信息)

(右侧用户退出) 

程序完成截图2:

利用公网服务器在两台任意电脑上运行客户端通信:

执行服务器程序 

 在两台电脑上运行客户端:

客户端1:

(用户1连接成功) 

 用户2:

 (用户2连接成功,用户1和用户2建立连接)

 

 

 (注:图片是之后添加进去的,源代码会存在中文乱码的问题,不过,只需要把所有的PrintWriter替换为BufferWriter并使用UTF-8编码即可解决)

如果本文对你有用,请帮忙点赞,让更多人看到,下方链接获取完整的资源(Java源代码,Class文件,Jar文件),制作不易,请多多支持,谢谢

https://download.csdn.net/download/A3010367369/85202654

;