1.说明
(1)这个Demo是网络系统实验课上的一次作业,题目是
实现一个高级聊天程序:一个服务器,多个客户端,服务器负责消息中转,客户端之间可以互相聊天。(广播/单播)
(2)本实验参考自一篇经典博文(http://blog.csdn.net/baolong47/article/details/6735853)
在这篇博文的基础上我重新梳理了部分程序架构,重新定义了客户机服务器通信的文本传输协议,并添加了客户机和客户机之间的私聊功能。
(3)运行环境
OSX10.10,JRE版本1.8
2.操作展示
(1)cd到实验目录下,一共有三个类,分别用javac编译三个类(Server.java, Client.java, User.java)。
注:编译器会提示警告,是JList缺少对DefaultListModel的泛型类型检查,我查了些资料还是没解决这个问题。幸好这个问题不影响程序运行,此Bug以后有待改进。
(2)java命令打开JVM运行Server和Client,本此实验支持单服务器多客户端,我打开了1个Server和3个Client作为演示。进程开启后界面如下:
服务器和客户端在未开启/连接的状态下无法发送消息只能修改配置,而在开启/连接状态下无法修改配置只能发送消息,UI逻辑均由开启/连接状态决定。
服务器配置中可以修改监听端口和人数上限,开启后接收所有来自客户端的消息,模仿解析包动作进行转发,服务器可以群发消息。
客户端可以设置自己的名字(注:本实验不涉及用户系统,名字随便取)、服务器IP和服务器端口(默认均是127.0.0.1本地IP和端口5555)。客户端连接后可以在在线用户列表中选择所有人或其他在线用户发送消息。其他用户上下线信息在在线用户列表和聊天消息框中都有提示。
(3)开启服务器并登陆客户端:
下图是最后一位登陆的用户6666登录时,从服务器接受其他用户,显示在左侧在线用户列表中。
(4)服务器群发消息:
下面是用户nihao收到的服务器消息,标记为群发:
(5)客户端群发消息:
客户端nihao选择所有人,并发送消息:
客户端gjc收到了消息,并标记为群发:
(6)客户端发送私聊消息:
客户端gjc和客户端6666背着客户端nihao私聊:
客户端6666收到来自gjc的私聊消息:
客户端nihao却不知道这条消息:
(6)客户端下线时所有在线用户收到其下线消息,在线列表中不再出现此用户:
客户端nihao下线,通知服务器,服务器转发其下线消息:
客户端gjc收到nihao的下线通知,左侧在线列表中已经没有nihao了:
(7)服务器的福利
服务器转发时需要解析用户消息的目的用户,顺带监视了所用用户的明文聊天记录!
(8)服务器关闭时通知其他用户自动下线
客户机收到服务器关闭信息后自动断开连接,不能发送消息:
(9)补充说明
程序可能还存在不少小bug,后期有时间再进行修复。目前发现的一个Bug是服务器关闭时,我关了服务器接收Socket请求的线程并close了该ServerSocket,但是该线程仍然继续执行了一次ServerSocket.accept()。我尝试用了synchronized方法并判断ServerSocket是否关闭,但这个异常还是会出现。我捕捉了该异常,仅仅printStackTrace而没有做其他错误处理,幸运的是这小Bug并不影响服务器关闭,所有客户端都能正确的接收服务器关闭的消息。
3.程序设计思想
(1)经典的TCP通信服务器客户端架构
服务器有一个服务器等待用户连接的线程,该线程循环等待客户端的TCP连接请求。一旦用ServerSocket.accept()捕捉到了连接请求,就为该TCP连接分配一个客户服务线程,通过该消息传递线程服务器与客户端通信。服务器发送消息通过该客户服务线程的方法在主线程完成,而接收消息全部在客户服务线程中循环接收并处理。
客户机能发起一个向服务器的socket连接请求,一旦收到服务器成功响应连接请求客户机便为这个socket分配一个消息接收线程,否则关闭该socket。和服务器任务分配类似,发送消息作为非常用方法在主线程中完成,而接收消息在消息接收线程中不停刷新并作相应处理。
(2)统一ASCII码级文本传输协议
为了实现客户机对服务器命令的响应、服务器对客户机需求的解读以及客户机与客户机之间的消息传递,我为服务器和客户端之间通信定义了一组文本传输协议。协议属于变长文本传输协议,用@作为各字段分隔符,所有消息的首节一定是消息类型,方便解析。协议定义了以下按发送方分类的消息格式
(3)MVC分层模式
Model-View-Controller是经典的应用程序开发设计模式,它讲数据管理、界面显示和用户交互、程序维护管理分别封装在MVC三种类中,够成松耦合关系。本次实验中我也利用MVC的设计思路,独立了Model类User用于保存客户机用户信息,DefaultListModel模型类用于储存在线用户队列;而View并没有单独放在一个类中,而是在Controller类Server和Client中以Java Swing的控件形式独立开来。Controller监听用户操作事件,反映给Model类处理并在View中更新。
MVC的思想即是M和V之间不要直接产生联系,业务逻辑均封装在MC中,而V仅仅负责显示。本实验为V类绑定了各自的Listener监听用户操作,在C中完成业务逻辑处理,保存并更新User和DefaultListModel,最后再显示到UI界面上。
(4)concurrentHashMap管理线程队列和用户列表
concurrentHashMap是java.util.concurrent包中定义的多线程安全的哈希表,利用哈希表管理线程队列和用户列表可以快速索引,多线程安全也保证了多个用户服务线程之间共享资源的数据一致性。
4.源代码
(1)User.java
public class User {
private String name;
private String ipAddr;
public User(String userDescription) {
String items[] = userDescription.split("%");
this.name = items[0];
this.ipAddr = items[1];
}
public User(String name, String ipAddr) {
this.name = name;
this.ipAddr = ipAddr;
}
public String getName() {
return name;
}
public String getIpAddr() {
return ipAddr;
}
public String description() {
return name + "%" + ipAddr;
}
}
import javax.swing.*;
import javax.swing.border.*;
import java.util.*;
import java.util.concurrent.*;
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
/*
message type
1.alias:
USER <= USER_NAME%USER_IPADDR
2.format:
server:
MSG @ to @ from @ content
ALL SERVER xxx
ALL USER xxx
USER USER xxx
LOGIN @ status @ content
SUCCESS xxx
FAIL xxx
USER @ type @ other
ADD USER
DELETE USER
LIST number {@ USER}+
ERROR @ TYPE
CLOSE
client:
MSG @ to @ from @ content
ALL USER xxx
USER USER xxx
LOGOUT
LOGIN @ USER
*/
public class Server {
//UI
private JFrame frame;
private JPanel settingPanel, messagePanel;
private JSplitPane centerSplitPanel;
private JScrollPane userPanel, logPanel;
private JTextArea logTextArea;
private JTextField maxClientTextField, portTextField, serverMessageTextField;
private JButton startButton, stopButton, sendButton;
private JList userList;
//Model
private DefaultListModel listModel;
//Socket
private ServerSocket serverSocket;
//Status
private boolean isStart = false;
private int maxClientNum;
//Threads
//ArrayList clientServiceThreads;
ConcurrentHashMap clientServiceThreads;
ServerThread serverThread;
public Server() {
initUI();
}
private void startServer() {
int port;
try {
port = Integer.parseInt(portTextField.getText().trim());
} catch(NumberFormatException e) {
showErrorMessage("端口号必须为整数!");
return;
}
if (port < 1024 || port > 65535) {
showErrorMessage("端口号必须在1024~65535之间");
return;
}
try {
maxClientNum = Integer.parseInt(maxClientTextField.getText().trim());
} catch(NumberFormatException e) {
showErrorMessage("人数上限必须是正整数!");
maxClientNum = 0;
return;
}
if (maxClientNum <= 0) {
showErrorMessage("人数上限必须是正整数!");
maxClientNum = 0;
return;
}
try {
clientServiceThreads = new ConcurrentHashMap();
serverSocket = new ServerSocket(port);
serverThread = new ServerThread();
serverThread.start();
isStart = true;
} catch (BindException e) {
isStart = false;
showErrorMessage("启动服务器失败:端口被占用!");
return;
} catch (Exception e) {
isStart = false;
showErrorMessage("启动服务器失败:启动异常!");
e.printStackTrace();
return;
}
logMessage("服务器启动:人数上限:" + maxClientNum + " 端口号:" + port);
serviceUISetting(true);
}
private synchronized void stopServer() {
try {
serverThread.closeThread();
for (Map.Entry entry : clientServiceThreads.entrySet()) {
ClientServiceThread clientThread = entry.getValue();
clientThread.sendMessage("CLOSE");
clientThread.close();
}
clientServiceThreads.clear();
listModel.removeAllElements();
isStart = false;
serviceUISetting(false);
logMessage("服务器已关闭!");
} catch(Exception e) {
e.printStackTrace();
showErrorMessage("关闭服务器异常!");
isStart = true;
serviceUISetting(true);
}
}
private void sendAll() {
if (!isStart) {
showErrorMessage("服务器还未启动,不能发送消息!");
return;
}
if (clientServiceThreads.size() == 0) {
showErrorMessage("没有用户在线,不能发送消息!");
return;
}
String message = serverMessageTextField.getText().trim();
if (message == null || message.equals("")) {
showErrorMessage("发送消息不能为空!");
return;
}
for (Map.Entry entry : clientServiceThreads.entrySet()) {
entry.getValue().sendMessage("MSG@ALL@SERVER@" + message);
}
logMessage("Server: " + message);
serverMessageTextField.setText(null);
}
private void logMessage(String msg) {
logTextArea.append(msg + "\r\n");
}
private void showErrorMessage(String msg) {
JOptionPane.showMessageDialog(frame, msg, "Error", JOptionPane.ERROR_MESSAGE);
}
//UI Settings!
private void initUI() {
frame = new JFrame("服务器");
frame.setSize(600, 400);
frame.setResizable(false);
frame.setLayout(new BorderLayout());
//setting panel
maxClientTextField = new JTextField("10");
portTextField = new JTextField("5555");
startButton = new JButton("启动");
stopButton = new JButton("停止");
settingPanel = new JPanel();
settingPanel.setLayout(new GridLayout(1, 6));
settingPanel.add(new JLabel("人数上限"));
settingPanel.add(maxClientTextField);
settingPanel.add(new JLabel("端口号"));
settingPanel.add(portTextField);
settingPanel.add(startButton);
settingPanel.add(stopButton);
settingPanel.setBorder(new TitledBorder("服务器配置"));
//user panel
listModel = new DefaultListModel();
userList = new JList(listModel);
userPanel = new JScrollPane(userList);
userPanel.setBorder(new TitledBorder("在线用户"));
//server log info
logTextArea = new JTextArea();
logTextArea.setEditable(false);
logTextArea.setForeground(Color.blue);
logPanel = new JScrollPane(logTextArea);
logPanel.setBorder(new TitledBorder("服务器日志"));
//server message
serverMessageTextField = new JTextField();
sendButton = new JButton("发送");
messagePanel = new JPanel(new BorderLayout());
messagePanel.add(serverMessageTextField, "Center");
messagePanel.add(sendButton, "East");
messagePanel.setBorder(new TitledBorder("广播消息"));
//add to frame
centerSplitPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, userPanel, logPanel);
centerSplitPanel.setDividerLocation(100);
frame.add(settingPanel, "North");
frame.add(centerSplitPanel, "Center");
frame.add(messagePanel, "South");
frame.setVisible(true);
addActionListenersToUI();
serviceUISetting(false);
}
private void serviceUISetting(boolean started) {
maxClientTextField.setEnabled(!started);
portTextField.setEnabled(!started);
startButton.setEnabled(!started);
stopButton.setEnabled(started);
serverMessageTextField.setEnabled(started);
sendButton.setEnabled(started);
}
private void addActionListenersToUI() {
serverMessageTextField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
sendAll();
}
});
sendButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
sendAll();
}
});
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!isStart) {
startServer();
}
}
});
stopButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (isStart) {
stopServer();
}
}
});
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
if (isStart) {
stopServer();
}
System.exit(0);
}
});
}
//Server Thread class
private class ServerThread extends Thread {
private boolean isRunning;
public ServerThread() {
this.isRunning = true;
}
public void run() {
while (this.isRunning) {
//System.out.println("server recieving connection request!");
try {
if (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
if (clientServiceThreads.size() == maxClientNum) {
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println("LOGIN@FAIL@对不起,服务器在线人数已达到上限,请稍候尝试!");
writer.flush();
writer.close();
socket.close();
} else {
ClientServiceThread clientServiceThread = new ClientServiceThread(socket);
User user = clientServiceThread.getUser();
clientServiceThreads.put(user.description(), clientServiceThread);
listModel.addElement(user.getName());
logMessage(user.description() + "上线...");
clientServiceThread.start();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
public synchronized void closeThread() throws IOException {
this.isRunning = false;
serverSocket.close();
System.out.println("serverSocket close!!!");
}
}
//Client Thread class
private class ClientServiceThread extends Thread {
private Socket socket;
private User user;
private BufferedReader reader;
private PrintWriter writer;
private boolean isRunning;
private synchronized boolean init() {
try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new PrintWriter(socket.getOutputStream());
String info = reader.readLine();
StringTokenizer tokenizer = new StringTokenizer(info, "@");
String type = tokenizer.nextToken();
if (!type.equals("LOGIN")) {
sendMessage("ERROR@MESSAGE_TYPE");
return false;
}
user = new User(tokenizer.nextToken());
sendMessage("LOGIN@SUCCESS@" + user.description() + "与服务器连接成功!");
int clientNum = clientServiceThreads.size();
if (clientNum > 0) {
//tell this client who else are online
StringBuffer buffer = new StringBuffer();
buffer.append("@");
for (Map.Entry entry : clientServiceThreads.entrySet()) {
ClientServiceThread serviceThread = entry.getValue();
buffer.append(serviceThread.getUser().description() + "@");
//tell other users that this user is online
serviceThread.sendMessage("USER@ADD@" + user.description());
}
sendMessage("USER@LIST@" + clientNum + buffer.toString());
}
return true;
} catch(Exception e) {
e.printStackTrace();
return false;
}
}
public ClientServiceThread(Socket socket) {
this.socket = socket;
this.isRunning = init();
if (!this.isRunning) {
logMessage("服务线程开启失败!");
}
}
public void run() {
while (isRunning) {
try {
String message = reader.readLine();
System.out.println("recieve message: " + message);
if (message.equals("LOGOUT")) {
logMessage(user.description() + "下线...");
int clientNum = clientServiceThreads.size();
//tell other users that this user is offline
for (Map.Entry entry : clientServiceThreads.entrySet()) {
entry.getValue().sendMessage("USER@DELETE@" + user.description());
}
//remove this user and service thread
listModel.removeElement(user.getName());
clientServiceThreads.remove(user.description());
System.out.println(user.description() + " logout, now " + listModel.size() + " client(s) online...(" + clientServiceThreads.size() + " Thread(s))");
close();
return;
} else {
dispatchMessage(message);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
public void dispatchMessage(String message) {
StringTokenizer tokenizer = new StringTokenizer(message, "@");
String type = tokenizer.nextToken();
if (!type.equals("MSG")) {
sendMessage("ERROR@MESSAGE_TYPE");
return;
}
String to = tokenizer.nextToken();
String from = tokenizer.nextToken();
String content = tokenizer.nextToken();
logMessage(from + "->" + to + ": " + content);
if (to.equals("ALL")) {
//send to everyone
for (Map.Entry entry : clientServiceThreads.entrySet()) {
entry.getValue().sendMessage(message);
}
} else {
//send to some one
if (clientServiceThreads.containsKey(to)) {
clientServiceThreads.get(to).sendMessage(message);
} else {
sendMessage("ERROR@INVALID_USER");
}
}
}
public void close() throws IOException {
this.isRunning = false;
this.reader.close();
this.writer.close();
this.socket.close();
}
public void sendMessage(String message) {
writer.println(message);
writer.flush();
}
public User getUser() {
return user;
}
}
public static void main(String args[]) {
new Server();
}
}
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import java.util.*;
import java.util.concurrent.*;
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
public class Client {
//UI
private JFrame frame;
private JPanel settingPanel, messagePanel;
private JSplitPane centerSplitPanel;
private JScrollPane userPanel, messageBoxPanel;
private JTextArea messageTextArea;
private JTextField nameTextField, ipTextField, portTextField, messageTextField;
private JLabel messageToLabel;
private JButton connectButton, disconnectButton, sendButton;
private JList userList;
//Model
private DefaultListModel listModel;
private User me;
private ConcurrentHashMap onlineUsers = new ConcurrentHashMap();
private String sendTarget = "ALL";
//Socket
private Socket socket;
private PrintWriter writer;
private BufferedReader reader;
//Thread
private MessageThread messageThread;
//Status
private boolean isConnected;
public Client() {
initUI();
}
private void connect() {
int port;
try {
port = Integer.parseInt(portTextField.getText().trim());
} catch(NumberFormatException e) {
showErrorMessage("端口号必须为整数!");
return;
}
if (port < 1024 || port > 65535) {
showErrorMessage("端口号必须在1024~65535之间");
return;
}
String name = nameTextField.getText().trim();
if (name == null || name.equals("")) {
showErrorMessage("名字不能为空!");
return;
}
String ip = ipTextField.getText().trim();
if (ip == null || ip.equals("")) {
showErrorMessage("IP不能为空!");
return;
}
try {
listModel.addElement("所有人");
me = new User(name, ip);
socket = new Socket(ip, port);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new PrintWriter(socket.getOutputStream());
String myIP = socket.getLocalAddress().toString().substring(1);
//System.out.println(myIP);
sendMessage("LOGIN@" + name + "%" + myIP);
messageThread = new MessageThread();
messageThread.start();
isConnected = true;
} catch(Exception e) {
isConnected = false;
logMessage("客户端连接失败");
listModel.removeAllElements();
e.printStackTrace();
return;
}
logMessage("客户端连接成功");
serviceUISetting(isConnected);
}
private void send() {
if (!isConnected) {
showErrorMessage("未连接到服务器!");
return;
}
String message = messageTextField.getText().trim();
if (message == null || message.equals("")) {
showErrorMessage("消息不能为空!");
return;
}
String to = sendTarget;
try {
sendMessage("MSG@" + to + "@" + me.description() + "@" + message);
logMessage("我->" + to + ": " + message);
} catch(Exception e) {
e.printStackTrace();
logMessage("(发送失败)我->" + to + ": " + message);
}
messageTextField.setText(null);
}
private synchronized void disconnect() {
try {
sendMessage("LOGOUT");
messageThread.close();
listModel.removeAllElements();
onlineUsers.clear();
reader.close();
writer.close();
socket.close();
isConnected = false;
serviceUISetting(false);
sendTarget = "ALL";
messageToLabel.setText("To: 所有人");
logMessage("已断开连接...");
} catch(Exception e) {
e.printStackTrace();
isConnected = true;
serviceUISetting(true);
showErrorMessage("服务器断开连接失败!");
}
}
private void sendMessage(String message) {
writer.println(message);
writer.flush();
}
private void logMessage(String msg) {
messageTextArea.append(msg + "\r\n");
}
private void showErrorMessage(String msg) {
JOptionPane.showMessageDialog(frame, msg, "Error", JOptionPane.ERROR_MESSAGE);
}
private void initUI() {
frame = new JFrame("客户端");
frame.setSize(600, 400);
frame.setResizable(false);
frame.setLayout(new BorderLayout());
//setting panel
ipTextField = new JTextField("127.0.0.1");
portTextField = new JTextField("5555");
nameTextField = new JTextField("");
connectButton = new JButton("连接");
disconnectButton = new JButton("断开");
settingPanel = new JPanel();
settingPanel.setLayout(new GridLayout(1, 8));
settingPanel.add(new JLabel(" 名字:"));
settingPanel.add(nameTextField);
settingPanel.add(new JLabel("服务器IP地址:"));
settingPanel.add(ipTextField);
settingPanel.add(new JLabel("服务器端口号:"));
settingPanel.add(portTextField);
settingPanel.add(connectButton);
settingPanel.add(disconnectButton);
settingPanel.setBorder(new TitledBorder("客户端配置"));
//user panel
listModel = new DefaultListModel();
userList = new JList(listModel);
userPanel = new JScrollPane(userList);
userPanel.setBorder(new TitledBorder("在线用户"));
//server log info
messageTextArea = new JTextArea();
messageTextArea.setEditable(false);
messageTextArea.setForeground(Color.blue);
messageBoxPanel = new JScrollPane(messageTextArea);
messageBoxPanel.setBorder(new TitledBorder("接收消息"));
//server message
messageToLabel = new JLabel("To:所有人 ");
messageTextField = new JTextField();
sendButton = new JButton("发送");
messagePanel = new JPanel(new BorderLayout());
messagePanel.add(messageToLabel, "West");
messagePanel.add(messageTextField, "Center");
messagePanel.add(sendButton, "East");
messagePanel.setBorder(new TitledBorder("发送消息"));
//add to frame
centerSplitPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, userPanel, messageBoxPanel);
centerSplitPanel.setDividerLocation(100);
frame.add(settingPanel, "North");
frame.add(centerSplitPanel, "Center");
frame.add(messagePanel, "South");
frame.setVisible(true);
addActionListenersToUI();
serviceUISetting(false);
}
private void addActionListenersToUI() {
messageTextField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
send();
}
});
sendButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
send();
}
});
connectButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!isConnected) {
connect();
}
}
});
disconnectButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (isConnected) {
disconnect();
}
}
});
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
if (isConnected) {
disconnect();
}
System.exit(0);
}
});
userList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
int index = userList.getSelectedIndex();
if (index < 0) return;
//System.out.print(index + ": ");
if (index == 0) {
sendTarget = "ALL";
messageToLabel.setText("To: 所有人");
} else {
String name = (String)listModel.getElementAt(index);
if (onlineUsers.containsKey(name)) {
sendTarget = onlineUsers.get(name).description();
messageToLabel.setText("To: " + name);
} else {
sendTarget = "ALL";
messageToLabel.setText("To: 所有人");
}
}
//System.out.println(sendTarget);
}
});
}
private void serviceUISetting(boolean connected) {
nameTextField.setEnabled(!connected);
ipTextField.setEnabled(!connected);
portTextField.setEnabled(!connected);
connectButton.setEnabled(!connected);
disconnectButton.setEnabled(connected);
messageTextField.setEnabled(connected);
sendButton.setEnabled(connected);
}
private class MessageThread extends Thread {
private boolean isRunning = false;
public MessageThread() {
isRunning = true;
}
public void run() {
while (isRunning) {
try {
String message = reader.readLine();
StringTokenizer tokenizer = new StringTokenizer(message, "@");
String command = tokenizer.nextToken();
if (command.equals("CLOSE")) {
logMessage("服务器已关闭,正在断开连接...");
disconnect();
isRunning = false;
return;
} else if (command.equals("ERROR")) {
String error = tokenizer.nextToken();
logMessage("服务器返回错误,错误类型:" + error);
} else if (command.equals("LOGIN")) {
String status = tokenizer.nextToken();
if (status.equals("SUCCESS")) {
logMessage("登录成功!" + tokenizer.nextToken());
} else if (status.equals("FAIL")) {
logMessage("登录失败,断开连接!原因:" + tokenizer.nextToken());
disconnect();
isRunning = false;
return;
}
} else if (command.equals("USER")) {
String type = tokenizer.nextToken();
if (type.equals("ADD")) {
String userDescription = tokenizer.nextToken();
User newUser = new User(userDescription);
onlineUsers.put(newUser.getName(), newUser);
listModel.addElement(newUser.getName());
logMessage("新用户(" + newUser.description() + ")上线!");
} else if (type.equals("DELETE")) {
String userDescription = tokenizer.nextToken();
User deleteUser = new User(userDescription);
onlineUsers.remove(deleteUser.getName());
listModel.removeElement(deleteUser.getName());
logMessage("用户(" + deleteUser.description() + ")下线!");
if (sendTarget.equals(deleteUser.description())) {
sendTarget = "ALL";
messageToLabel.setText("To: 所有人");
}
} else if (type.equals("LIST")) {
int num = Integer.parseInt(tokenizer.nextToken());
for (int i = 0; i < num; i++) {
String userDescription = tokenizer.nextToken();
User newUser = new User(userDescription);
onlineUsers.put(newUser.getName(), newUser);
listModel.addElement(newUser.getName());
logMessage("获取到用户(" + newUser.description() + ")在线!");
}
}
} else if (command.equals("MSG")) {
StringBuffer buffer = new StringBuffer();
String to = tokenizer.nextToken();
String from = tokenizer.nextToken();
String content = tokenizer.nextToken();
buffer.append(from);
if (to.equals("ALL")) {
buffer.append("(群发)");
}
buffer.append(": " + content);
logMessage(buffer.toString());
}
} catch(Exception e) {
e.printStackTrace();
logMessage("接收消息异常!");
}
}
}
public void close() {
isRunning = false;
}
}
public static void main(String[] args) {
new Client();
}
}
5.程序详细分析
实验要求中看见需要分析代码所以这里再补充一下
1.User模型类
(1)构造方法:
有两个,一个是用独立的name和ip实例化一个User,另一个是用name%ip拼接而成的字符串实例化User
(2)只读字段
name和ipAddr均是private的,给他们配置一个只读的getter
(3)description()用户描述
返回name%ip拼接而成的字符串,用以代表一个独立的用户
2.Server类
(1)UI相关的方法
构造函数中的initUI()大部分是设置UI界面,其中用到了GridLayout和BorderLayout。最后调用了addActionListenersToUI()来绑定监听器,并且用serviceUISetting(false)把所有连接状态才起作用的button和textField全部关闭了(false改为true开启他们,并关闭所有设置相关的button和textField)。
(2)startServer()开启服务器方法———startButton绑定
先检查maxClientNum和port的合法输入,如果不合法弹出出错窗口并退出。
接着初始化管理客户服务线程队列的并发哈希表clientServiceThreads,初始化监听客户机连接请求的serverSocket,并且初始化和开启一个监听连接请求的线程。最后有一些差错处理以及服务器log日志。
(3)请求监听线程ServerThread类
isRunning作为线程运行标志位控制线程存活,线程start后会调用的函数run()里完成了监听逻辑。如果开启则一直循环,serverSocket.accept()是阻塞的,线程不会运行直到有其他线程/进程向其请求Socket连接。这也是我之前提到的一个bug的原因:accept()阻塞了线程它一直在等待,仅仅用标志位来结束线程并不能使之跳出阻塞状态(还没有循环到下一次while的判断),因此我在closeThread()中强行关闭serverSocket会报出一个异常!
收到连接请求后accept()返回一个socket,这个socket用于和请求连接的客户机通信。至此时TCP建立连接3次握手已经完成,全部被serverSocket类封装起来了。获取了通信socket之后检查服务器在线人数是否已满,向客户机发送一个登陆成功或失败的消息。若在线人数未满连接成功,则为客户机分配一个clientServiceThread线程专门用于发送和接受客户机的TCP包。
(4)监听客户机消息的ClientServiceThread线程类
该类比较庞大,我挑重点介绍
[1]关键字段
private Socket socket;
private User user;
private BufferedReader reader;
private PrintWriter writer;
private boolean isRunning;
分别保存了通信socket、当前连接用户Model、绑定在socket输入流上的BufferedReader、绑定在socket输出流上的PrintWriter以及线程运行控制标志位isRunning。reader用来读取客户机消息输入,readLine方法也是阻塞的,直到客户机有消息发送过来。writer有一个写缓冲区,有flush()函数强制发送消息并刷新缓冲区,我把写消息封装在sendMessage(String)中。
[2]初始化
初始化中先绑定reader和writer到socket响应流,在判断用户socket请求发送的消息格式是否正确(不正确线程将不能执行)。接着向所有已上线的用户通知一遍这个新用户上线了,发送通知需要遍历整个服务线程队列并发送文本传输协议中定义的各式的通知。注意到这时候该服务线程并没有加入到服务线程队列中,是在初始化完成之后加入的。
通知了其他用户这个新客户机上线后,再告诉该客户机现在已经有哪些用户在线了,这也是用协议中的格式发送通知即可。这里用到了StringBuffer类,多字符串连接时该类比String的+的效率要高。
[3]线程run
收到客户机消息后判断消息类型,若是LOGOUT通知客户机下线,则向所有其他客户机进程发送该用户下线的信息,并删除model类里的该用户对象和线程队列里的该线程。
如果是消息则交与dispatchMessage(String)方法专门分发消息。
[4]分发消息方法dispatchMessage(String)
该方法解析MSG类消息的to字段,根据to字段选择是将消息发给特定用户还是直接群发。发给特定用户的话根据to字段(userDescription)做索引,快速从服务线程队列找出服务该用户客户机的线程来发送信息。
[5]其他的绑定时间如stopServer关闭服务器和sendAll群发消息都比较直白便省略介绍,主要需要注意一下其中的差错控制。关闭服务器还需要更新UI控制逻辑。
[6]说明:Server类虽然通过ClientServiceThread里的writer发送消息,并且也是调用封装在这个Thread内部类中的,但是调用writer来sendMessage并不是一定在该线程内完成的(该线程内指的是run()里的while循环内部),sendMessage是非阻塞的我们没有必要专门在线程中执行。ClientServiceThread主要工作是收听各个客户端向服务器发送的消息。
3.Client类
Client和Server稍微有点不一样,只有一个辅助线程MessageThread用于接收服务器消息。由于只需要绑定在一个socket上,所以writer和reader只有一个,是直接属于Client实例的字段。
[1]UI相关方法
构造函数里的init和Server中几乎完全一样,这部分属于代码复用。注意需要多绑定一个监听器:
javax.swing.event.ListSelectionListener类用来监听用户选择JList框里的条目,JList框里固定一个所有人的项(点击选中表示消息发送给所有人,默认发送给所有人,目标对象下线后也是自动把对象转变成所有人),其他则是在线用户。点击这些列表项时触发一个选择事件,通过判断index来判断用户的选择,并更新模型记录sendTarget和UI中messageToLabel显示的text。
[2]connect连接到服务器
差错检测这里没有判断ip地址合法性,判断也不是很麻烦。用户输入合法时,根据服务器IP地址和端口实例化一个socket,这个socket用于将来和服务器通信。
获取客户机本地IP地址并用这个IP地址实例化,通过socket给服务器发送一条自己用户信息(name和ip)的消息表示请求。发送完毕后立即开启MessageThread等待服务器的回应。
[3]MessageThread接受服务器消息线程
reader.readLine()阻塞读取服务器消息。一直忘记介绍StringTokenizer类,这里说明一下。StringTokenizer类通过一个String和一个分割字符串实例或一个tokenizer,通过分割得到一系列记号流通过tokenizer.nextToken()获取这些记号字符串。不难发现其作用和String.split(String)一样也是做字符串分割,但是其效率显著优于split方法(百度搜索两者比较会有较详细的性能分析)。
根据tokenizer返回的记号流我们来判断消息类型,
服务器关闭:向服务器发送一个下线信息,关闭socket和wr,清空记录Model,最后退出线程。
服务器错误:log错误类型,啥也不干进入下一轮循环。
登陆信息,
成功:log成功,进入下一轮循环。
失败:log失败,关闭socket和wr,清空记录Model,最后退出线程。
用户操作,
添加新用户:log信息,更新Model类和UI绘制(添加一条)。
删除某用户:log信息,更新Model类和UI绘制(删除一条)。
获取所有在线用户列表:根据num字段一个一个获取token流,全部添加到Model类中并log和更新UI。
[4]其他方法disconnect和send比较简单,也不讲了,同Server一样提醒差错控制。我不一定考虑了所有可能出现的bug,因此程序不是完全完善的产品,只能做一个demo。