java实现qq聊天(超详细)

java语言实践课内容:聊天室小程序或QQ

功能要求:
聊天室:使用图形用户界面,能实现一个聊天室中多人聊天,可以两人私聊。
QQ:实现类似QQ登录、聊天等功能。
注意:有一定等级。完全照搬别人的代码,不超过70分。
提示:使用socket通信
前面是逐步讲解,要是想看最终代码,请直接找到该文章的最下面
//
感谢这位up博主的视频讲解!!!
准备工作
建立包,类,文件
插入图片要创建一个文件夹,文件夹里保存的就是所需要的图片啦,比如我创建的文件夹为image;
两个设备上进行通信,要用到socket通信协议,即客户端的内容先传到服务器,在服务器上处理,传回客户端

在这里插入图片描述
image素材文件里存放的图片
链接:https://pan.baidu.com/s/1zns86NC6Qm80xKZHMIRSbA
提取码:3f6w
在这里插入图片描述

//

1.首先我们先建一个qq登陆界面

成品图如下:
在这里插入图片描述
详细代码及注释如下

// 客户端的登陆界面
package com.qq.client.view;
//抽象窗口工具包
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class QqClientlLogin  extends JFrame{  //JFrame的子类
	
	  JLabel jup;   //定义上部需要的组件   //标签
	
	//定义中部需要的组件
	  JPanel jmid ;
      JLabel jmid_num1,jmid_key1;
      JTextField jmid_num2;    //文本框
      JPasswordField jmid_key2;   //密码框
      JCheckBox jmid_rember,jmid_automatic;  //复选框
	  
	 JPanel  jdown;         //JPanel 面板
	 JButton jdown_1,jdown_2;    //定义下部需要的组件
	 
	 public  QqClientlLogin()      //构造方法
	 {
		  //处理上部      
	    jup=new JLabel(new  ImageIcon("image/up3.png"));
	
	    //处理中部
	    jmid=new JPanel(new GridLayout(3,3));//网格布局 3行3列
	    Font font = new Font("宋体", Font.PLAIN, 25);    //创建1个字体实例
	    jmid_num1=new JLabel("QQ号码:",JLabel.CENTER);  
	    jmid_key1=new JLabel("QQ密码:",JLabel.CENTER);
	    jmid_num1.setFont(font);    jmid_key1.setFont(font);  //字体大小
        jmid_num2= new JTextField() ;    
	    jmid_key2=new JPasswordField();
	    jmid_rember=new JCheckBox("自动登录");
	    jmid_automatic=new JCheckBox("记住密码");
	    jmid_rember.setFont(font);    jmid_automatic.setFont(font); //字体大小
	    jmid_rember.setForeground(Color.blue); 	  //设置字体颜色
	    jmid_automatic.setForeground(Color.blue);
	    jmid.add(jmid_num1);
	    jmid.add(jmid_num2);
	    jmid.add(jmid_key1);
	    jmid.add(jmid_key2);
	    jmid.add(jmid_rember);
	    jmid.add(jmid_automatic);
	    
	    //处理下部
	    jdown=new JPanel(new FlowLayout()); //流式布局
	    jdown_1=new JButton(new ImageIcon("image/denglu.png"));
	    jdown_2=new JButton(new ImageIcon("image/tuichu.png"));
	    jdown.add(jdown_1);
	    jdown.add(jdown_2);
	    
	    setLocation(300,300); //窗口的位置
	    add(jup,"North");   //放在最北部    
	    add(jmid,"Center");  //放在中间
	    add(jdown,"South"); //放在南部
	    setSize(700,540);     //设置大小                                  
	    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //结束窗口所在的应用程序  
	    setVisible(true);       //可见,默认是不可见
	 }
	 
	 public static void main(String[]  args)
	 {
		 QqClientlLogin  qqClientlLogin=new  QqClientlLogin();
	 } 
}

//

2.好友列表界面

成品图如下:
在这里插入图片描述

要建立两个卡片的转换,即点击卡片一的"黑名单"转到第二个卡片界面,点击第二个卡片的"我的好友",转到第一个界面上
此时,就需要创建监听器的类,用到ActionListener接口
除此之外,我还建立了鼠标监听MouseListener接口,当鼠标停留在好友是,好友编号变红,离开时变回黑色,双击好友时,可得到该好友的编号(并进入与该好友的聊天界面)
详细代码及注释如下

//好友列表(也包括黑名单)
package com.qq.client.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class QqFriendList extends JFrame implements ActionListener,MouseListener {   
	//负责创建监听器的类,  点击按钮触发ActionListener事件;  鼠标监听MouseListener 
	
    // 第一张卡片(我的好友);
		JPanel jup1,jmid1,jdown1;   //jup1即为总JPanel;
		JButton jup1_1,jdown1_1;
		JScrollPane jmid1_1;      //滑动窗口;
	
	//第二张卡片(黑名单);
		JPanel jup2,jup2_button,jmid2;
		JButton jup2_1,jup2_2;
		JScrollPane jmid2_1;      //滑动窗口;
		
  //把整个JFrame设置成CardLayout布局
		CardLayout cl;

   public  QqFriendList ()
   {         Font font = new Font("宋体", Font.PLAIN, 35);    //创建字体实例

	
	   //处理第一张卡片(我的好友);
	    jup1=new JPanel(new BorderLayout());   //第一张卡片的总JPane,BorderLayout()布局;
	   jup1_1=new JButton("我的好友");
	   jup1_1.setFont(font);  //字体
	
	   //假设有50个好友,具体是服务器返回的结果(好友信息存放在服务器)
	   jmid1=new JPanel(new  GridLayout(50,1,4,4));  //(4,4)行间距和列间距
       //给jmid1初始化50个好友
	   JLabel []jlist1=new JLabel [50];  //数组;
	   for ( int i=0; i<jlist1.length ; i++ )
	   {
		   jlist1[i]=new JLabel(i+1+" ",new ImageIcon("image/touxiang.png"),JLabel.LEFT);
		   jlist1[i].addMouseListener(this);
		   jmid1.add(jlist1[i]);  
	   }
	   jmid1_1=new JScrollPane(jmid1);
	   
	   jdown1_1=new JButton("黑名单");
	   jdown1_1.addActionListener(this);     //给jdown1_1按钮增加一个监听器,这个监听器对象就是“QqFriendList ”类型的,
	                          //当用户点击了jdown1_1按钮时,会程序自动前往QqFriendList类的“actionPerformed”方法中,处理产生的事件。
	   jdown1_1.setFont(font);        //字体
	   jdown1=new JPanel(new GridLayout(1,1));//黑名单按钮
	   jdown1.add(jdown1_1);  //加入按钮;
	   
       jup1.add(jup1_1,"North");
       jup1.add(jmid1_1,"Center");
       jup1.add(jdown1,"South");
   

       
       //处理第二张卡片(黑名单)	   
       jup2=new JPanel(new BorderLayout());   //第二张卡片的总JPane,BorderLayout()布局;
       jup2_1=new JButton("我的好友");
       jup2_1.addActionListener(this);   //监听
       jup2_2=new JButton("黑名单");
	   jup2_1.setFont(font);  //字体
       jup2_2.setFont(font);
       jup2_button=new JPanel(new GridLayout(2,1));//黑名单按钮
       jup2_button.add(jup2_1);
       jup2_button.add(jup2_2);
       
	   //假设有20个黑名单,具体是服务器返回的结果(好友信息存放在服务器)
	   jmid2=new JPanel(new  GridLayout(20,1,4,4));  //(4,4)行间距和列间距
       //给jmid2初始化20个好友
	   JLabel []jlist2=new JLabel [20];  //数组;
	   for ( int i=0; i<jlist2.length ; i++ )
	   {
		   jlist2[i]=new JLabel(i+1+" ",new ImageIcon("image/heitou.png"),JLabel.LEFT);
		   jmid2.add(jlist2[i]);
	   }
	   jmid2_1=new JScrollPane(jmid2);
	   
       jup2.add(jup2_button,"North");
       jup2.add(jmid2_1);
  
       cl=new CardLayout();
       setLayout(cl);
       add(jup1,"1");  //第一张卡片jup1;
       add(jup2,"2");  //第二张卡片jup2;
       setLocation(300,300); //窗口的位置
       setSize(440,800);
       setVisible(true);     
   }
  
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		QqFriendList  qqFriendList=new  QqFriendList();
	}
	
	public void actionPerformed(ActionEvent arg0)
	{ 
		//如果点击了黑名单按钮,就显示第二张卡片
		if(arg0.getSource()==jdown1_1)
		{
			cl.show(this.getContentPane(), "2");  //用getContentPane()方法获得JFrame的内容面板
		}
		else
			if(arg0.getSource()==jup2_1)
			{
				cl.show(this.getContentPane(), "1");
			}
	}
	public void mouseEntered(MouseEvent arg0) {    //进入组件触发鼠标事件
		// TODO Auto-generated method stub
		JLabel jl=(JLabel)arg0.getSource(); //变成JLable形式
                                              //getSource()获取鼠标事件的事件源;
		jl.setForeground(Color.red);   //设置前景色
	}

	public void mouseExited(MouseEvent arg0) {  //鼠标离开组件触发的鼠标事件
		// TODO Auto-generated method stub
		JLabel jl=(JLabel)arg0.getSource();   
		jl.setForeground(Color.black);
	}
	
	public void mouseClicked(MouseEvent arg0) {   //点击鼠标触发鼠的标事件
		// TODO Auto-generated method stub
		//响应用户双击的事件,并得到好友的编号.
		if(arg0.getClickCount()==2)   //getClickCount() 获取鼠标被单机的次数
		{
			//得到该好友的编号 
			//getText() 的意思是:返回数据窗口控件中 悬浮在当前行列之上的
			String friendNum=((JLabel)arg0.getSource()).getText();
			
			//System.out.println("你希望和 "+friendNum+" 聊天");
		}
	}


	public void mousePressed(MouseEvent arg0) { //按下按鼠标键触发的鼠标事件
		// TODO Auto-generated method stub
		
	}

	public void mouseReleased(MouseEvent arg0) {   //释放鼠标键触发的鼠标事件
		// TODO Auto-generated method stub
		
	}

} 

3.聊天界面,用服务器判断登录qq,对象流

这部分的代码成果图:
在这里插入图片描述

注:正常是要有数据库来存qq账号密码信息的,秉着由简至难的原则,此处先不设计数据库,直接用服务器判断
有了上诉两个界面的设置,相信设置聊天界面应该很好模拟退出
聊天界面初步成果:在这里插入图片描述

//与好友聊天的界面
package com.qq.client.view;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class QqChat extends JFrame {
	 Font font = new Font("宋体", Font.PLAIN, 20); 
	JTextArea jta;    //文本域  存聊天内容
	JTextField jtf;    //文本框
	JButton jb;
	JPanel jp;
	String ownerId;
	String friendId;
	
	public QqChat(String owner,String friend)
	{
		jta=new JTextArea();
		jtf=new JTextField(25);     //25个字符那么宽
		jb=new JButton("发送");
		jb.setFont(font);
		jp=new JPanel();
		jp.add(jtf);
		jp.add(jb);	
		add(jta,"Center");
		add(jp,"South");
		setTitle(owner+"正在和 "+friend+" 聊天");  //设置标题
		setIconImage((new ImageIcon("image/qq.png").getImage())); //获取图像
		setLocation(800,400); //窗口的位置
		setSize(600, 500);
		setVisible(true);
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
	    QqChat qqChat=new QqChat("1");   //传入好友1,你正在和1聊天;
	}

思考一下,如果是要与其余好友聊天应该如何表示呢?
在 QqFriendList中,我们双击得到好友的编号,只需把编号传到QqChat中即可
在这里插入图片描述
现在来实现登录操作:当用户点击登录后,把qq号码和密码发送给QqServer(服务器)去验证,如果该用户合法,返回true,否则返回false;
以对象流的方式读取客户端发来的账号和密码,在网络间传递对象流,
避免直接读入由特殊符号,空格,换行等干扰

对象流是一种高级流,可以方便我们将java中的任何对象进行读写操作。
java.io.objectoutputstream对象输出流,可以将对象转换为一组字节写出。
java.io.objectinputstream对象输入流,可以读取一组字节转换为原对象,
还原为原对象的条件是读取这个字节应该是对象输出流将一个对象转换的字节。

一个对象要想序列化,该类必须实现java.io.Serializable 接口
Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException 。
使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中

在这里,我们还要用到socket socket是两台主机之间的一个连接
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接需要一对socket,两个socket之间形成一个管道(通道),进行信息流的传输(联想IO流中文件和程序之间读写)。

ServerSocket与Socket入门详解
分别在Qqclient(客户端)和QqServe(服务端)项目里建立com.qq.common包,包里存放Message.java和User.java两个类
User类就是获得用户输入的账号和密码

//用户信息类
package com.qq.common;

public class User  implements java.io.Serializable{
	 
	private String userId;
	private String passwd;
	public String getUserId()
	{
		return userId;
	}
	public void setUserId(String userId) {
		this.userId = userId;
	}
	public String getPasswd()
	{
		return passwd;
	}
	public void setPasswd(String passwd)
	{
		this.passwd=passwd;
	}
}

对发送服务端的信息规定一些规则
mesType为true 表明登陆成功
mesType为false 表明登录失败
所以Message就是用来判断的

package com.qq.common;
public class Message implements java.io.Serializable { //序列化     
	private String mesType;
	public String getMesType()
	{
		return mesType;
	}
	public void setMesType(String mesType)
	{
		this.mesType =mesType;
	}
}

主要思想
在这里插入图片描述
要在第一部分qq登录界面的代码中加上监听(登录按钮),将得到的账户和密码信息逐步传入到破服务器里面,再有服务器返回,判断是否能登录,如果能,打开好友列表界面,
否则,触发提示框"用户名或密码错误".
客户端
注意:我判断的是如果密码为"123456"即为正确

// 客户端的登陆界面
package com.qq.client.view;
//抽象窗口工具包
import javax.swing.*;
import java.awt.*;  
import java.awt.event.*;
import com.qq.client.model.*;
import com.qq.common.*;
public class QqClientlLogin  extends JFrame implements ActionListener{  //JFrame的子类
	
	  JLabel jup;   //定义上部需要的组件   //标签
	
	//定义中部需要的组件
	  JPanel jmid ;
      JLabel jmid_num1,jmid_key1;
      JTextField jmid_num2;    //文本框
      JPasswordField jmid_key2;   //密码框
      JCheckBox jmid_rember,jmid_automatic;  //复选框
	  
	 JPanel  jdown;         //JPanel 面板
	 JButton jdown_1,jdown_2;    //定义下部需要的组件
	 
	 public  QqClientlLogin()      //构造方法
	 {
		  //处理上部      
	    jup=new JLabel(new  ImageIcon("image/up3.png"));
	
	    //处理中部
	    jmid=new JPanel(new GridLayout(3,3));//网格布局 3行3列
	    Font font = new Font("宋体", Font.PLAIN, 25);    //创建1个字体实例
	    jmid_num1=new JLabel("QQ号码:",JLabel.CENTER);  
	    jmid_key1=new JLabel("QQ密码:",JLabel.CENTER);
	    jmid_num1.setFont(font);    jmid_key1.setFont(font);  //字体大小
        jmid_num2= new JTextField() ;    
	    jmid_key2=new JPasswordField();
	    jmid_rember=new JCheckBox("自动登录");
	    jmid_automatic=new JCheckBox("记住密码");
	    jmid_rember.setFont(font);    jmid_automatic.setFont(font); //字体大小
	    jmid_rember.setForeground(Color.blue); 	  //设置字体颜色
	    jmid_automatic.setForeground(Color.blue);
	    jmid.add(jmid_num1);
	    jmid.add(jmid_num2);
	    jmid.add(jmid_key1);
	    jmid.add(jmid_key2);
	    jmid.add(jmid_rember);
	    jmid.add(jmid_automatic);
	    
	    //处理下部
	    jdown=new JPanel(new FlowLayout()); //流式布局
	    jdown_1=new JButton(new ImageIcon("image/denglu.png"));
	    
	    //响应用户点击登录
	    jdown_1.addActionListener(this);  //监控
	    
	    jdown_2=new JButton(new ImageIcon("image/tuichu.png"));
	    jdown.add(jdown_1);
	    jdown.add(jdown_2);
	    
	    setLocation(300,300); //窗口的位置
	    add(jup,"North");   //放在最北部    
	    add(jmid,"Center");  //放在中间
	    add(jdown,"South"); //放在南部
	    setSize(700,540);     //设置大小                                  
	    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //结束窗口所在的应用程序  
	    setVisible(true);       //可见,默认是不可见
	 }
	 
	 public static void main(String[]  args)
	 {
		 QqClientlLogin  qqClientlLogin=new  QqClientlLogin();
	 } 
	public void actionPerformed(ActionEvent arg0)
	{
		if(arg0.getSource()==jdown_1);   //请求登录
		{
	     	User u=new User();
	     	u. setUserId(jmid_num2.getText().trim());   //trim() 方法用于 删除字符串的头尾空白符
	     	u. setPasswd(new String (jmid_key2.getPassword()));   //字符串;
	    	QqClientUser qqclientuser=new QqClientUser();
	    	
	     	if(qqclientuser.checkUser(u))
	     	{
	     		new QqFriendList(u.getUserId());  //好友列表
	     		this.dispose();  //关闭登陆界面
	     	}
	     	else
	     	{
	     		JOptionPane.showMessageDialog(this,"用户名或密码错误");
	     		//JOptionPane弹窗,消息提示框
	     		//showMessageDialog这个方法里有两个参数
	     		//第一个参数-确定Frame在其中显示的对话框
	     		//第二个参数message就是我们要在提示框里显示的信息
	     	}
		}
	}
}


//好友列表(也包括黑名单)
package com.qq.client.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class QqFriendList extends JFrame implements ActionListener,MouseListener {   
	//负责创建监听器的类,  点击按钮触发ActionListener事件;  鼠标监听MouseListener 
	    
	     String owner;      
	     
    // 第一张卡片(我的好友);
		JPanel jup1,jmid1,jdown1;   //jup1即为总JPanel;
		JButton jup1_1,jdown1_1;
		JScrollPane jmid1_1;      //滑动窗口;
	
	//第二张卡片(黑名单);
		JPanel jup2,jup2_button,jmid2;
		JButton jup2_1,jup2_2;
		JScrollPane jmid2_1;      //滑动窗口;
		
  //把整个JFrame设置成CardLayout布局
		CardLayout cl;

   public  QqFriendList (String ownerId)
   {   
	   Font font = new Font("宋体", Font.PLAIN, 35);    //创建字体实例

	    owner=ownerId;
	   //处理第一张卡片(我的好友);
	    jup1=new JPanel(new BorderLayout());   //第一张卡片的总JPane,BorderLayout()布局;
	   jup1_1=new JButton("我的好友");
	   jup1_1.setFont(font);  //字体
	
	   //假设有50个好友,具体是服务器返回的结果(好友信息存放在服务器)
	   jmid1=new JPanel(new  GridLayout(50,1,4,4));  //(4,4)行间距和列间距
       //给jmid1初始化50个好友
	   JLabel []jlist1=new JLabel [50];  //数组;
	   for ( int i=0; i<jlist1.length ; i++ )
	   {
		   jlist1[i]=new JLabel(i+1+"",new ImageIcon("image/touxiang.png"),JLabel.LEFT);
		   jlist1[i].addMouseListener(this);
		   jmid1.add(jlist1[i]);  
	   }
	   jmid1_1=new JScrollPane(jmid1);
	   
	   jdown1_1=new JButton("黑名单");
	   jdown1_1.addActionListener(this);     //给jdown1_1按钮增加一个监听器,这个监听器对象就是“QqFriendList ”类型的,
	                          //当用户点击了jdown1_1按钮时,会程序自动前往QqFriendList类的“actionPerformed”方法中,处理产生的事件。
	   jdown1_1.setFont(font);        //字体
	   jdown1=new JPanel(new GridLayout(1,1));//黑名单按钮
	   jdown1.add(jdown1_1);  //加入按钮;
	   
       jup1.add(jup1_1,"North");
       jup1.add(jmid1_1,"Center");
       jup1.add(jdown1,"South");
   

       
       //处理第二张卡片(黑名单)	   
       jup2=new JPanel(new BorderLayout());   //第二张卡片的总JPane,BorderLayout()布局;
       jup2_1=new JButton("我的好友");
       jup2_1.addActionListener(this);   //监听
       jup2_2=new JButton("黑名单");
	   jup2_1.setFont(font);  //字体
       jup2_2.setFont(font);
       jup2_button=new JPanel(new GridLayout(2,1));//黑名单按钮
       jup2_button.add(jup2_1);
       jup2_button.add(jup2_2);
       
	   //假设有20个黑名单,具体是服务器返回的结果(好友信息存放在服务器)
	   jmid2=new JPanel(new  GridLayout(20,1,4,4));  //(4,4)行间距和列间距
       //给jmid2初始化20个好友
	   JLabel []jlist2=new JLabel [20];  //数组;
	   for ( int i=0; i<jlist2.length ; i++ )
	   {
		   jlist2[i]=new JLabel(i+1+" ",new ImageIcon("image/heitou.png"),JLabel.LEFT);
		   jmid2.add(jlist2[i]);
	   }
	   jmid2_1=new JScrollPane(jmid2);
	   
       jup2.add(jup2_button,"North");
       jup2.add(jmid2_1);
  
       cl=new CardLayout();
       setLayout(cl);
       add(jup1,"1");  //第一张卡片jup1;
       add(jup2,"2");  //第二张卡片jup2;
        setTitle(ownerId);//显示自己的编号
       setLocation(300,300); //窗口的位置
       setSize(440,800);
       setVisible(true);     
   }
  
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		QqFriendList  qqFriendList=new  QqFriendList();
	}
	
	public void actionPerformed(ActionEvent arg0)
	{ 
		//如果点击了黑名单按钮,就显示第二张卡片
		if(arg0.getSource()==jdown1_1)
		{
			cl.show(this.getContentPane(), "2");  //用getContentPane()方法获得JFrame的内容面板
		}
		else
			if(arg0.getSource()==jup2_1)
			{
				cl.show(this.getContentPane(), "1");
			}
	}
	public void mouseEntered(MouseEvent arg0) {    //进入组件触发鼠标事件
		// TODO Auto-generated method stub
		JLabel jl=(JLabel)arg0.getSource(); //变成JLable形式
                                              //getSource()获取鼠标事件的事件源;
		jl.setForeground(Color.red);   //设置前景色
	}

	public void mouseExited(MouseEvent arg0) {  //鼠标离开组件触发的鼠标事件
		// TODO Auto-generated method stub
		JLabel jl=(JLabel)arg0.getSource();   
		jl.setForeground(Color.black);
	}
	
	public void mouseClicked(MouseEvent arg0) {   //点击鼠标触发鼠的标事件
		// TODO Auto-generated method stub
		//响应用户双击的事件,并得到好友的编号.
		if(arg0.getClickCount()==2)   //getClickCount() 获取鼠标被单机的次数
		{
			//得到该好友的编号 
			//getText() 的意思是:返回数据窗口控件中 悬浮在当前行列之上的
			String friendNum=((JLabel)arg0.getSource()).getText();
			
			//System.out.println("你希望和 "+friendNum+" 聊天");
			new QqChat(owner,friendNum);  //传入编号
		}
	}

	public void mousePressed(MouseEvent arg0) { //按下按鼠标键触发的鼠标事件
		// TODO Auto-generated method stub
	}

	public void mouseReleased(MouseEvent arg0) {   //释放鼠标键触发的鼠标事件
		// TODO Auto-generated method stub		
	}
} 

package com.qq.client.model;
import com.qq.common.*;
public class QqClientUser {
  
	  public boolean checkUser(User u)
	  {
		 return new QqClientConServer().sendLoginInfotoSSErver(u);
	  }
}

//客户端链接服务器的部分
package com.qq.client.model;

import java.util.*;
import java.net.*;
import java.io.*;
import com.qq.common.*;
public class QqClientConServer{
 
	//发送第一次请求,登录请求
	public boolean sendLoginInfotoSSErver(Object o) //发送对象到服务器
	{  
		boolean b=false;
		try
		 { 
			 Socket s=new Socket("127.0.0.1",9999);	 //传入要连接主机与端口
			 ObjectOutputStream oos=new ObjectOutputStream(s.getOutputStream()); //输出流
		     oos.writeObject(o);  //把o发出去
			 
		     ObjectInputStream ois=new ObjectInputStream(s.getInputStream());
		     Message ms=(Message)ois.readObject();  
		     if(ms.getMesType().equals("1"))
		    	b=true;
		 }
		 catch (Exception e)
		 {
			 e.printStackTrace();
		 }
		 finally
		 {
		 }
		return b;
	}
}

服务端
除了需要的Message.java和User.java两个类还需要

//服务器端控制界面,可以完成启动服务器,关闭服务器,可以管理和监控用户
package com.qq.server.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.qq.server.model.MyQqServer;
public class ControlPanel  extends JFrame implements ActionListener{
    
	JPanel jp1;
	JButton jstart,jend;

	public ControlPanel()
	{   Font font = new Font("宋体",Font.PLAIN,30);
	
		jp1=new JPanel();
		jstart=new JButton("启动服务器");
		jstart.addActionListener(this);
		jend=new JButton("关闭服务器");
		jstart.setFont(font); jend.setFont(font);
		jstart.setForeground(Color.BLUE); jend.setForeground(Color.BLUE);
		
		jp1.add(jstart);
		jp1.add(jend);
		
		add(jp1);
		setLocation(150,150);
		setSize(900,900);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //结束窗口所在的应用程序  
		setVisible(true);
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ControlPanel controlpanel =new ControlPanel();
	}
	
	public void actionPerformed(ActionEvent arg0)
	{
		if(arg0.getSource()==jstart)
		{
			new MyQqServer();
		}
	}
}

//qq服务器,监听,等待某个qq客户端链接
package com.qq.server.model;

import java.io.*; //(I/O)流库,提供大量的流类
import java.net.*;//导入网络包
import java.util.*; //实用工具类,提供了一些实用的方法和数据结构
import com.qq.common.*;

public class MyQqServer  {

	public MyQqServer ()
	{
		//try-catch语句处理异常
        try
        { 
        	//ServerSocket类表示服务器socket
        	//在9999监听
    		System.out.println("服务器正在9999监听");//简单判断一下是否启动服务端
        	ServerSocket ss=new ServerSocket(9999);
        while(true)   //服务器可多次监听
        {
            Socket s=ss.accept() ;  //阻塞,等待连接(与客户端)
        	//接收客户端发来的信息
           ObjectInputStream ois=new ObjectInputStream(s.getInputStream());
            User u=(User)ois.readObject();
            System.out.println("用户Id:"+u.getUserId()+"  密码"+u.getPasswd()); 
          	Message m=new Message();
          	 ObjectOutputStream oos=new ObjectOutputStream(s.getOutputStream());
            if(u.getPasswd().equals("123456"))
            {
            	//返回一个成功登录的信息
            	m.setMesType("1");    //mesType1 =1  表明登陆成功
                oos.writeObject(m);  
            }
            else
            {
            	m.setMesType("2"); 
                oos.writeObject(m); 
                s.close();  //关闭连接(流)
            }
        }
       }
        catch(Exception e)
        {
        	e.printStackTrace();//将错误信息全部打印
        }
        finally{
        	
        }
	}
}

4.一对一的聊天

在这里插入图片描述

进程概念:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
线程概念:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

此处要用线程用来接收客户端的消息,因为不仅仅一个用户(可实现多用户登录)
要在message类里面加入,用户编号,接受方,发送信息(即获得聊天界面文本框的内容)等
在chat类里面,加入读取,文本域中显示聊天代码等内容
在管理客户端线程中使用HashMap<关键字,值>记录发送者与服务器的socket,关键字记录用户编号,值记录socket

每个包(类)或多或少都有所更改,完整代码如下,希望对大家有所帮助,同时请大佬指正错误%%%

//
客户端代码

package com.qq.common;

public class Message implements java.io.Serializable { //序列化
     
	private String mesType;
	
	private String sender;//发送者
	private String getter; //接收者
	private String con;  //信息
	
	public String getSender()
	{
		return sender;
	}
	public void setSender(String sender)
	{
		this.sender=sender;
	}
	
	public String getGetter()
	{
		return getter;
	}
	public void setGetter(String getter)
	{
		this.getter=getter;
	}
	
	public String getCon()
	{
		return con;
	}
	public void setCon(String con)
	{
		this.con=con;
	}
	
	public String getMesType()
	{
		return mesType;
	}
	public void setMesType(String mesType)
	{
		this.mesType =mesType;
	}
}

//用户信息类
package com.qq.common;

public class User  implements java.io.Serializable{
	 
	private String userId;
	private String passwd;
	public String getUserId()
	{
		return userId;
	}
	public void setUserId(String userId) {
		this.userId = userId;
	}
	public String getPasswd()
	{
		return passwd;
	}
	public void setPasswd(String passwd)
	{
		this.passwd=passwd;
	}
	
}
// 客户端的登陆界面
package com.qq.client.view;
//抽象窗口工具包
import javax.swing.*;
import java.awt.*;  
import java.awt.event.*;
import com.qq.client.model.*;
import com.qq.common.*;
public class QqClientlLogin  extends JFrame implements ActionListener{  //JFrame的子类
	
	  JLabel jup;   //定义上部需要的组件   //标签
	
	//定义中部需要的组件
	  JPanel jmid ;
      JLabel jmid_num1,jmid_key1;
      JTextField jmid_num2;    //文本框
      JPasswordField jmid_key2;   //密码框
      JCheckBox jmid_rember,jmid_automatic;  //复选框
	  
	 JPanel  jdown;         //JPanel 面板
	 JButton jdown_1,jdown_2;    //定义下部需要的组件
	 
	 public  QqClientlLogin()      //构造方法
	 {
		  //处理上部      
	    jup=new JLabel(new  ImageIcon("image/up3.png"));
	
	    //处理中部
	    jmid=new JPanel(new GridLayout(3,3));//网格布局 3行3列
	    Font font = new Font("宋体", Font.PLAIN, 25);    //创建1个字体实例
	    jmid_num1=new JLabel("QQ号码:",JLabel.CENTER);  
	    jmid_key1=new JLabel("QQ密码:",JLabel.CENTER);
	    jmid_num1.setFont(font);    jmid_key1.setFont(font);  //字体大小
        jmid_num2= new JTextField() ;    
	    jmid_key2=new JPasswordField();
	    jmid_rember=new JCheckBox("自动登录");
	    jmid_automatic=new JCheckBox("记住密码");
	    jmid_rember.setFont(font);    jmid_automatic.setFont(font); //字体大小
	    jmid_rember.setForeground(Color.blue); 	  //设置字体颜色
	    jmid_automatic.setForeground(Color.blue);
	    jmid.add(jmid_num1);
	    jmid.add(jmid_num2);
	    jmid.add(jmid_key1);
	    jmid.add(jmid_key2);
	    jmid.add(jmid_rember);
	    jmid.add(jmid_automatic);
	    
	    //处理下部
	    jdown=new JPanel(new FlowLayout()); //流式布局
	    jdown_1=new JButton(new ImageIcon("image/denglu.png"));
	    
	    //响应用户点击登录
	    jdown_1.addActionListener(this);  //监控
	    
	    jdown_2=new JButton(new ImageIcon("image/tuichu.png"));
	    jdown.add(jdown_1);
	    jdown.add(jdown_2);
	    
	    setLocation(300,300); //窗口的位置
	    add(jup,"North");   //放在最北部    
	    add(jmid,"Center");  //放在中间
	    add(jdown,"South"); //放在南部
	    setSize(700,540);     //设置大小                                  
	    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //结束窗口所在的应用程序  
	    setVisible(true);       //可见,默认是不可见
	 }
	 
	 public static void main(String[]  args)
	 {
		 QqClientlLogin  qqClientlLogin=new  QqClientlLogin();
	 } 
	public void actionPerformed(ActionEvent arg0)
	{
		if(arg0.getSource()==jdown_1);   //请求登录
		{
	     	User u=new User();
	     	u. setUserId(jmid_num2.getText().trim());   //trim() 方法用于 删除字符串的头尾空白符
	     	u. setPasswd(new String (jmid_key2.getPassword()));   //字符串;
	    	QqClientUser qqclientuser=new QqClientUser();
	    	
	     	if(qqclientuser.checkUser(u))
	     	{
	     		QqFriendList qqList=new QqFriendList(u.getUserId());  //好友列表
	     		this.dispose();  //关闭登陆界面
	     	}
	     	else
	     	{
	     		JOptionPane.showMessageDialog(this,"用户名或密码错误");
	     		//JOptionPane弹窗,消息提示框
	     		//showMessageDialog这个方法里有两个参数
	     		//第一个参数-确定Frame在其中显示的对话框
	     		//第二个参数message就是我们要在提示框里显示的信息
	     	}
		}
	}
}

//好友列表(也包括黑名单)
package com.qq.client.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class QqFriendList extends JFrame implements ActionListener,MouseListener {   
	//负责创建监听器的类,  点击按钮触发ActionListener事件;  鼠标监听MouseListener 
	    
	     String owner;      
	     
    // 第一张卡片(我的好友);
		JPanel jup1,jmid1,jdown1;   //jup1即为总JPanel;
		JButton jup1_1,jdown1_1;
		JScrollPane jmid1_1;      //滑动窗口;
	
	//第二张卡片(黑名单);
		JPanel jup2,jup2_button,jmid2;
		JButton jup2_1,jup2_2;
		JScrollPane jmid2_1;      //滑动窗口;
		
  //把整个JFrame设置成CardLayout布局
		CardLayout cl;

   public  QqFriendList (String ownerId)
   {   
	   Font font = new Font("宋体", Font.PLAIN, 35);    //创建字体实例

	    owner=ownerId;
	   //处理第一张卡片(我的好友);
	    jup1=new JPanel(new BorderLayout());   //第一张卡片的总JPane,BorderLayout()布局;
	   jup1_1=new JButton("我的好友");
	   jup1_1.setFont(font);  //字体
	
	   //假设有50个好友,具体是服务器返回的结果(好友信息存放在服务器)
	   jmid1=new JPanel(new  GridLayout(50,1,4,4));  //(4,4)行间距和列间距
       //给jmid1初始化50个好友
	   JLabel []jlist1=new JLabel [50];  //数组;
	   for ( int i=0; i<jlist1.length ; i++ )
	   {
		   jlist1[i]=new JLabel(i+1+" ",new ImageIcon("image/touxiang.png"),JLabel.LEFT);
		   jlist1[i].addMouseListener(this);
		   jmid1.add(jlist1[i]);  
	   }
	   jmid1_1=new JScrollPane(jmid1);
	   
	   jdown1_1=new JButton("黑名单");
	   jdown1_1.addActionListener(this);     //给jdown1_1按钮增加一个监听器,这个监听器对象就是“QqFriendList ”类型的,
	                          //当用户点击了jdown1_1按钮时,会程序自动前往QqFriendList类的“actionPerformed”方法中,处理产生的事件。
	   jdown1_1.setFont(font);        //字体
	   jdown1=new JPanel(new GridLayout(1,1));//黑名单按钮
	   jdown1.add(jdown1_1);  //加入按钮;
	   
       jup1.add(jup1_1,"North");
       jup1.add(jmid1_1,"Center");
       jup1.add(jdown1,"South");
   

       
       //处理第二张卡片(黑名单)	   
       jup2=new JPanel(new BorderLayout());   //第二张卡片的总JPane,BorderLayout()布局;
       jup2_1=new JButton("我的好友");
       jup2_1.addActionListener(this);   //监听
       jup2_2=new JButton("黑名单");
	   jup2_1.setFont(font);  //字体
       jup2_2.setFont(font);
       jup2_button=new JPanel(new GridLayout(2,1));//黑名单按钮
       jup2_button.add(jup2_1);
       jup2_button.add(jup2_2);
       
	   //假设有20个黑名单,具体是服务器返回的结果(好友信息存放在服务器)
	   jmid2=new JPanel(new  GridLayout(20,1,4,4));  //(4,4)行间距和列间距
       //给jmid2初始化20个好友
	   JLabel []jlist2=new JLabel [20];  //数组;
	   for ( int i=0; i<jlist2.length ; i++ )
	   {
		   jlist2[i]=new JLabel(i+1+" ",new ImageIcon("image/heitou.png"),JLabel.LEFT);
		   jmid2.add(jlist2[i]);
	   }
	   jmid2_1=new JScrollPane(jmid2);
	   
       jup2.add(jup2_button,"North");
       jup2.add(jmid2_1);
  
       cl=new CardLayout();
       setLayout(cl);
       add(jup1,"1");  //第一张卡片jup1;
       add(jup2,"2");  //第二张卡片jup2;
        setTitle(ownerId);//显示自己的编号
       setLocation(300,300); //窗口的位置
       setSize(440,800);
       setVisible(true);     
   }
 
   public static void main(String[] args) {
		// TODO Auto-generated method stub
		//QqFriendList  qqFriendList=new  QqFriendList();
	} 
   
	public void actionPerformed(ActionEvent arg0)
	{ 
		//如果点击了黑名单按钮,就显示第二张卡片
		if(arg0.getSource()==jdown1_1)
		{
			cl.show(this.getContentPane(), "2");  //用getContentPane()方法获得JFrame的内容面板
		}
		else
			if(arg0.getSource()==jup2_1)
			{
				cl.show(this.getContentPane(), "1");
			}
	}
	public void mouseEntered(MouseEvent arg0) {    //进入组件触发鼠标事件
		// TODO Auto-generated method stub
		JLabel jl=(JLabel)arg0.getSource(); //变成JLable形式
                                              //getSource()获取鼠标事件的事件源;
		jl.setForeground(Color.red);   //设置前景色
	}

	public void mouseExited(MouseEvent arg0) {  //鼠标离开组件触发的鼠标事件
		// TODO Auto-generated method stub
		JLabel jl=(JLabel)arg0.getSource();   
		jl.setForeground(Color.black);
	}
	
	public void mouseClicked(MouseEvent arg0) {   //点击鼠标触发鼠的标事件
		// TODO Auto-generated method stub
		//响应用户双击的事件,并得到好友的编号.
		if(arg0.getClickCount()==2)   //getClickCount() 获取鼠标被单机的次数
		{
			//得到该好友的编号 
			//getText() 的意思是:返回数据窗口控件中 悬浮在当前行列之上的
			String friendNum=((JLabel)arg0.getSource()).getText();
			
			//System.out.println("你希望和 "+friendNum+" 聊天");
			QqChat qqChat=new QqChat(owner,friendNum);  //传入编号
		   Thread t=new Thread(qqChat);  
		   t.start();     // 启动线程
		}
	}

	public void mousePressed(MouseEvent arg0) { //按下按鼠标键触发的鼠标事件
		// TODO Auto-generated method stub
	}

	public void mouseReleased(MouseEvent arg0) {   //释放鼠标键触发的鼠标事件
		// TODO Auto-generated method stub		
	}
} 

//与好友聊天的界面
 //因为客户端,要处于读取的状态,因此我们把它做成一个线程
package com.qq.client.view;
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.awt.event.*;
import com.qq.common.*;
import com.qq.client.model.*;
public class QqChat extends JFrame implements ActionListener,Runnable{  //监听发送按钮
	 Font font = new Font("宋体", Font.PLAIN, 20);                                          //实现Runnable接口实现多线程,
	JTextArea jta;    //文本域  存聊天内容
	JTextField jtf;    //文本框
	JButton jb;
	JPanel jp;
	String ownerId;
	String friendId;
	
	public QqChat(String owner,String friend)
	{ 
		ownerId= owner;
		friendId=friend;
		jta=new JTextArea();
		jtf=new JTextField(25);     //25个字符那么宽
		jb=new JButton("发送");
		jb.addActionListener(this);
		jb.setFont(font);
		jp=new JPanel();
		jp.add(jtf);
		jp.add(jb);	
		add(jta,"Center");
		add(jp,"South");
		setTitle(owner+"正在和 "+friend+" 聊天");  //设置标题
		setIconImage((new ImageIcon("image/qq.png").getImage())); //获取图像
		setLocation(800,400); //窗口的位置
		setSize(600, 500);
		setVisible(true);
	}
	
	//写一个方法,让它显示消息
	public void showMessage(Message m)
	 {
		String info=m.getSender()+" 给"+m.getGetter()+" 发送:"+m.getCon()+"\r\n";
		this.jta.append(info);
	}
		
	public void actionPerformed(ActionEvent arg0) {
		// TODO Auto-generated method stub
		if(arg0.getSource()==jb)//如果用户点击了发送按钮
		{
			Message m=new Message();
			m.setSender(this.ownerId);
			m.setGetter(this.friendId);
			m.setCon(jtf.getText());   //文本框中得到 
			  
			//发送给服务器  要拿到socket,可以把socket写成静态的,从类里面拿出来;
			try 
			{ 
				ObjectOutputStream oos=new ObjectOutputStream(QqClientConServer.s.getOutputStream());
				oos.writeObject(m);
			} 
			catch (Exception e) 
			{
				e.printStackTrace();
			}	
		}	
	}
	public void run()
	{
		while(true)
		{   //一直处于读取状态(如果读不到就等待)
			try
			{
				ObjectInputStream ois=new ObjectInputStream(QqClientConServer.s.getInputStream());
				Message m =(Message)ois.readObject();		
				
				//显示
				String info=m.getSender()+"给"+m.getGetter()+"发送"+m.getCon()+"\r\n";
			    this.jta.append(info);  //(添加)追加到文本域
			}
			catch(Exception e)
			{
				e.printStackTrace();
			}
		
		}
	}
}

package com.qq.client.model;
import com.qq.common.*;
public class QqClientUser {
  
	  public boolean checkUser(User u)
	  {
		 return new QqClientConServer().sendLoginInfotoSSErver(u);
	  }
}

//客户端链接服务器的部分
package com.qq.client.model;

import java.util.*;
import java.net.*;
import java.io.*;
import com.qq.common.*;
public class QqClientConServer{
    
	public static Socket s;
	
	//发送第一次请求,登录请求
	public boolean sendLoginInfotoSSErver(Object o) //发送对象到服务器
	{  
		boolean b=false;
		try
		 { 
		     s=new Socket("127.0.0.1",9999);	 //传入要连接主机与端口
			 ObjectOutputStream oos=new ObjectOutputStream(s.getOutputStream()); //输出流
		     oos.writeObject(o);  //把o发出去
			 
		     ObjectInputStream ois=new ObjectInputStream(s.getInputStream());
		     Message ms=(Message)ois.readObject();  
		     if(ms.getMesType().equals("1"))
		    	  b=true;
		 }
		 catch (Exception e)
		 {
			 e.printStackTrace();
		 }
		 finally
		 {
		 }
		return b;
	}
}

服务端代码(message,user与客户端一样,这里不在写出,直接复制过去就可以)

//服务器端控制界面,可以完成启动服务器,关闭服务器,可以管理和监控用户
package com.qq.server.view;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.qq.server.model.MyQqServer;
public class ControlPanel  extends JFrame implements ActionListener{
    
	JPanel jp1;
	JButton jstart,jend;

	public ControlPanel()
	{   Font font = new Font("宋体",Font.PLAIN,30);
	
		jp1=new JPanel();
		jstart=new JButton("启动服务器");
		jstart.addActionListener(this);
		jend=new JButton("关闭服务器");
		jstart.setFont(font); jend.setFont(font);
		jstart.setForeground(Color.BLUE); jend.setForeground(Color.BLUE);
		
		jp1.add(jstart);
		jp1.add(jend);
		
		add(jp1);
		setLocation(150,150);
		setSize(900,900);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //结束窗口所在的应用程序  
		setVisible(true);
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ControlPanel controlpanel =new ControlPanel();
	}
	
	public void actionPerformed(ActionEvent arg0)
	{
		if(arg0.getSource()==jstart)
		{
			new MyQqServer();
		}
	}
}

//qq服务器,监听,等待某个qq客户端链接
package com.qq.server.model;

import java.io.*; //(I/O)流库,提供大量的流类
import java.net.*;//导入网络包
import java.util.*; //实用工具类,提供了一些实用的方法和数据结构
import com.qq.common.*;

public class MyQqServer  {

	public MyQqServer ()
	{
		//try-catch语句处理异常
        try
        { 
        	//ServerSocket类表示服务器socket
        	//在9999监听
    		System.out.println("服务器正在9999监听");//简单判断一下是否启动服务端
        	ServerSocket ss=new ServerSocket(9999);
        while(true)   //服务器可多次监听
        {
            Socket s=ss.accept() ;  //阻塞,等待连接(与客户端)
        	//接收客户端发来的信息
           ObjectInputStream ois=new ObjectInputStream(s.getInputStream());
            User u=(User)ois.readObject();
            System.out.println("用户Id:"+u.getUserId()+"  密码"+u.getPasswd()); 
          	Message m=new Message();
          	 ObjectOutputStream oos=new ObjectOutputStream(s.getOutputStream());
            if(u.getPasswd().equals("123456"))
            {
            	//返回一个成功登录的信息
            	m.setMesType("1");    //mesType1 =1  表明登陆成功
                oos.writeObject(m); 
                
                //单开一个线程,让该线程与该客户端保持通信
                SerConClientThread scct=new SerConClientThread(s);
                ManageClientThread.addClientThread(u.getUserId(),scct);//放到HashMap
                scct.start(); //启动与该客户端通信的线程
            }
            else
            {
            	m.setMesType("2"); 
                oos.writeObject(m); 
                s.close();  //关闭连接(流)
            }
        }
       }
        catch(Exception e)
        {
        	e.printStackTrace();//将错误信息全部打印
        }
        finally{
        	
        }
	}
}
//服务器和某个客户端的通信线程
package com.qq.server.model;

import java.net.*;
import java.io.*;
import com.qq.common.*;
public class SerConClientThread extends Thread    //继承Thread类
{  
	Socket s;
	public SerConClientThread(Socket s)
	{
		this.s=s;  //把服务器和该客户端的连接赋给s;
	}
	
    public void run()
    {
	   while(true)
	   {
		   //这里该线程就可以接受客户端的信息
		   try
		   {
			     ObjectInputStream ois=new ObjectInputStream(s.getInputStream());
			     Message m=(Message)ois.readObject();
			     
			     System.out.println(m.getSender()+"给 "+m.getGetter()+"发送: "+m.getCon());//判断接受到客户端的信息
		      
			     //拿到发送者与服务器的socket,此时就需要hashmap;
			     SerConClientThread sc=ManageClientThread.getClientThread(m.getGetter());
			     ObjectOutputStream oos=new ObjectOutputStream(sc.s.getOutputStream()); //接受人的通信线程;
		         oos.writeObject(m);
		   }
		   catch(Exception e)
		   {
			   e.printStackTrace();
		   }
	   }
   }
}

//管理客户端线程
package com.qq.server.model;

import java.util.*;

public class ManageClientThread { 
  
	 public static HashMap hm=new HashMap<String, SerConClientThread>();
	  
	  //向hm中添加一个客户通讯线程
	  public static void addClientThread(String uid,SerConClientThread ct)
	  {
		   hm.put(uid, ct);
	  }
	  
	  //返回socket
	  public static SerConClientThread getClientThread(String uid)
	  {
		  return (SerConClientThread) hm.get(uid);
	  }
	  
}

  • 85
    点赞
  • 483
    收藏
    觉得还不错? 一键收藏
  • 72
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 72
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值