其实就是我们的一个Java实验:
TCP聊天程序。结合多线程设计方法写出聊天程序,使得可以在服务器端同时与多个客户端同时聊天。这些客户允许在不同的计算机上。
具体要求:
1) 可以多个客户使用同一套客户端代码工作,多个客户同时上线。
2)服务器总是监听客户连接请求。客户首先输入自己的姓名,再向服务器发出连接请求。当服务器监听到客户的连接请求后,服务器为该客户创建一个服务线程。双方连接成功后,客户端首先发送自己的名字给服务器,服务器收到客户名字后,向客户发送回答。然后开始双方的聊天。
3)因为服务器要同时跟多个客户聊天,所以,服务器要指定每一次的发言字符串是发给哪一个在线客户的。服务器要监测哪些客户在线。
4)当客户想结束聊天时,直接关闭聊天窗口。
我的不成熟想法:
客服端代码:
import java.io.*;
import java.net.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class SERVER extends JFrame implements Runnable, ActionListener {
private JButton sendMassage;
private JTextField inputmassage;
private JTextArea chatText;
private ServerSocket server = null;
private Socket socket = null;
private DataInputStream inNet = null;
private DataOutputStream outNet = null;
private Thread receiveThread;
public SERVER() {
setTitle("服务器");
JPanel p1 = new JPanel();
JPanel p2 = new JPanel();
sendMassage = new JButton("发送信息");
chatText = new JTextArea(6, 42);
inputmassage = new JTextField(16);
p2.add(inputmassage);
p2.add(sendMassage);
add(chatText, BorderLayout.CENTER);
add(p2, BorderLayout.SOUTH);
sendMassage.addActionListener(this);
receiveThread = new Thread(this);
setBounds(10, 30, 650, 400);
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
validate();
try {
server = new ServerSocket(8888);
chatText.append("等待客户呼叫...\n");
socket = server.accept();
inNet = new DataInputStream(socket.getInputStream());
outNet = new DataOutputStream(socket.getOutputStream());
outNet.writeUTF("您好,请问您的名字是?");
receiveThread.start();
} catch (IOException ex) {
JOptionPane.showMessageDialog(this, "建立服务失败!");
System.exit(0);
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == sendMassage) {
String msg = inputmassage.getText().trim();
if (msg.equals("")) {
JOptionPane.showMessageDialog(this, "请输入消息内容");
return;
}
try {
outNet.writeUTF("服务器: " + msg);
chatText.append("本机: " + "\n" + msg + "\n");
inputmassage.setText("");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
@Override
public void run() {
try {
String msg = "";
while (true) {
msg = inNet.readUTF();
if (!msg.equals("")) {
if (getTitle().equals("服务器")) {
setTitle(msg);
outNet.writeUTF("欢迎你" + msg + "!");
} else {
chatText.append("客服: " + msg + "\n");
}
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
public static void main(String args[]) {
new SERVER();
}
}
客户端代码:
import java.io.*;
import java.net.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class CLIENT extends JFrame implements Runnable, ActionListener {
private JButton connection, sendName, sendMassage;
private JTextField inputName, inputmassage;
private JTextArea chatText;
private Socket socket = null;
private DataInputStream inNet = null;
private DataOutputStream outNet = null;
private Thread receiveThread;
private boolean connected = false;
private String name = "";
public CLIENT() {
setTitle("客户端");
JPanel p1 = new JPanel();
JPanel p2 = new JPanel();
connection = new JButton("连接服务器");
sendName = new JButton("发送姓名");
sendMassage = new JButton("发送信息");
inputName = new JTextField(8);
chatText = new JTextArea(6, 42);
inputmassage = new JTextField(16);
p1.add(new JLabel("请输入您的姓名:"));
p1.add(inputName);
p1.add(sendName);
p1.add(connection);
p2.add(inputmassage);
p2.add(sendMassage);
add(p1, BorderLayout.NORTH);
add(new JScrollPane(chatText), BorderLayout.CENTER);
add(p2, BorderLayout.SOUTH);
connection.addActionListener(this);
sendName.addActionListener(this);
sendMassage.addActionListener(this);
receiveThread = new Thread(this);
setBounds(10, 30, 650, 400);
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
validate();
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == connection) {
if (!connected) {
try {
socket = new Socket("127.0.0.1", 8888);
inNet = new DataInputStream(socket.getInputStream());
outNet = new DataOutputStream(socket.getOutputStream());
connection.setText("断开连接");
connected = true;
chatText.append("成功连接服务器\n");
} catch (IOException ex) {
JOptionPane.showMessageDialog(this, "连接服务器失败");
}
} else {
try {
socket.close();
inNet.close();
outNet.close();
connection.setText("连接服务器");
connected = false;
sendName.setEnabled(false);
sendMassage.setEnabled(false);
chatText.append("成功断开与服务器的连接\n");
} catch (IOException ex) {
ex.printStackTrace();
}
}
} else if (e.getSource() == sendName) {
if (name.equals("")) {
name = inputName.getText();
if (name.equals("")) {
JOptionPane.showMessageDialog(this, "请先输入姓名");
return;
}
inputName.setEditable(false);
sendName.setEnabled(false);
sendMassage.setEnabled(true);
try {
outNet.writeUTF(name);
chatText.append("成功发送姓名\n");
} catch (IOException ex) {
ex.printStackTrace();
}
}
} else if (e.getSource() == sendMassage) {
String msg = inputmassage.getText().trim();
if (msg.equals("")) {
JOptionPane.showMessageDialog(this, "请输入消息内容");
return;
}
try {
outNet.writeUTF(msg);
chatText.append(name +"\n" + ": " + msg + "\n");
inputmassage.setText("");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
@Override
public void run() {
while (true) {
try {
String msg = inNet.readUTF();
if (!msg.equals("")) {
if (name.equals("")) {
name = msg;
chatText.append("欢迎你!" + "\n" + name + "!\n");
sendName.setEnabled(false);
sendMassage.setEnabled(true);
} else {
chatText.append("客服: " + "\n" + msg + "\n");
}
}
} catch (IOException ex) {
ex.printStackTrace();
break;
}
}
}
public static void main(String args[]) {
new CLIENT();
}
}
运行结果其实还好,有以下几个优点:
1:要求一都实现了。
2:没创建线程,本来是想在客服端加上“一个客户创建一个线程,创建一个窗口”功能,但是没实现成功
3:要求三超额完成,不仅只能单独聊天,而且还只能单向发送。
4:要求四完成了, 还在客户端连接服务器后未关闭服务器端就关闭客户端时附赠了报错大礼包:
以下是大体运行结果,错误都写在里面了(消息格式可以不用在意):
那么final修饰的静态字段是不是也是这样的呢?
不难发现直接就编译报错了, 其实也好理解, 静态的成员变量在类加载进方法区的类代码区时就已经在静态数据区中开辟了空间, 而此时并没有创建对象也就并不能调用构造方法, 没法在该字段开辟空间后为其初始化, 所以以上面的理解, final修饰的字段在开辟空间后必须得保证它会被显式赋值一次且只被赋值一次的结论显然是不符合的, 所以final修饰的静态字段只能被静态初始化块或者显示初始化初始化, 同样的也只能被初始化一次.
以上都为个人理解, 若有错误或不足还请大佬指出指教
这是一个比较典型的Java网络编程案例,需要实现多线程和多客户端聊天功能。下面是具体的解决方案:
在服务器端,每当有一个客户端连接上来之后,就需要为这个客户端创建一个服务线程。所以我们可以将之前的逻辑封装到一个ServerThread
类中,并让其实现Runnable
接口,这样每当有新的客户端连接上来时,我们就可以为其创建一个ServerThread
对象,然后调用start()
方法启动服务线程。
我们需要对代码进行一定的改动: * 服务器首先需要监听客户连接请求。当客户端发起连接请求并成功连接服务器后,服务器为该客户端分配一个线程来进行服务。因为需要不断地接收数据、处理数据并返回结果,所以我们需要使用while循环,不断地接收客户端发来的消息,处理后再返回给客户端。 * 客户端先输入自己的姓名,再向服务器发出连接请求。客户端与服务器成功连接后,需要向服务器发送自己的姓名。客户输入的每一条消息需要将发送方、接收方、消息内容以一定的格式发送给服务器。 * 还需要为每一个在线的客户端分配一个Socket对象,这样才能够向指定的/socket发送消息。
使用Map来存储每个在线客户的信息,包括客户姓名和对应的Socket对象。
实现客户端的图形化界面可以提供更好的用户体验。可以使用Swing、JavaFX等GUI库来完成客户端的开发。
下面是修改后的ChatServer
和ServerThread
代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class ChatServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("Server started ...");
Map<String, Socket> clientMap = new HashMap<String, Socket>();
while (true) {
Socket socket = serverSocket.accept();
new Thread(new ServerThread(socket, clientMap)).start();
}
}
}
class ServerThread implements Runnable {
private Socket socket;
private Map<String, Socket> clientMap;
private String clientName;
public ServerThread(Socket socket, Map<String, Socket> clientMap) {
this.socket = socket;
this.clientMap = clientMap;
}
@Override
public void run() {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), "UTF-8"));
// Get client name and add it to the map
clientName = reader.readLine();
System.out.println(clientName + " is online now.");
clientMap.put(clientName, socket);
// Broadcast the new client to all the other clients
for (Socket client : clientMap.values()) {
if (client != socket) {
Writer writer = new OutputStreamWriter(client.getOutputStream(), "UTF-8");
writer.write(clientName + " is online now.\n");
writer.flush();
}
}
while (true) {
String message = reader.readLine();
if (message == null) {
break;
}
String[] parts = message.split("\\|");
String from = parts[0];
String to = parts[1];
String words = parts[2];
if ("exit".equalsIgnoreCase(words)) {
break;
}
if (to.equalsIgnoreCase("All")) {
broadcast(from, words);
} else {
unicast(from, to, words);
}
}
reader.close();
socket.close();
// Remove this client from the map
clientMap.remove(clientName);
// Broadcast the logout message to the other clients
for (Socket client : clientMap.values()) {
Writer writer = new OutputStreamWriter(client.getOutputStream(), "UTF-8");
writer.write(clientName + " has logged out.\n");
writer.flush();
}
System.out.println(clientName + " has logged out.");
} catch (IOException e) {
e.printStackTrace();
}
}
// 发送消息给所有客户端
private synchronized void broadcast(String from, String words) throws IOException {
for (Socket client : clientMap.values()) {
Writer writer = new OutputStreamWriter(client.getOutputStream(), "UTF-8");
writer.write(from + ": " + words + "\n");
writer.flush();
}
}
// 发送消息给指定客户端
private synchronized void unicast(String from, String to, String words) throws IOException {
Socket toSocket = clientMap.get(to);
if (toSocket == null) {
Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
writer.write("The client '" + to + "' is not online.\n");
writer.flush();
} else {
Writer writer = new OutputStreamWriter(toSocket.getOutputStream(), "UTF-8");
writer.write(from + ": " + words + "\n");
writer.flush();
}
}
}
这个服务器和客户端代码都能够用于实际的生产环境了。但由于时间和篇幅的制约,我们没有对GUI界面展示过多的讲解,这个可以采用Swing、JavaFX等GUI库来进行界面构建,可根据实际情况自行开发。
Connection Reset一般来说是端口被拒绝,网络不通,或者被防火墙拦截了。你先关闭防火墙,本机连本机试试看。