手把手教会你 | 多用户-服务器聊天室应用软件开发

01.项目分析

这个项目利用Socket、ServerSocket、DatainputStream、DataOutputStream、Thread以及GUI技术,开发一个简单聊天室应用程序。图1显示这个聊天室程序本机模拟运行后的典型结果。其中可以看到服务器接受两个用户以及显示两个用户的对话过程。下图是在本机操作系统窗口运行一个聊天用户的截图。

图1 聊天室两个用户对话截图

类的设计:

在下面的代码解释中将详细讨论主要类和方法的设计目的编写技术。这里将项目中的两大类:服务端类ChatServer以及用户端类ChatClient做一个概括性描述。

ChatServer——利用ServerSocket创建连接、监控用户端连接端口、利用API类Vector管理和协调多用户端的请求与对话操作,以及各种异常处理。

ChatClient——利用Socket创建对服务端ServerSocket的连接和异常处理、运行时得到用户名以及显示、利用GUI组件创建聊天室窗口、布局、显示聊天内容以及事件处理。利用线程执行对多聊天用户的协调处理和操作等。ChatClient包括如下内部类:

WindowExitHandler——执行按下Exit按钮时的关闭聊天室窗口操作。

TextActionHandler——执行将输入的聊天信息显示到每个用户聊天室窗口的功能。

ChatClientReceive——执行聊天用户线程之间聊天信息的协调性显示以及运行。

实战项目测试和模拟运行:

建议你首先在本机对这个实战项目进行模拟试运行。服务端程序可在Eclipse或者本机操作系统运行;然后打开两到三个本机操作系统窗口,按照图1下图运行用户端程序。

以下是用户端程序的主要代码部分:

void client() { //自定义方法执行用户连接和线程创建
  try {
    if(hostname.equals("local"))
    hostname=null;
    InetAddress serverAddr= InetAddress.getByName(hostname);
    sock=new Socket(serverAddr.getHostName(), port); //按指定地址和端口创建Socket
    remoteOut=new DataOutputStream(sock.getOutputStream()); //创建输出流
    System.out.println("Connected to server " + serverAddr.getHostName() //打印信息
              + " on port " + sock.getPort() + " user: " + username);
    new ChatClientReceive(this).start(); //创建用户线程准备执行
  }
  catch(IOException e) {
    System.out.println(e.getMessage() + " : Failed to connect to server.");
  }
}
class TextActionHandler implements ActionListener {//处理文本框聊天信息事件和发送信息到服务器
    public void actionPerformed(ActionEvent e) {
      try  {
        if (e.getSource() == sendText) { //如果是文本框触发事件
          remoteOut.writeUTF(username + "->" + sendText.getText()); //发送信息
          receivedText.append("\n" + username + "->" + sendText.getText());//显示到本机
          sendText.setText(""); //清除文本框内容
        }
      }
      catch(IOException x) {
        System.out.println(x.getMessage() + " : connection to peer lost.");
      }
    }
  }
}
class ChatClientReceive extends Thread { //处理用户接收聊天室信息线程
  private ChatClient chat;
  ChatClientReceive(ChatClient chat) { //构造方法
    this.chat=chat;
  }
  public synchronized void run() { //应用协调覆盖run()方法
    String s;
    DataInputStream remoteIn=null;
    try {
      remoteIn= new DataInputStream(chat.sock.getInputStream()); //创建接收信息数据流
      while(true) {
        s = remoteIn.readUTF(); //按UTF方式读入信息
        chat.receivedText.append("\n" + s); //添加到文本窗口
      }
    }
    catch(IOException e) {
      System.out.println(e.getMessage() + " : connection to peer lost.");
    }
  }
}

 用户端主方法创建GUI窗口以及ChatClient对象如下:

//聊天室用户端主方法代码
public static void main(String args[]) {
  JFrame frame= new JFrame("Connecting to Dear "+args[0]); //利用指令参数指定用户名
  ChatClient chat=new ChatClient(frame,args[0],"localhost"); //创建用户
  frame.add("Center",chat); //注册聊天室窗口
  frame.setSize(350,600); //指定大小
  frame.setResizable(false); //不可变更
  frame.setVisible(true); //显示窗口
  chat.client(); //调用client()方法
  }

 代码中利用指令行参数args[0]来指定用户名。所以必须在操作系统窗口利用java指令运行这个用户端程序。例如:

java ChatClient UserName //打入具体用户名,如 Emily

如下是聊天室服务器端程序的主要代码:

//
...
public class ChatServer { //聊天室服务器端程序
  private static final int port = 1688; //指定端口
  private boolean connected = true; //假设连接无误
  private Vector<DataOutputStream> clients=new Vector<DataOutputStream>(); //创建用户队列
  public static void main(String args[]) {
    new ChatServer().server(); //创建无名服务器对象并调用方法server()
  }
void server() { //自定义方法执行连接用户和聊天室操作
    ServerSocket serverSock = null;
    try {
      InetAddress serverAddr=InetAddress.getByName(null); //地址初始化
      System.out.println("Welcome to Chat Server. The server is running..."); //显示运行信息
      System.out.println("Waiting for " + serverAddr.getHostName() + " on port "+ port);
      serverSock=new ServerSocket(port, 50); //聊天室最大用户为50, 可以是任何整数
    catch(IOException e) {
      System.out.println(e.getMessage()+": Disconnected/Failed");
    }
    while(connected) {
      try {
        Socket socket=serverSock.accept(); //接受用户连接请求
        System.out.println("Accept"+socket.getInetAddress()
        getHostName()); //显示信息
        //如下代码行创建输出流
        DataOutputStream remoteOut= new DataOutputStream(socket.get
        OutputStream());
        clients.addElement(remoteOut); //将一个用户加入聊天室队列
        new ServerHelper(socket,remoteOut,this).start();
                  //启动这个用户的线程
      }
      catch(IOException e) {
      System.out.println(e.getMessage()+": Disconncted/Failed");
      }
    }
  }
  synchronized Vector getClients() { //自定义方法返回用户队列
    return clients;
  }
  synchronized void removeFromClients(DataOutputStream remoteOut){ //删除推出聊天室用户
    clients.removeElement(remoteOut);
  }
}

虽然在这个代码中规定聊天室最大用户容量为50,这个数量可根据网络速度调整。代码中利用Vector对象来记录聊天室中的用户对象,以便向所有聊天室用户发送对话信息。每一个用户都是一个独立的线程,由其创建一个无名ServerHelper对象,并调用其start()方法来启动执行。因为在调用自定义方法getClients()和removeFromClients()时存在多线程协调问题,所以应用协调操作synchronized。

如下是自定义类ServerHelper的代码:

//聊天室服务器端用来协调处理用户聊天信息I/O的线程
class ServerHelper extends Thread {
  private Socket sock;
  private DataOutputStream remoteOut;
  private ChatServer server;
  private boolean connected = true;
  private DataInputStream remoteIn;
  //构造器对服务器和输入聊天室信息的用户初始化
  ServerHelper(Socket sock,DataOutputStream remoteOut,ChatServer server) throws IOException {
    this.sock=sock;
    this.remoteOut=remoteOut;
    this.server=server;
    remoteIn=new DataInputStream(sock.getInputStream());
  }
  public synchronized void run() { //覆盖run()方法并协调运行
    String s;
    try {
      while(connected) {
        s = remoteIn.readUTF(); //读入一个用户输入信息
        broadcast(s); //传播这个信息到所有聊天室用户
      }
    }
    catch(IOException e) {
      System.out.println(e.getMessage()+"connection failed");
    }
  }
  private void broadcast(String s) { //自定义方法执行聊天室信息传播
    Vector clients=server.getClients(); //得到聊天室所有用户
    DataOutputStream dataOut=null; //声明输出流
    for(Enumeration e=clients.elements(); e.hasMoreElements(); ) { //对每一个聊天室用户
      dataOut=(DataOutputStream)(e.nextElement()); //得到用户输出流对象
      if(!dataOut.equals(remoteOut)) { //如果不是删除用户
        try {
          dataOut.writeUTF(s); //输出这个聊天室信息
        }
        catch(IOException x) {
          System.out.println(x.getMessage()+"Failed");
          server.removeFromClients(dataOut);
        }
      }
    }
  }
}

可以看到,由于在Vector的队列中储存有所有聊天室用户输出流信息,因而容易实现对聊天室所有用户传播对话信息。当某个用户在聊天室输入任何对话后,这个信息作为参数,传入自定义方法broadcast()中,并利用循环和枚举技术,遍历用户队列,执行向所有用户传播这个对话信息的操作。由于方法run()有可能在同一时间被多个用户运行的情况,代码中利用synchronized来实现这个协调。 

猜你喜欢

转载自blog.csdn.net/qq_41640218/article/details/126781378