使用Java练习聊天室,首先我们要了解的是基本概念模型图样,如图下:![请添加图片描述](https://img-blog.csdnimg.cn/8e131c929ab94b3ba391d597f916a63f.png)
Client就是客户端
- 包装的对象分别要有对象名字,socket通信对象 ,输出流和输入流,包装消息的队列(LinkedBlockingQueue阻塞队列);
- 封装的方法分别有接收消息,发送普通消息,发送在线人数的信息和最终关闭socket通信对象
- 注意所有客户端都用线程开启
public class ClientCom {
public static final ExecutorService service = Executors.newCachedThreadPool();
private String name;//名字
private Socket socket;
private ObjectInputStream in;
private ObjectOutputStream out;
private Set<String> users;
private LinkedBlockingQueue<MessageObject> queue = new LinkedBlockingQueue<>();
public ClientCom(String name,String serverIp,int serverPort) throws IOException {
this.name = name;
this.socket = new Socket(serverIp,serverPort);
out = new ObjectOutputStream(socket.getOutputStream());
in = new ObjectInputStream(socket.getInputStream());
//初始化里将初始化信息定义
MessageObject msgObj =
new MessageObject(MessageObject.TYPE_INIT,null,this.name,null,null);
//输出初始化信息
out.writeObject(msgObj);
//开始接收信息
startReceiving();
}
private void startReceiving() {
service.execute(()->{
while (true){
try {
//读取信息
MessageObject msgObj = (MessageObject) in.readObject();
//如果是用户名单,则返回这个名单
if(msgObj.isUserList()){
this.users = msgObj.getUsers();
//如果是普通信息的话,则将消息扔到队列当中
}else if(msgObj.isNormal()){
queue.put(msgObj);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
break;
}
}
this.close();
});
}
//发送普通信息
public void sendMsg(MessageObject msgObj){
try {
out.writeObject(msgObj);
} catch (IOException e) {
e.printStackTrace();
this.close();
}
}
private void close() {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public Set<String> getUserSet() {
return users;
}
public String getName() {
return name;
}
public LinkedBlockingQueue<MessageObject> getQueue() {
return queue;
}
}
消息封装
- private static final long serialVersionUID = 999L;//注意一定要整理好版本
- 设置消息类型:普通消息,初始化消息,返回对象个数消息;
- 包装对象:消息的类型,消息的发送者,消息的接受者
public class MessageObject implements Serializable {//消息对象
private static final long serialVersionUID = 999L;
public static final int TYPE_INIT=1;//初始化消息
public static final int TYPE_NORMAL=2;//普通消息
public static final int TYPE_USERLIST=3;//用户列表消息
private int type;
private String receiver;
private String sender;
private String content;
private Set<String> users;
public boolean isNormal(){//是否普通消息
return type == TYPE_NORMAL;
}
public boolean isUserList(){//是否客户端表
return type == TYPE_USERLIST;
}
public boolean isInit(){//是否为初始化消息
return type == TYPE_INIT;
}
}
服务端
- 创建一个Map集合存储所有的ServerCom对象,出于安全角度,我们采用ConcurrentHashMap
- 缓存收到的新消息的队列(公共消息队列)
- 缓存私人消息(私人消息队列)
- 初始化时进行通信socket的连接并将消息放入Map集合当中,然后执行发送消息
- 静态块执行将公共消息队列内容获取到
- 接收消息的动作:读入流读到信息,将公共消息队列的消息放入私人消息队列
- 发出消息的动作:将私人消息队列写出
- 最后关闭通信连接
public class ServerCom {
private static ExecutorService service = Executors.newCachedThreadPool();
//存储所有的ServerCom对象
private static Map<String,ServerCom> serverComMap = new ConcurrentHashMap<>();
//缓存收到的新消息的队列
private static LinkedBlockingQueue<MessageObject> publicQueue = new LinkedBlockingQueue<>();
static {
//处理新消息的线程
service.execute(()->{
while(true){
try {
MessageObject messageObject = publicQueue.take();
ServerCom com = serverComMap.get(messageObject.getReceiver());
com.putMsg(messageObject);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//发送用户列表消息
service.execute(()->{
while(true){
Set<String> _users = serverComMap.keySet();
Set<String> users = new HashSet<>(_users);
MessageObject msgObj = new MessageObject(MessageObject.TYPE_USERLIST,null,null,null,users);
serverComMap.values().forEach(com->com.putMsg(msgObj));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
private String name;//对应客户单的名字
private Socket socket;
private ObjectInputStream in;
private ObjectOutputStream out;
private LinkedBlockingQueue<MessageObject> queue = new LinkedBlockingQueue<>();
public ServerCom(Socket socket) throws IOException, ClassNotFoundException {
this.socket = socket;
in = new ObjectInputStream(socket.getInputStream());
out = new ObjectOutputStream(socket.getOutputStream());
//刚刚建立连接,从客户端读取初始化消息
MessageObject msgObj = (MessageObject) in.readObject();
if(!msgObj.isInit()){
socket.close();
throw new RuntimeException("非初始化消息,拒绝连接!");
}
this.name = msgObj.getSender();
serverComMap.put(this.name,this);
log.debug("与客户端({})连接成功!",this.name);
//启动收消息线程
startReceiving();
//启动发消息线程
startSending();
//启动发送用户列表消息的线程
//startSendUsers();
}
private void startSendUsers() {
service.execute(()->{
while (true){
try {
Set<String> users = serverComMap.keySet();
MessageObject msgObj = new MessageObject(MessageObject.TYPE_USERLIST,null,null,null,users);
out.writeObject(msgObj);
TimeUnit.SECONDS.sleep(10);
} catch (IOException e) {
e.printStackTrace();
break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.close();
});
}
public void putMsg(MessageObject messageObject) {
try {
queue.put(messageObject);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void startSending() {
service.execute(()->{
while (true){
try {
MessageObject messageObject = this.queue.take();
log.debug("{}",messageObject);
out.writeObject(messageObject);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
break;
}
}
this.close();
});
}
private void startReceiving() {
service.execute(()->{
while (true){
try {
MessageObject msgObj = (MessageObject) in.readObject();
publicQueue.put(msgObj);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
break;
}
}
this.close();
});
}
private void close() {
serverComMap.remove(name);
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
组件部分(最终显示)
基础组件:
public abstract class ClientFrame extends JFrame {
protected ClientCom clientCom;
private JTextField nameTextField = new JTextField("u1",10);
private JTextField serverIpTextField = new JTextField("localhost",10);
private JTextField serverPortTextField = new JTextField("9999",10);
private JButton connButton = new JButton("连接");
private JTextArea msgTextArea = new JTextArea();
private JComboBox<String> userComboBox = new JComboBox<>();
private JTextField sendTextField = new JTextField(50);
private JButton sendButton = new JButton("发送");
public ClientFrame(String title){
super(title);
this.setSize(800,600);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
Dimension size = this.getSize();
this.setLocation((screenSize.width-size.width)/2,(screenSize.height-size.height)/2);
JPanel paramPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
paramPanel.add(new JLabel(" 服务器IP:"));
paramPanel.add(serverIpTextField);
paramPanel.add(new JLabel(" 服务器端口:"));
paramPanel.add(serverPortTextField);
paramPanel.add(new JLabel(" 您的名称:"));
paramPanel.add(nameTextField);
paramPanel.add(connButton);
this.getContentPane().add(paramPanel,BorderLayout.NORTH);
this.getContentPane().add(msgTextArea,BorderLayout.CENTER);
JPanel sendPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
sendPanel.add(new JLabel("我对"));
sendPanel.add(userComboBox);
sendPanel.add(new JLabel("说:"));
sendPanel.add(sendTextField);
sendPanel.add(sendButton);
this.getContentPane().add(sendPanel,BorderLayout.SOUTH);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
connButton.addActionListener(this::connButtonActionListener);
sendButton.addActionListener(this::sendButtonActionListener);
this.msgTextArea.setEnabled(false);
this.userComboBox.setEnabled(false);
this.sendTextField.setEnabled(false);
this.sendButton.setEnabled(false);
}
public synchronized void setMsg(String msg){
msgTextArea.setText(msgTextArea.getText()+"\n"+msg);
}
protected void resetUserSet(){
userComboBox.removeAllItems();
if(clientCom != null && clientCom.getUserSet() != null){
clientCom.getUserSet().forEach(name->userComboBox.addItem(name));
}
}
private void connButtonActionListener(ActionEvent actionEvent) {
try {
String name = nameTextField.getText();
String serverIp = serverIpTextField.getText();
int serverPort = Integer.valueOf(serverPortTextField.getText());
if(serverPort < 1){
JOptionPane.showMessageDialog(null,"服务器端口错误!");
return;
}
this.clientCom = createClientCom(name,serverIp,serverPort);
//new ClientCom(name,serverIp,serverPort);
this.refreshUsers();
this.nameTextField.setEnabled(false);
this.serverIpTextField.setEnabled(false);
this.serverPortTextField.setEnabled(false);
this.connButton.setEnabled(false);
this.msgTextArea.setEnabled(true);
this.userComboBox.setEnabled(true);
this.sendTextField.setEnabled(true);
this.sendButton.setEnabled(true);
JOptionPane.showMessageDialog(null,"连接服务器成功!");
showMsg();
} catch (Exception e) {
JOptionPane.showMessageDialog(null,"错误:"+e.getMessage());
}
}
private void sendButtonActionListener(ActionEvent actionEvent) {
String receiverName = (String)userComboBox.getSelectedItem();
String msg = sendTextField.getText();
String myName = clientCom.getName();//????
this.sendTextField.setText("");
sendButtonAction(myName,receiverName,msg);
}
protected ClientCom getClientCom() {
return clientCom;
}
/**
* 点击发送按钮的逻辑
*/
protected abstract void sendButtonAction(String senderName,String recevierName,String content);
/**
* 接收展示消息逻辑
*/
protected abstract void showMsg();
/**
* 创建客户端通信组件
* @param name
* @param serverIp
* @param serverPort
* @return
*/
protected abstract ClientCom createClientCom(String name, String serverIp, int serverPort);
/**
* 刷新用户列表
*/
protected abstract void refreshUsers();
}
组件改进重写:
public class MyFrame extends ClientFrame{
public MyFrame(String title) {
super(title);
}
@Override
protected void sendButtonAction(String senderName, String recevierName, String content) {
//封装消息对象
MessageObject messageObject = new MessageObject(MessageObject.TYPE_NORMAL,recevierName,senderName,content,null);
//调用ClientCom的send方法
ClientCom com = this.getClientCom();
com.sendMsg(messageObject);
}
@Override
protected void showMsg() {
ClientCom.service.execute(()->{
while(true){
try {
MessageObject messageObject = this.getClientCom().getQueue().take();
String msg = messageObject.getSender()+"对"+messageObject.getReceiver()+"说"+messageObject.getContent();
this.setMsg(msg);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
@Override
protected ClientCom createClientCom(String name, String serverIp, int serverPort) {
try {
return new ClientCom(name,serverIp,serverPort);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void refreshUsers() {
ClientCom.service.execute(()->{
while (true){
while(this.getClientCom().getUserSet()==null || this.getClientCom().getUserSet().isEmpty()){
this.resetUserSet();//显示用户列表
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.resetUserSet();//显示用户列表
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
}
最后的测试
Server:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ssc = new ServerSocket(9999);
while(true){
try {
Socket sc = ssc.accept();
new ServerCom(sc);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
Client:
public class Client {
public static void main(String[] args) {
new MyFrame("聊天室");
}
}
出现问题
- 首先我们服务端和客户端的端口要一致
- 初始化的部分一定将通信组件初始化进来
- 消息类型一定每个都要强制转换