就这样吧
bug基本解决了,用两台电脑加上hamachi测试了一下,能用。
干,刚说完拿自己的外网IP测试了一下,报空指针异常,懒得改了。理由大概是虽然连不上服务器的IP会报错,但是其实你随便连个谁的IP也能连上,所以程序还会执行下去,但是因为对面没有人给你接收,发包没人接,就 报错了。
我猜的。
有外网服务器当然更好,没有就只能挂hamachi或者用内网映射什么的我不懂的东西。
分成两个部分,随便哪台机器运行OIFaQ-Server启动服务器,是第三者的机器也无所谓。
然后用户启动OIFaQ-Client就完事了。
还是挺直观的。
惯例发一下代码,就这样吧,将来重构:
1 import java.io.*; 2 import java.net.*; 3 import java.util.ArrayList; 4 import java.awt.BorderLayout; 5 import java.awt.Color; 6 import java.awt.Dimension; 7 8 import javax.swing.*; 9 10 /** 11 * 服务器端。 12 * 增加了GUI来显示服务器日志。 13 * 14 * @author mlxy 15 * @version 1.1 16 * */ 17 public class SimChatServer extends JFrame { 18 private static final long serialVersionUID = 1L; 19 ServerSocket ss = null; 20 ArrayList<Client> clients = null; 21 22 JTextArea log = null; 23 JScrollPane scrollPane; 24 25 /** 服务器构造方法。*/ 26 public SimChatServer() { 27 // 初始化已连接客户端列表。 28 clients = new ArrayList<Client>(); 29 30 // 初始化框架内容。 31 JPanel mainPanel = new JPanel(); 32 mainPanel.setLayout(new BorderLayout()); 33 mainPanel.setPreferredSize(new Dimension(400, 500)); 34 35 log = new JTextArea(); 36 log.setBackground(new Color(66, 204, 255)); 37 log.setEditable(false); 38 log.setLineWrap(true); 39 log.setWrapStyleWord(true); 40 41 scrollPane = new JScrollPane(log); 42 43 mainPanel.add(scrollPane, BorderLayout.CENTER); 44 add(mainPanel); 45 pack(); 46 } 47 48 /** 主方法。*/ 49 public static void main(String[] args) { 50 try { 51 UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); 52 } catch (UnsupportedLookAndFeelException e) { 53 e.printStackTrace(); 54 } catch (ClassNotFoundException e) { 55 e.printStackTrace(); 56 } catch (InstantiationException e) { 57 e.printStackTrace(); 58 } catch (IllegalAccessException e) { 59 e.printStackTrace(); 60 } 61 62 SimChatServer server = new SimChatServer(); 63 server.launchFrame(); 64 server.startUp(); 65 } 66 67 /** 初始化框架。*/ 68 public void launchFrame() { 69 setTitle("OIFaQ服务端日志"); 70 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 71 setLocationRelativeTo(null); 72 setVisible(true); 73 } 74 75 /** 启动服务器。*/ 76 public void startUp() { 77 // 尝试启动服务器,端口被占用则弹窗提示并退出。 78 try { 79 ss = new ServerSocket(2333); 80 } catch (BindException e) { 81 JOptionPane.showMessageDialog(null, 82 "Port occupied.", "Error", JOptionPane.WARNING_MESSAGE); 83 System.exit(0); 84 } catch (IOException ex) { 85 ex.printStackTrace(); 86 } 87 88 // 死循环,等待客户端连接,连接成功则启动一个新线程。 89 try { 90 while (true) { 91 Client newClient = new Client(ss.accept()); 92 newClient.start(); 93 clients.add(newClient); 94 updateLog("Current clients: " + clients.size()); 95 } 96 } catch (IOException e) { 97 e.printStackTrace(); 98 } 99 } 100 101 /** 102 * 内部类。 103 * 单个客户端线程,包含了处理客户端发来信息的功能,以及向客户端发送信息的方法。 104 * 105 * @author mlxy 106 * @version 1.0 107 */ 108 private class Client extends Thread { 109 Socket singleSocket; 110 ObjectInputStream singleInput; 111 ObjectOutputStream singleOutput; 112 113 /** 一个客户端线程的构造方法。*/ 114 public Client(Socket singleSocket) { 115 this.singleSocket = singleSocket; 116 updateLog("Client connected."); 117 try { 118 singleInput = new ObjectInputStream(singleSocket.getInputStream()); 119 singleOutput = new ObjectOutputStream(singleSocket.getOutputStream()); 120 } catch (IOException e) { 121 e.printStackTrace(); 122 } 123 } 124 125 @Override 126 public void run() { 127 // 主运行部分,抓到客户端退出的异常就打印提示并退出。 128 try { 129 // 死循环,读包拆包打印。 130 while (true) { 131 String[] packReceived = (String[]) singleInput.readObject(); 132 updateLog("[" + packReceived[0] + "]: " + packReceived[1]); 133 for (int i = 0; i < clients.size(); i++) { 134 clients.get(i).sendPackage(packReceived); 135 } 136 } 137 138 } catch (ClassNotFoundException e) { 139 e.printStackTrace(); 140 } catch (EOFException ex) { 141 updateLog("Client disconnected."); 142 } catch (IOException exc) { 143 exc.printStackTrace(); 144 } finally { 145 // 进程准备结束,释放相关资源。 146 try { 147 if (singleSocket != null) 148 singleSocket.close(); 149 if (singleInput != null) 150 singleInput.close(); 151 if (singleOutput != null) 152 singleOutput.close(); 153 154 // 报告当前客户端退出。 155 clientOut(); 156 } catch (IOException e) { 157 e.printStackTrace(); 158 } 159 } 160 } 161 162 /** 向客户端发包的方法。*/ 163 public void sendPackage(Object o) { 164 try { 165 singleOutput.writeObject(o); 166 singleOutput.flush(); 167 } catch (IOException e) { 168 e.printStackTrace(); 169 } 170 } 171 172 /** 管理客户端退出的方法。*/ 173 private void clientOut() { 174 /* 175 // 找出当前客户端所在下标并标记删除。不能一边遍历一边修改列表哇,同志。 176 int indexToRemove = -1; 177 for (int i = 0; i < clients.size(); i++) { 178 if (clients.get(i).equals(this)) { 179 indexToRemove = i; 180 break; 181 } 182 } 183 clients.remove(indexToRemove); 184 */ 185 186 // 同志我跟你讲,你看着像个呆逼。 187 clients.remove(this); 188 updateLog("Current clients: " + clients.size()); 189 } 190 } 191 192 /** 把一个字符串追加到当前日志内容的末尾。*/ 193 private void updateLog(String s) { 194 log.setText(log.getText() + s + "\n"); 195 JScrollBar tempBar = scrollPane.getVerticalScrollBar(); 196 tempBar.setValue(tempBar.getMaximum()); 197 } 198 }
1 import java.awt.*; 2 import java.awt.event.*; 3 4 import javax.swing.*; 5 6 import java.io.*; 7 import java.net.*; 8 9 /** 10 * 客户端程序。 11 * 修改了更新文本框内容的方法,删掉了用来存储内容的StringBuffer,改为动态更新。 12 * 在服务器未启动的情况下启动客户端会弹窗报错并退出。 13 * ServerWaiter类没找到更好的方法,留待将来解决吧。 14 * 15 * @author mlxy 16 * @version 1.1 17 * */ 18 public class SimChatClient extends JFrame { 19 private static final long serialVersionUID = 1L; 20 21 // 框架相关变量。 22 private JPanel panel; 23 private JTextArea area; 24 private JTextField field; 25 private JScrollPane scrollPane; 26 27 // 用户相关变量。 28 private String userID; 29 30 // 网络相关变量。 31 private String serverIP; 32 //private int serverPort; *备用* 33 private Socket socket = null; 34 private ObjectOutputStream oos = null; 35 private ObjectInputStream ois = null; 36 37 /** 默认构造方法,初始化用户ID及框架内容。 */ 38 public SimChatClient() { 39 String userName = (String) JOptionPane.showInputDialog( 40 null, "请输入用户ID:", "新用户", JOptionPane.PLAIN_MESSAGE, 41 null, null, new String("Default")); 42 if (userName == null) 43 System.exit(0); 44 else 45 userID = userName; 46 47 String ip = (String) JOptionPane.showInputDialog( 48 null, "请输入服务器IP:", "确认客户端IP", JOptionPane.PLAIN_MESSAGE, 49 null, null, new String("192.168.1.104")); 50 if (ip == null) 51 System.exit(0); 52 else 53 serverIP = ip; 54 55 panel = new JPanel(); 56 panel.setLayout(new BorderLayout()); 57 58 area = new JTextArea(); 59 area.setLineWrap(true); 60 area.setWrapStyleWord(true); 61 area.setEditable(false); 62 area.setBackground(new Color(255, 204, 255)); 63 scrollPane = new JScrollPane(area); 64 65 field = new JTextField(); 66 field.setBackground(new Color(66, 204, 255)); 67 field.addActionListener(new FieldListener()); 68 69 panel.add(scrollPane, BorderLayout.CENTER); 70 panel.add(field, BorderLayout.SOUTH); 71 72 this.add(panel); 73 this.addWindowListener(new WindowAdapter() { 74 75 @Override 76 public void windowClosing(WindowEvent e) { 77 disconnect(); 78 System.exit(0); 79 } 80 81 @Override 82 public void windowActivated(WindowEvent e) { 83 super.windowActivated(e); 84 field.requestFocusInWindow(); 85 } 86 }); 87 } 88 89 /** 主运行方法。 */ 90 public static void main(String[] args) { 91 // 设置UI风格。 92 try { 93 UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); 94 } catch (UnsupportedLookAndFeelException e) { 95 e.printStackTrace(); 96 } catch (ClassNotFoundException e) { 97 e.printStackTrace(); 98 } catch (InstantiationException e) { 99 e.printStackTrace(); 100 } catch (IllegalAccessException e) { 101 e.printStackTrace(); 102 } 103 104 SimChatClient frame = new SimChatClient(); 105 frame.launchFrame(); 106 } 107 108 /** 框架参数初始化。 */ 109 public void launchFrame() { 110 setTitle("Simple Chat Online: " + userID); 111 setSize(400, 500); 112 setLocationRelativeTo(null); 113 setVisible(true); 114 115 connect(); 116 } 117 118 /** 连接到服务器端。 */ 119 private void connect() { 120 try { 121 socket = new Socket(serverIP, 2333); 122 oos = new ObjectOutputStream(socket.getOutputStream()); 123 new Thread(new ServerWaiter()).start(); 124 } catch(ConnectException e) { 125 JOptionPane.showMessageDialog(null, 126 "服务器未启动,请联系系统管理员。", "连接失败", JOptionPane.WARNING_MESSAGE); 127 System.exit(0); 128 } catch (IOException e) { 129 e.printStackTrace(); 130 } 131 area.setText("Connection established.\n"); 132 } 133 134 /** 释放资源,与服务器断开连接。 */ 135 private void disconnect() { 136 try { 137 if (ois != null) 138 ois.close(); 139 if (oos != null) 140 oos.close(); 141 } catch (IOException e) { 142 e.printStackTrace(); 143 } 144 } 145 146 /** 147 * 按照格式更新文本框内容。 148 * 149 * @param userName 用户名 150 * @param content 消息内容 151 */ 152 private void updateTextArea(String userName, String content) { 153 // 之后加上发送时间。 154 area.setText(area.getText() + "[ " + userName + " ] : " + content + "\n"); 155 JScrollBar tempBar = scrollPane.getVerticalScrollBar(); 156 tempBar.setValue(tempBar.getMaximum()); 157 } 158 159 /** 内部类,输入框的监听器。 */ 160 private class FieldListener implements ActionListener { 161 162 @Override 163 public void actionPerformed(ActionEvent e) { 164 // 按下回车后向服务器发包,然后清空输入框。 165 sendPackage(); 166 field.setText(""); 167 } 168 169 /** 私有方法,读取用户ID和输入内容,向服务器发送包。 */ 170 private void sendPackage() { 171 String[] packToSend = { userID, field.getText() }; 172 173 try { 174 oos.writeObject(packToSend); 175 oos.flush(); 176 } catch (IOException ex) { 177 ex.printStackTrace(); 178 } 179 } 180 } 181 182 /** 内部类,等待服务器发包的线程类。 183 * 184 * @author mlxy 185 * @version 0.8 186 * */ 187 class ServerWaiter implements Runnable { 188 public void run() { 189 try { 190 ois = new ObjectInputStream(socket.getInputStream()); 191 192 // 死循环,阻塞式地从服务器读包,之后更新文本框。 193 while (true) { 194 // 程序被readObject阻塞住的时候如果关闭框架会报错。之后解决。 195 String[] packReceived = (String[]) ois.readObject(); 196 updateTextArea(packReceived[0], packReceived[1]); 197 } 198 } catch (SocketException e) { 199 // 目前是简单粗暴地抓了异常就跳出程序。 200 System.out.println("Socket closed, client out."); 201 System.exit(-1); 202 } catch (IOException e) { 203 e.printStackTrace(); 204 } catch (ClassNotFoundException e) { 205 e.printStackTrace(); 206 } 207 } 208 } 209 }