一、核心代码分析说明
(1)Sever类——connect()方法:在建立与客户端的连接后,首先需要开启多线程,主线程是显示界面,新开的线程是对数据的处理,包括对客户信息的存储以及对客户发送过来信息的处理。
public static void connect() {
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try {
Socket connection = server.accept();
new Thread() {// 多线程
@Override
public void run() {
String client_mes = connection.getInetAddress().getHostName() + ":" + connection.getPort();
clientMap.put(client_mes, connection);// 把客户信息用hashMap存储起来 方便查找
item[count++] = client_mes;// 连接上的客户端的列表
String[] items = new String[count];
for (int k = 0; k < count; k++)
items[k] = item[k];
list.setListData(items);// 把列表放入组件中显示
lblNewLabel_1.setText(count + " client connect");// 更改界面中连接的客户数量
mes_receive(connection, client_mes);// 对客户发送过来信息的处理
}
}.start();
} catch (IOException ex) {
}
}
} catch (Exception ex) {
System.out.println("connect error");
System.err.println(ex);
}
}
(2)Sever类——mes_receive()方法:用一个while来不断的判断发送过来的信息是属于哪一类的,是talk、login、还是logout的,根据此进行不同的操作。
while (true) {
request = buffer_reader.readLine();
System.out.println("request: " + request + "\n");
if (request.startsWith("talk")) {
request = request.substring(4);
receive_mes_textArea.append(client_mes + " : " + request + "\n");
} else if (request.startsWith("logout")) {// 客户端退出登录
System.out.println("clients: " + clients);
request = request.substring(6);
clients = request;
clientMap.remove(clients);
for (int k = 0; k < count; k++) {
if (item[k].equals(clients)) {
for (int j = k; j < count; j++)
item[j] = item[j + 1];
item[count] = null;
count--;
System.out.println("count0: " + count);
break;
}
}
// for(String s:item)
// System.out.println(s);
String[] items = new String[count];
for (int k = 0; k < count; k++)
items[k] = item[k];
list.setListData(items);
receive_mes_textArea.append(clients + " is logout...\n");
System.out.println("count: " + count);
lblNewLabel_1.setText(count + " client connect");
}
(3)Sever类——按钮触发事件:当按了发送信息(Send Message)的按钮后,就根据选择的socket来进行消息的传送。
JButton btnNewButton = new JButton("Send Message");
btnNewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
String new_mes = Send_mes_textArea.getText();
for (int i = 0; i < client.size(); i++) {
Socket connection = clientMap.get(client.get(i));
receive_mes_textArea.append("I say to " + client.get(i) + " : " + new_mes + "\n");
out = new OutputStreamWriter(connection.getOutputStream());
out.write("talk" + new_mes + "\r\n");
out.flush();
}
} catch (Exception e1) {
System.out.println("bt error");
e1.printStackTrace();
}
}
});
btnNewButton.setBounds(130, 297, 122, 23);
contentPane.add(btnNewButton);
(4)Sever类——鼠标点击触发事件:这个算是相当重要的一个东西了,通过鼠标点击,确定选择要发送消息的客户端的地址,并把这些地址存储在ArrayList数组中,在后续的发送按钮触发事件的处理中相当有用。
JScrollPane scrollPane = new JScrollPane();
scrollPane.setBounds(20, 119, 232, 69);
list = new JList<String>();
contentPane.add(scrollPane);
list.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
client.clear();
client = list.getSelectedValuesList();
for (String s : client) {
System.out.println(s + "\n");
}
}
});
scrollPane.setViewportView(list);
(5)Sever类——窗口关闭的监听:当服务器的窗口关闭后,如果有客户端还连接着,就会给这些客户端发送logout的消息,然后客户端就会开始不断的监听,知道有服务器再次连接。
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
Socket s = null;
if (count == 0)
System.exit(0);
try {
for (int i = 0; i < count; i++) {
String client_mess = item[i];
s = clientMap.get(client_mess);
out = new OutputStreamWriter(s.getOutputStream());
out.write("logout" + "\r\n");
out.flush();
}
} catch (Exception ex) {
System.err.println(ex);
}
System.exit(0);
}
});
(6)Client类——main和connect()方法:main中的多线程,实现边处理界面边处理数据。connect()则负责连接,其中一个比较重要的一点是,在catch中,除了要捕捉异常并抛出外,还要重新调用connect的这个连接方法,为的就是让与服务器断开的客户端能够不断的监听,行程一种递归的机制,然后能在服务器出现时再次与它连上。(这是能让客户端与服务器断开后能重新连接上的重要的一步!!!)
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
Client frame = new Client();
frame.setTitle("Client");
frame.setVisible(true);
new Thread() {
@Override
public void run() {
connect();
text_content();
}
}.start();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public static void connect() {
try {
socket = new Socket("127.0.0.1", 10055);
out = new OutputStreamWriter(socket.getOutputStream());
in = socket.getInputStream();
} catch (IOException ex) {
System.err.println(ex);
lblNewLabel_1.setText("try connect...");
connect();
text_content();
}
}
(7)Client类——按钮触发事件:当按了发送信息(Send Message)的按钮后,就消息传送给服务器。
JButton btnNewButton = new JButton("Send Message");
btnNewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String new_mes = Send_mes_textArea.getText();
receive_mes_textArea.append("I say to server : " + new_mes + "\n");
try {
out.write("talk" + new_mes.toString() + "\r\n");
out.flush();
} catch (IOException ex) {
System.err.println(ex);
}
}
});
btnNewButton.setBounds(130, 297, 122, 23);
contentPane.add(btnNewButton);
(8)Client类——窗口关闭的监听:当服务器的窗口关闭后,就会给连接着的服务器发送logout的消息,让服务器知道该客户端断开了连接,然后就可以重新处理界面中显示的列表信息。
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
try {
String messs = socket.getInetAddress().getHostName() + ":" + socket.getLocalPort();
out.write("logout" + messs +"\r\n");
out.flush();
} catch (IOException ex) {
System.err.println(ex);
}
System.exit(0);
}
});
(9)Client类——text_content()方法:对服务器发送过来的消息的接收并分析是talk还是logout消息,并让消息在界面中显示。其中该方法跟connect中一样重要的是,在捕捉并抛出异常后还需要对connect()和text_content()两个方法进行递归,以不断地监听服务器。
![](/qrcode.jpg)
public static void text_content() {
try {
lblNewLabel_1.setText("Connecting...");
// receive_mes_textArea.append("Connect with Server...\n");
while (true) {
byte[] b = new byte[100];
int len = in.read(b);// m为实际读取的字节的数量
if (len != 0) {
String rece = new String(b, 0, len);
if (rece.startsWith("talk"))
receive_mes_textArea.append("server say : " + rece.substring(4) + "\n");
else if (rece.startsWith("logout")) {
receive_mes_textArea.append("out of connecting with server...\n");
}
}
}
} catch (Exception ex) {
System.err.println("与服务器断开连接 " + ex);
lblNewLabel_1.setText("try connect...");
connect();
text_content();
}
}
二、总结
1、我觉得这次的实验有几个关键点,把根源搞清楚就能很快且清晰的做完这个实验。
(1)客户端的信息存放问题:利用HashMap<String,Socket>(全局变量)来存储。
(2)客户端怎么与不断监听才能与服务器建立连接:利用类似于递归的方法,在连接connect()方法中的catch再次调用自己,知道能够连接上为止。
(3)如何给选择的客户端发送消息:利用鼠标点击事件,把点击后的值全部存储在ArrayList数组(全局变量)中,然后在发送按钮的触发事件中调用。
(4)对关闭窗口的监听:利用windowListener()
(5)多线程:为了可以边处理界面信息,边处理数据!!!
2、在这个实验过程中遇到一些应该注意的小错误如下:
(1)用mouseClicked鼠标点击事件时,写成了MouseClicked,但eclipse只warning,不报错,而且错误信息也很模棱两可,就很苦恼了,让我找错找了很久。
(2)在catch中最好定义Exception e,而不是IOException e,否则一些非相关性的错误不会显示,会在找错上花费时间。
(3)图形界面的方法调用和普通的方法的调用,要注意顺序,或者看是否需要用到多线程,否则很容易因为其中一个方法阻塞所以另一个方法没办法正常调用。
3、收获吧:
(1)对多线程有了更深了解
(2)界面方面,这次用的是Jframe,其中JScrollPane和JList的结合是新学习到的东西。
(3)其实整个实验过程,动手做一做,学到的东西还是不少的,关键还是要学会去总结,还有做的时候把思路理清,理解清楚在做,不然就会变成在bug中改bug,然后改都改不完了。
4、参考链接
https://blog.csdn.net/qq_37267015/article/details/54311188