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 下图运行用户端程序。
以下是用户端程序的主要代码部分:
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 来实现这个协调。
评论