1.需求分析
设计程序,分别构建通信的两端:服务器端和客户端应用程序,套接字类型为面向连接的Socket,自己构建双方的应答模式,实现双方的数据的发送和接收(S发给C,C发给S)。
服务端程序能响应单个或任意多个客户端连接请求;服务端能向单个客户发送消息,支持群发消息给所有客户端;通信的双方具备异常响应功能,包括对方异常退出的处理。如果客户端退出,服务器有响应;反之亦然。
编程实现的思路:在服务器端,首先要启动一条线程用于监听某个指定端口(比如:8000),并且还要再开一条线程用于接收消息,客户端尝试连接该端口(8000),如果成功连接则会返回一个Socket类的实例对象(Socket socket = serversocket.accept()😉 ,很显然服务器端便应该保存有一个客户列表(比如可以用:ArrayList),使得服务器可以发消息给某个指定的客户端。而客户端在连接服务器之后也应该启动一条线程用于接收消息。当某个客户端进来时,便发送Login消息给服务器,服务器将此消息广播发给当前在线的所有用户,当某个客户端退出时,发送Logout消息给服务器,服务器将此消息广播发给当前在线的所有用户,当客户端之间进行通信时,会发送Talk消息给服务器,服务器再将此消息转发给指定的客户端,也就是所有的通信都是通过服务器进行转发的。
1. 创建JAVA应用程序工程
如图
2. 1创建UI视图类
2.1.1建立ClientView类
创建成功:
public class ClientView extends JFrame implements{
}
2.1.2实现处理交互事件的接口
让ClientView类实现ActionListener, KeyListener接口。并使用IDE自动生成接口中的方法。
public class ClientView extends JFrame implements ActionListener, KeyListener{
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
}
}
keyPressed():处理键盘按下事件
actionPerformed():处理鼠标点击事件
2.1.3UI元素初始化
首先,在类中添加如下成员变量:
private JTextArea taChatList; // 聊天内容区
private JTextField tfMessage; // 聊天输入框
private JTextField tfName; // 用户名输入框
private JButton btnSend; // 发送按钮
private JLabel labelNick;
private JPanel jp1, jp2;
private JScrollPane scrollPane;
private JLabel labelHost;
private JLabel labelPort;
private JTextField tfHost; // 服务器地址输入框
private JTextField tfPort; // 服务器端口输入框
private JButton btnConnect; // 连接/断开服务器按钮
接下来,编写一个用户界面初始化函数initView(),对各个UI元素对象分配存储空间,并按照设计要求添加到视图中。代码如下:
private void initView() {
taChatList = new JTextArea(20, 20);
taChatList.setEditable(false);
scrollPane = new JScrollPane(taChatList);
tfMessage = new JTextField(15);
btnSend = new JButton("发送");
jp1 = new JPanel();
labelHost = new JLabel("主机地址");
tfHost = new JTextField(15);
tfHost.setText("localhost");
labelPort = new JLabel("端口号");
tfPort = new JTextField(4);
tfPort.setText("8765");
btnConnect = new JButton("连接");
jp1.add(labelHost);
jp1.add(tfHost);
jp1.add(labelPort);
jp1.add(tfPort);
jp1.add(btnConnect);
labelNick = new JLabel("昵称:");
tfName = new JTextField(8);
jp2 = new JPanel();
jp2.add(labelNick);
jp2.add(tfName);
tfName.setText("用户0");
jp1.setLayout(new FlowLayout(FlowLayout.CENTER));
jp2.add(tfMessage);
jp2.add(btnSend);
jp2.setLayout(new FlowLayout(FlowLayout.CENTER));
add(jp1, BorderLayout.NORTH);
add(scrollPane, BorderLayout.CENTER);
add(jp2, BorderLayout.SOUTH);
setTitle("聊天室");
setSize(500, 500);
setLocation(450, 150);
setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
// 当光标定位在聊天输入框时监听回车键按下事件
tfMessage.addKeyListener(this);
// 为发送按钮增加鼠标点击事件监听
btnSend.addActionListener(this);
// 为连接按钮增加鼠标点击事件监听
btnConnect.addActionListener(this);
// 当窗口关闭时触发
addWindowListener(new WindowAdapter() { // 窗口关闭后断开连接
@Override
public void windowClosing(WindowEvent e) {
}
});
}
找到keyPressed()方法,添加对按下回车键事件的处理:
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
// 发送聊天消息
}
}
找到actionPerformed()方法,添加对两个按钮的响应:
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
if (e.getSource() == btnSend) {
// 响应发送按钮
} else if (e.getSource() == btnConnect) {
// 响应连接/断开按钮
}
}
最后,为ClientView创建一个构造方法,并且在构造方法中调用initView()方法对用户界面进行创建:
public ClientView() {
initView();
}
2.1.4运行程序查看主窗口
在ClientView类的末尾增加主函数作为程序运行的入口:
public static void main(String[] args) {
ClientView view = new ClientView();
}
运行结果如图:
2.2 编写网络服务模块
2.2.1 创建Networkservice类
创建成功:
public class NetworkService {
}
2.2.2 定义NetworkService模块的功能
connnect():连接到服务器
disconnect():断开与服务器的连接
isConnected():判断当前是否已经连接到服务器
sendMessage():发送聊天消息
为NetworkService类添加与这些功能相对应的方法:
public class NetworkService {
/**
* 连接到服务器
* @param host 服务器地址
* @param port 服务器端口
*/
public void connect(String host, int port) {
}
/**
* 断开连接
*/
public void disconnect() {
}
/**
* 是否已经连接到服务器
* @return true为已连接,false为未连接
*/
public boolean isConnected() {
}
/**
* 发送聊天消息
* @param name 用户名
* @param msg 消息内容
*/
public void sendMessage(String name, String msg) {
}
}
2.2.3 定义回调接口
以上网络功能操作完成后,往往需要反馈一些状态,例如在收到消息后通知用户界面刷新聊天内容,使得用户能够看到新消息。而根据分层设计的原理,这样的代码放在专注于网络操作的NetworkService类中是非常丑陋的做法。因此,我们定义一个回调接口,其中定义各项处理结束的通知函数。稍后我们在用户界面类(即ClientView类)中实现这个接口,就可以实现UI对网络处理的响应了。
在NetworkService类的最前面中添加接口Callback的定义,同时用Callback类型定义一个成员变量,并创建setter方法:
public class NetworkService {
public interface Callback {
void onConnected(String host, int port); //连接成功
void onConnectFailed(String host, int port); //连接失败
void onDisconnected(); //已经断开连接
void onMessageSent(String name, String msg); //消息已经发出
void onMessageReceived(String name, String msg);//收到消息
}
private Callback callback;
public void setCallback(Callback callback) {
this.callback = callback;
}
...
}
2.2.4 添加网络通信相关的成员变量
添加网络通信所需要的以下成员变量:
// 套接字对象
private Socket socket = null;
// 套接字输入流对象,从这里读取收到的消息
private DataInputStream inputStream = null;
// 套接字输出流对象,从这里发送聊天消息
private DataOutputStream outputStream = null;
// 当前连接状态的标记变量
private boolean isConnected = false;
2.2.5 实现connect()操作
connect()方法实现连接服务器的操作。它的逻辑如下:
- 根据参数提供的服务器地址和端口创建套接字。创建套接字的过程即建立连接的过程。
- 如果创建成功,记录已连接状态,同时通过回调函数通知外界连接成功。同时还要启动一个线程来监听是否有服务器发来的聊天消息。
- 如果创建套件字失败,则记录未连接状态,通过回调函数通知外界连接失败
为connect()方法编写代码如下:
public void connect(String host, int port) {
try {
// 创建套接字对象,与服务器建立连接
socket = new Socket(host, port);
isConnected = true;
// 通知外界已连接
if (callback != null) {
callback.onConnected(host, port);
}
// 开始侦听是否有聊天消息到来
beginListening();
} catch (IOException e) {
// 连接服务器失败
isConnected = false;
// 通知外界连接失败
if (callback != null) {
callback.onConnectFailed(host, port);
}
e.printStackTrace();
}
}
其中,用来监听聊天记录到来的方法beginListening()实现如下:
private void beginListening() {
Runnable listening = new Runnable() {
@Override
public void run() {
try {
inputStream = new DataInputStream(socket.getInputStream());
while (true) {
String[] s = inputStream.readUTF().split("#");
if (callback != null) {
callback.onMessageReceived(s[0], s[1]);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
};
(new Thread(listening)).start();
}
2.2.6 实现disconnect()操作
disconnect()的功能是断开与服务器的连接。在这里要关闭套接字,并且关闭所有的输入、输出流。实现代码如下:
public void disconnect() {
try {
if (socket != null) {
socket.close();
}
if (inputStream!= null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
isConnected = false;
// 通知外界连接断开
if (callback != null) {
callback.onDisconnected();
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.2.7 实现isConnected()
外界通过调用isConnected()方法来获知当前是否已经连接到服务器。简单的返回isConnected变量即可:
public boolean isConnected() {
return isConnected;
}
2.2.8 实现sendMessage()操作
sendMessage()方法将参数传来的用户名和消息串按照一定的格式发送出去。操作的实质就是将消息写入到套接字对象的输出流。实现如下:
public void sendMessage(String name, String msg) {
// 检查参数合法性
if (name == null || "".equals(name) || msg == null || "".equals(msg)) {
return;
}
if (socket == null) { //套接字对象必须已创建
return;
}
try {
// 将消息写入套接字的输出流
outputStream = new DataOutputStream(socket.getOutputStream());
outputStream.writeUTF(name + "#" + msg);
outputStream.flush();
// 通知外界消息已发送
if (callback != null) {
callback.onMessageSent(name, msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.3 用户界面与网络服务模块对接
2.3.1 为ClientView类增加NetworkService模块并实现回调
重新打开ClientView.java文件进行修改。
为了在ClientView类中对网络服务模块即NetworkService类进行调用,首先必须在ClientView类中增加一个NetworkService类型的成员:
然后专门写一个函数initNetworkService()来初始化这个networkService对象。这个函数主要做两件事:
- 创建networkService对象(new)
- 设置回调接口以处理NetworkService中各个网络操作发来的通知
initNetworkService()函数实现如下:
private void initNetworkService() {
networkService = new NetworkService();
networkService.setCallback(new Callback() {
@Override
public void onConnected(String host, int port) {
// 连接成功时,弹对话框提示,并将按钮文字改为“断开”
alert("连接", "成功连接到[" + host + ":" + port + "]");
btnConnect.setText("断开");
}
@Override
public void onConnectFailed(String host, int port) {
// 连接失败时,弹对话框提示,并将按钮文字设为“连接”
alert("连接", "无法连接到[" + host + ":" + port + "]");
btnConnect.setText("连接");
}
@Override
public void onDisconnected() {
// 断开连接时,弹对话框提示,并将按钮文字设为“连接”
alert("连接", "连接已断开");
btnConnect.setText("连接");
}
@Override
public void onMessageSent(String name, String msg) {
// 发出消息时,清空消息输入框,并将消息显示在消息区
tfMessage.setText("");
taChatList.append("我(" + name + "):\r\n" + msg + "\r\n");
}
@Override
public void onMessageReceived(String name, String msg) {
// 收到消息时,将消息显示在消息区
taChatList.append(name + ":\r\n" + msg + "\r\n");
}
});
}
其中,alert()函数用来显示一个对话框以向用户通告某个信息,实现如下:
// 显示标题为title,内容为message的对话框
private void alert(String title, String message) {
JOptionPane.showMessageDialog(this, message, title, JOptionPane.INFORMATION_MESSAGE);
}
然后找到构造方法ClientView(),在其末尾调用这个函数如下:
public ClientView() {
initView();
initNetworkService();
}
2.3.2 实现用户交互
不同的用户交互将会触发相应的网络操作,包括:
- 未连接状态下,点击连接/断开按钮执行连接操作
- 已连接状态下,点击连接/断开按钮执行断开连接操作
- 已连接状态下,关闭窗口执行断开连接操作
- 按回车键发送消息
下面分别调用NetworkService模块提供的功能来完成以上的交互操作。
2.3.2.1 处理关闭窗口操作
在ClientView类中找到如下代码:
// 当窗口关闭时触发
addWindowListener(new WindowAdapter() { // 窗口关闭后断开连接
@Override
public void windowClosing(WindowEvent e) {
}
});
增加断开连接操作,如下:
// 当窗口关闭时触发
addWindowListener(new WindowAdapter() { // 窗口关闭后断开连接
@Override
public void windowClosing(WindowEvent e) {
networkService.disconnect();
}
});
2.3.2.2 处理按钮点击操作
其中包括对连接/断开按钮的点击,以及对发送按钮的点击。前者连接或者断开服务器,后者将编辑框中的消息发送出去。
找到actionPerformed()方法,改写如下:
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
if (e.getSource() == btnSend) {
sendMessage();
} else if (e.getSource() == btnConnect) {
// 响应连接/断开按钮
if (!networkService.isConnected()) {
// 未连接状态下,执行连接服务器操作
String host = tfHost.getText();
int port = Integer.valueOf(tfPort.getText());
networkService.connect(host, port);
} else {
// 已连接状态下,执行断开连接操作
networkService.disconnect();
}
}
}
其中,sendMessage()方法实现如下:
private void sendMessage() {
// 响应发送按钮
String name = tfName.getText();
String msg = tfMessage.getText();
// 检查参数合法性
if (name == null || msg == null || "".equals(name) || "".equals(msg)) {
return;
}
// 发送消息
networkService.sendMessage(name, msg);
}
2.3.2.3
当按下回车键时,如果聊天输入框中有内容,就将其发送出去。
找到keyPressed()方法,改下如下:
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
// 发送聊天消息
sendMessage();
}
}
三 测试聊天室系统
3.1 运行服务器端程序
从以下链接下载已编写好的聊天室服务器端代码工程:
链接: https://pan.baidu.com/s/1VlgKoIr-JrHdLXVEieEXdQ 提取码: 5xsm
下载后解压。在eclipse中选择菜单项“File -> Import”,在弹出的对话框中选择“General -> Existing Project into Workspace”,点击“Next”按钮进入下一步对话框,在“Select Root Directory”项下选择刚才解压出来的目录:
按照之前运行客户端程序同样的方法运行此项目,将会看到如下的窗口:
3.2 运行客户端程序并连接服务器
连续运行两次我们编写的客户端程序,得到两个客户端程序窗口。分别点击各自的“连接按钮”,观察服务器程序窗口的变化。
分别在各个客户端窗口输入并发送消息,观察是否能够出现在另一客户端窗口。
注:若下载代码出现中文乱码现象,可以右键类,选择properties
将编码格式设置为UTF-8