JAVA实现服务器和客户端的单播/广播消息转发的Socket编程
实验要求:
一个服务器,多个客户端,服务器负责消息中转,客户端之间可以互相聊天。(广播/单播)
实验环境:WINDOWS
编程环境:JAVA
界面流程展示:
1. 服务器界面展示
服务器监听本机的8288端口,首先需要点击启动按钮,才能通过客户端连接
2. 客户端界面展示
新建客户端界面后需要先输入用户昵称,服务器ip地址和端口号,点击连接既可以连接服务器。
3. 服务器启动
点击服务器界面的启动按钮,日志显示服务器启动成功,好友列表显示全体成员标签。
4. 客户端连接
客户端(左)单击连接按钮,客户端好友列表中显示了所有人标签(用于广播,可点击),服务器日志显示接收到客户端连接,并为客户端创建线程,同时在在线用户列表中添加客户端用户昵称。
5.用户上线通知
再打开两个客户端,服务器收到新的客户端连接的请求,并将用户上线的消息向所有用户转发。服务器和每一个客户端同时也会更新在线用户列表。新上线的用户会收到服务器向其发送的原有在线用户列表信息。
6.用户进行群发操作(通过服务器广播)
左下角的用户点击其界面上的所有人标签,或者什么都不点击(默认为向所有人发送消息),并在输入框内部输入消息内容,点击发送。服务器接收到用户的消息,并将其向每一个用户进行转发。
7.用户进行私聊转发(单播)
右下角的用户点击了在线好友列表中的一位用户,并在输入框内输入消息点击发送。服务器接收到该消息,并将其向对应客户端转发。
8.服务器向群体用户广播
服务器点击所有人标签或不点击(默认为所有人),在输入框内输入消息并点击发送。每个客户端都收到服务器发送的内容。
9.服务器私聊转发(单播)
服务器点击在线用户列表中的某一个人,并输入消息点击发送。对应客户端收到消息。
说明文档:
客户端:
1. 声明主类Client的成员变量
这些变量都是在多处调用的,如果不声明为成员变量,利用传参的方法传递很麻烦,故在外声明。尤其是注释内容1.2的socket相关变量,不在外部声明将会导致socket关闭。
1.1 UI组件:连接、断开、发送、昵称、IP、端口、输入内容、在线列表、聊天内容
1.2 Socket相关:客户端socket,输入和输出
1.3 用户昵称:声明DefaultListModel类型的列表,向其内部添加数据可以自动同步到在线列表中
1.4 客户端线程:为了防止死锁,将客户端新建一个与UI界面无关的线程
2. 构造函数
2.1 调用UI函数显示窗口
3. 与连接服务器相关
3.1 连接服务器
3.1.1 获取用户昵称、服务器ip、port
3.1.2 New与socket相关
3.1.3 在线列表添加所有人标签
3.1.4 向服务器发送本地账号登陆
3.1.5 为客户端新建一个与UI分离的线程用于等待服务器发送消息
4. UI相关
4.1 显示界面
4.2 添加事件监听
4.2.1 连接
4.2.2 发送:将消息按照固定协议发送
4.2.3 选择消息发送对象
4.3输出错误:红色
4.4输出私聊内容:黑色
4.5输出上线下线内容:蓝色
4.6输出广播内容:绿色
5. 客户端线程的内部类
考虑到读取服务器消息时调用readline会将进程死锁,故新建一个线程,该线程继承了Runnable接口,在构造函数被调用后自动执行run方法
5.1构造函数
5.2run()
5.2.1对服务器消息进行解析(分割)
5.2.1按照制定协议处理消息:
登陆: LOGIN@USERNAME
普通消息: MESSAGE@TO@FROM@CONTENT
6. 发送消息
7.消息分割器
服务端:
1. 在主类Server中声明必要的成员变量
1.1 UI组件
1.2 Socket相关:服务器socket
1.3 线程相关:服务器线程、记录每一个客户端的线程地图
1.4 用户昵称
2. 构造函数
3. 启动服务器
3.1建立socket
3.2新建服务器线程
3.3新建存储客户端线程的地图
4. UI相关
4.1生成界面窗口
4.2添加监听器:启动、发送消息、检验目标用户
4.3输出消息内容:黑色
4.4输出错误提示:红色
4.5输出成功提示:绿色
5. 服务器线程内部类
5.1isRuning用于确定服务器是否在运行
5.2构造函数:调用构造函数后自动调用对应线程
5.3run()
5.3.1循环等待下一个客户端连接,注意,这里的socket在内部声明,保证每一个客户端的socket不是同一个。
5.3.2每接收到一个客户端socket就为其新建一个客户端线程,保证每个客户端之间互不干扰
5.3.3将新的客户端线程添加到记录客户端线程的地图中
6.客户端线程内部类
6.1新建线程时首先调用初始化函数,接收登陆消息
6.1.1为每一个其他用户转发该用户的登陆消息
6.1.2为该用户发送已经在线的用户列表
6.1.3在线用户增加该用户的昵称
6.2run()第二次和以后调用该方法
6.1阻塞等待客户端传送消息
6.2收到消息后按照对应要求解析消息
6.2.1MESSAGE@TO@FROM@CONTENT
6.3按照To指向的用户对消息进行单播或广播
源码如下
Client.java
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
public class Client {
//1.以下为必须在外部声明的变量
//1.1 UI组件
JButton ConnectServer;//连接
JButton DisconnectServer;//断开
JButton SendMessageButton;//发送
JTextField NickNameText;//昵称
JTextField ServerIPAddressText;//服务器ip
JTextField ServerPortText;//服务器端口
JTextField InputContentText;//输入内容
JList OnlineClientList;//在线列表
JLabel ChatContentLabel;//聊天内容
//1.2 socket相关
Socket socket;//input和output是通过socket定义的,如果socket关闭了,其他两个也失效
BufferedReader input;//input为服务器传来的数据
PrintStream output;//output为向服务器输出的数据
//1.3 用户昵称
DefaultListModel OnlineClientNickName;//在线用户昵称列表:向其中插入数据,自动将数据插入到JList中
String ToTargetName = "ALL";//目标用户昵称:OnlineClientList的监听器对其修改
//1.4客户端线程
ClientThread cliendThread;
//2.构造函数
public Client() {
//2.1 调用UI函数显示窗口
CreateFrame();
}
//3.与连接服务器相关
//3.1连接服务器
public void ConnectServer() {
//3.1.1 获取基本信息
String ServerIPAddress = ServerIPAddressText.getText().trim();
int ServerPort = Integer.parseInt(ServerPortText.getText().trim());
String NickName = NickNameText.getText();
try {
//3.1.2 socket相关
socket = new Socket(ServerIPAddress, ServerPort);
input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
output = new PrintStream(socket.getOutputStream());
//3.1.2 在线列表添加所有人标签
OnlineClientNickName.addElement("所有人");
//3.1.3 向服务器发送本帐号登陆消息
SendMessage("LOGIN@"+NickName);
//3.1.4 为客户端建立线程
cliendThread = new ClientThread();
} catch (UnknownHostException e) {
Error("Client:主机地址异常"+e.getMessage());
return;
} catch (IOException e) {
Error("Client:连接服务器异常"+e.getMessage());
return;
}
}
//3.2 断开连接
//3.3连接或断开时的按钮是否可点击设置
//4. UI相关
//4.1界面
public void CreateFrame() {
//4.1.1 总窗口
JFrame ClientFrame = new JFrame("客户端");
ClientFrame.setSize(800,600);//设置长宽,注意数值不需要引号
ClientFrame.setLocationRelativeTo(null);//设置在屏幕中央显示
ClientFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置默认关闭按钮,不设置也可以
//4.1.2 客户端信息
JPanel ClientIdPanel = new JPanel();
ClientIdPanel.setLayout(new FlowLayout(FlowLayout.LEFT));//4.2.1 设置客户端id栏的布局为浮动布局
ClientIdPanel.setSize(800, 100);
//4.1.2.2 昵称栏
JLabel NickNameLabel = new JLabel("昵称");
NickNameText = new JTextField(10);
NickNameText.setText("jiangbowen");
ClientIdPanel.add(NickNameLabel);
ClientIdPanel.add(NickNameText);
//4.1.2.3 服务器IP地址
JLabel ServerIPAddressLabel = new JLabel("IP地址");
ServerIPAddressText = new JTextField(10);
ServerIPAddressText.setText("127.0.0.1");
ClientIdPanel.add(ServerIPAddressLabel);
ClientIdPanel.add(ServerIPAddressText);
//4.1.2.4 端口号
JLabel ServerPortLabel = new JLabel("端口");
ServerPortText = new JTextField(10);
ServerPortText.setText("8288");
ClientIdPanel.add(ServerPortLabel);
ClientIdPanel.add(ServerPortText);
//4.1.2.5 连接服务器/断开连接
ConnectServer = new JButton("连接");
DisconnectServer = new JButton("断开");
ClientIdPanel.add(ConnectServer);
ClientIdPanel.add(DisconnectServer);
//4.1.2.6 设置标题
ClientIdPanel.setBorder(new TitledBorder("用户信息栏"));
//4.1.3 好友列表
JPanel FriendListPanel = new JPanel();
FriendListPanel.setPreferredSize(new Dimension(200,400));
FriendListPanel.setBorder(new TitledBorder("好友列表"));
//4.1.3.1 好友列表内容
OnlineClientNickName = new DefaultListModel();
OnlineClientList = new JList(OnlineClientNickName);
FriendListPanel.add(OnlineClientList);
//4.1.4 聊天内容面板
JPanel ChatContentPanel = new JPanel();
ChatContentPanel.setPreferredSize(new Dimension(490,400));
ChatContentPanel.setBorder(new TitledBorder("聊天内容"));
//4.1.4.1 声明聊天内容标签
ChatContentLabel = new JLabel("");
ChatContentLabel.setPreferredSize(new Dimension(490,400));
ChatContentPanel.add(ChatContentLabel);
//4.1.5 输入内容面板
JPanel InputContentPanel = new JPanel();
InputContentPanel.setPreferredSize(new Dimension(600,100));
//4.1.5.1 聊天输入框
InputContentText = new JTextField();
InputContentText.setPreferredSize(new Dimension(600,60));
//4.1.5.2 发送按钮
SendMessageButton = new JButton("发送");
InputContentPanel.add(InputContentText);
InputContentPanel.add(SendMessageButton);
InputContentPanel.setBorder(new TitledBorder("输入内容"));
//4.1.6 客户端整体布局
ClientFrame.add(ClientIdPanel, BorderLayout.NORTH);
ClientFrame.add(FriendListPanel, BorderLayout.WEST);
ClientFrame.add(ChatContentPanel,BorderLayout.CENTER);
ClientFrame.add(InputContentPanel,BorderLayout.SOUTH);
//4.1.7设置可见
ClientFrame.setVisible(true); //设置可见必须在所有内容都add进Frame之后
//4.1.8 添加监听事件
AddActionListener();
}
//4.2 添加事件监听
private void AddActionListener() {
//4.2.1 点击连接
ConnectServer.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ConnectServer();
}
});
//4.2.2 点击断开
DisconnectServer.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
}
});
//4.2.3 点击发送
SendMessageButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String message = InputContentText.getText().trim();
SendMessage("MESSAGE@"+ToTargetName+"@"+NickNameText.getText()+"@"+message);
}
});
//4.2.4 检验目标发送者是谁
OnlineClientList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
int index = OnlineClientList.getSelectedIndex();
if(index<0) {
Error("Client:检测到目标发送者下标为负数");
return;
}
if(index == 0) {
ToTargetName = "ALL";
}else {
String ToClientNickName = (String)OnlineClientNickName.getElementAt(index);
ToTargetName = ToClientNickName;
}
}
});
}
//4.3 输出错误(红色)
private void Error(String message){
//JLabel不支持\n换行,故添加html标签进行换行,没有结束标签不影响显示
ChatContentLabel.setText(ChatContentLabel.getText()+""+message+""+"
");
}
//4.4 输出上线下线内容
private void Log(String message){
//JLabel不支持\n换行,故添加html标签进行换行,没有