一个多人聊天工具(C/S结构),实现了如下功能:


一个可视化窗口,支持鼠标点击事件

注册功能,用户可以注册自己的聊天账号,

注册信息包括:

账号名(可以用姓名来替代账号,支持中文),

密码(聊天框输入时,会自动隐藏密码),

个性签名(设置座右铭)

成功登录账号后,进入好友列表页面,登陆成功后其他用户会收到该用户的登录成功的消息,但是本人不可见(模仿实际qq登录的场景)

可以创建群聊,拉指定的好友聊天,也可以私聊


用到的工具:IDEA

主要用到的技术:Json字符串,MySQL Jdbc、druid连接池,Socket编程,Java Swing,Java awt,Juc,反射,IO、单元测试


为什么用到json字符串?

为什么用到json?什么是json(json是一种字符串)
前后端交互的标准
str{"1":"test"} key,value id为1,用户名为test
相当于给服务器发送的特殊字符,然后把特殊字符解析做的一个判断
群聊字符串:G:msg
私聊字符串:P:username-msg

将对象序列化为字符串再将字符串还原为对象,因为如果不用json处理,那么带对话框界面时字符串将会非常复杂,
那么对字符串处理会很麻烦,用到json会很简单

如何添加json?

谷歌的gson库
所有第三方创建,成熟的开源库,创建者模式
private static final Gson GSON = new GsonBuilder().create();

json字符串:带界面后更复杂,字符串拆分比较复杂,{"1":"value",}

json序列化 Object -> json字符串 反序列化 json -> Object

客户端 (向服务器发送 O -> json;接收服务器,json ->O) <-> 服务器(向客户端发送,O变为json;接收客户端 json变为O)

 

Java序列化 把对象编程二进制流,一个个字节

判断普通字符串和json字符串?
只要字符串开头不是{}就是普通字符串

{"type":"1","content":".."}

深拷贝与浅拷贝:

深拷贝:比如说student类中包含了其他类也创建了一个对象
浅拷贝:不创造新对象,与被复制的是一个对象

深浅拷贝和正反序列化的关系?

//序列化都是深拷贝
//{"id":1,"password":"999","brief":"帅","usernames":["test4","test2","test3"]}
@Test
public void object2Json(){
User user = new User();
user.setId(1);
user.setPassword("999");
user.setBrief("帅");
Set<String> strings = new HashSet<>();
strings.add("test2");
strings.add("test3");
strings.add("test4");
user.setUsernames(strings);
String str = CommUtils.object2Json(user);
System.out.println(str);
}
//反序列化是浅拷贝
//["test4","test2","test3"]
@Test
public void json2object(){
String jsonStr = "{\"id\":1,\"password\":\"999\",\"brief\":\"帅\"," +
"\"usernames\":[\"test4\",\"test2\",\"test3\"]}";
User user = (User) CommUtils.json2object(jsonStr,User.class);//强转
System.out.println(user.getUsernames());
}

第一步如何从java程序去操作数据库?
数据源和传统jdbc格式
传统jdbc:第一步加载驱动 Class.forName("com.mysql.jdbc.Driver");
第二步获取连接 Connection= DriverManager.getConnection(url)
第三步执行sql,String sql = "select id,name,created_time,modify_time from memo_group";
Statement(语句执行计划):Statement statement = connection.createStatement();SQL注入漏洞 如何避免?
PreparedStatement

结果集,封装查询后的对象结果 resultSet = statement.executeQuery(sql);

CURD:查询:ResultSet resultSet = statement.executeQuery(sql); String sql = "select id,name,created_time,modify_time from memo_group";

更新(插入)int effect = statement.executeUpdate(sql);String sql = "insert into map(keymap,valuemap) values ('4','2019-08-30 10:00:00')";
更新(删除):int effect = statement.executeUpdate(sql); String sql = "delete from memo_group where id = 6661 ";
更新操作返回整形而不是结果集,告诉你受影响行数,若不跟where后缀,一行行删,删几行返回几 除了更新操作返回结果集,其余都是返回int
还有truncate操作?
谁快?
truncate,没有返回值,怎么删,直接将表文件大小改为0,而且truncate操作不能回滚,delete from可以回滚
将表打开一行行删除

第四步、关闭资源(三个)
Connection
Statement
ResultSet

那么除了传统JDBC,还有数据源:datasource连接数据库有何步骤区别?
除了第一步不一样,其他步骤全一样
1.加载数据源(区别:类比线程池)(注意不是线程池)),二jdbc传统格式可以类比Thread类
要使用多线程统统用的线程池,有啥好处?1.线程池有空闲线程在等待任务,当需要用其执行任务的
时候少了创建销毁过程,更快执行 2.当一堆线程被一个对象管理时,方便管理也方便监控
3.普通线程执行任务执行一次就完了,线程池线程可以变废为宝,减少cpu资源开销

类比结果:传统jdbc调用connection.close()确实关了连接,每个用户都要创建一次,当用户连接操作执行完后,关了
连接池放了一堆connection,当有用户到达的时候,我已经创建好了,直接取就完了
当用户调用数据源的connection.close,他并不是将其连接关闭,而是将连接再放回池子里,方便后续用户复用

有哪些商用数据源?
c3p0(最早版本,已经不用了,效率很低)
druid(性能并不是最高的,但功能完全,方便监控)
Spring Hakari(Spring内置数据库,效率很高)


用到的依赖:阿里巴巴数据源、Google-gson、junit(单元测试)库、密码加密包 commons-codec,apche(web 服务器软件)组织的一个密码加密包

加密工具用法:String password = DigestUtils.md5Hex("123");//加密密码

  <dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<!--密码加密-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.0</version>
</dependency>
</dependencies>

第一步:创建maven项目,创建maven框架 (用其搭好的框架):

org.jetbrains.idea.maven.model.MavenArchetype@88f75e0f

第二步:下载依赖

第三步:添加数据源:

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost/hhy_chatroom?charset=utf8&useSSL=false&allowPublicKeyRetrieval=true
username=root
password=hhyuan
filters=stat
initialSize=5
maxActive=30
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
validationQuery=SELECT 1
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
poolPreparedStatements=false
address=127.0.0.1
port=6666

配置文件在resources,与java同目录,将其右键设置为root resources目录
创建database.properties和socket.properties(资源文件夹)

localhost 本机默认 3306 charset=utf8连接编码集为utf-8
连接底层为TCP连接,http,https底层都为TCP连接,SSL(安全通道),而如果是普通的tcp连接
一旦被监听管道,用户名和密码将都会被看见,如果是SSL的话,是加密的,即便监听管道,也看不到
一般情况下,mysql要求我们用安全连接(SSL),但由于其比较麻烦,需要配置证书,一般而言,本地不需要
所以useSSL=false设置为false

再来说一下其他的参数
initialSize相当于线程中的核心池,默认创造五个连接给你连
maxWait=60000 数据源可以空闲也可以等待,但不能无限等待,防止无限等待
validationQuery=SELECT 1测试连接对还是不对

第四步:

建一个类,写一个封装公共工具方法,加载配置文件、json序列化

第一步加载数据文件
我们先把配置文件加载到程序中,不管是是数据库操作还是后面其他操作,都需要加载配置文件
所以应将其作为公共方法,CommUtils(封装我们公共工具的方法:加载配置文件、json序列化等)
哪些属于公共方法?
SE部分,数组拷贝copy方法、排序(都是类名.方法名,因为所有程序都会用到)

 
package com.hhy.java.util;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import java.io.IOException;
import java.io.InputStream;
import java.util.Properties; /**
* @Information:封装公共工具方法,如加载配置文件、json序列化
* @Author: HeHaoYuan
* @Date: Created at 13:48 on 2019/8/16
* @Package_Name: com.java.util
*/
public class CommUtils {
//成熟的第三方开源库,gson在谷歌库上
private static final Gson GSON = new GsonBuilder().create();
/**
* @Author: HeHaoYuan
* @Date: 2019/8/16
* @Description:
加载配置文件名称,fileName是其名称
先获取一个文件或者网络输入流 用最顶层的InputStream接受 */
//ctrl+shift+T 单元测试,由开发人员写
public static Properties loadProperties(String fileName){
Properties properties = new Properties();
//获取类加载器,加载指定文件变为输入流()只要资源文件和类在一个路径下,就可以加载它变为输入流
InputStream in = CommUtils.class.getClassLoader().getResourceAsStream(fileName);//核心代码
try{
//加载资源
properties.load(in);
} catch (IOException e) {
return null;
}
return properties;
} /**
* @Author: HeHaoYuan
* @Date: 2019/8/16
* @Description:
将任意字符串变为json字符串 */
public static String object2Json(Object obj){
return GSON.toJson(obj);
} /**
* @Author: HeHaoYuan
* @Date: \
* @Description:反序列化(反射原理) jsonStr json字符串 objClass 反序列化的类反射对象
*/
public static Object json2object(String jsonStr,Class objClass){
return GSON.fromJson(jsonStr,objClass);
}
}

单元测试:对其是否能够成功的加载数据源,以及json的序列化和反序列化

package com.hhy;

import com.hhy.java.client.entity.User;
import com.hhy.java.util.CommUtils;
import org.junit.Assert;
import org.junit.Test; import java.util.HashSet;
import java.util.Properties;
import java.util.Set; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 13:59 on 2019/8/16
* @Package_Name: com.java.util
*/
public class CommUtilsTest { @Test
public void loadProperties() {
String fileName = "datasource.properties";
Properties properties = CommUtils.loadProperties(fileName);
// System.out.println(properties);
Assert.assertNotNull(properties);//我认为他不为空 显示结果如果为绿色的√说明测试正确
//Assert
} //序列化都是深拷贝
//{"id":1,"password":"999","brief":"帅","usernames":["test4","test2","test3"]}
@Test
public void object2Json(){
User user = new User();
user.setId(1);
user.setPassword("999");
user.setBrief("帅");
Set<String> strings = new HashSet<>();
strings.add("test2");
strings.add("test3");
strings.add("test4");
user.setUsernames(strings);
String str = CommUtils.object2Json(user);
System.out.println(str);
} //反序列化是浅拷贝 @Test
public void json2object(){
String jsonStr = "{\"id\":1,\"password\":\"999\",\"brief\":\"帅\"," +
"\"usernames\":[\"test4\",\"test2\",\"test3\"]}";
User user = (User) CommUtils.json2object(jsonStr,User.class);
System.out.println(user.getUsernames());
} }

单元测试:我们需要对是否能进行MySQL编程进行测试,测试功能为插入数据(注册功能),查询数据(登录验证功能),针对查询数据(登陆功能)又做了使用statement和preparestatement对比,

我们选择后者,防止sql注入,利用sql语句关键字、注释,从而无需验证就能登录,这对用户的隐私是不安全的

之后再关闭数据源

package com.hhy.java;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.hhy.java.util.CommUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Assert;
import org.junit.Test; import java.sql.*;
import java.util.Properties; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 14:09 on 2019/8/16
* @Package_Name: com.java.java
*/
public class JDBCTest {
private static DruidDataSource dataSource;
//加载配置文件,随着类加载运行,仅一次
static {
Properties props = CommUtils.loadProperties("datasource.properties");
try {
dataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(props);
} catch (Exception e) {
e.printStackTrace();
}
} //查询
//八月 16, 2019 2:40:16 下午 com.alibaba.druid.pool.DruidDataSource info
//信息: {dataSource-1} inited //阿里巴巴数据源打印,-1表示数据库的一个连接
//id为1,用户名为:java,密码为:123,简介为:帅 @Test
public void testQuery(){
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
connection = (Connection)dataSource.getPooledConnection();
String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
statement = connection.prepareStatement(sql);
String user = "hhl";
String pass = "123";
//为什么用preparedstatement?防止sql注入,由于有占位符存在,不会作为关键字处理-
((PreparedStatement) statement).setString(1,user);
((PreparedStatement) statement).setString(2,pass); resultSet = ((PreparedStatement) statement).executeQuery();
if (resultSet.next()){
System.out.println("登陆成功");
}
else {
System.out.println("登录失败");
}
// while (resultSet.next()){
// int id = resultSet.getInt("id");
// String userName = resultSet.getString("username");
// String password = resultSet.getString("password");
// String brief = resultSet.getString("brief");
// System.out.println("id为"+id+",用户名为:"+userName+",密码为:"+password+
// ",简介为:"+brief);
//
// }
}
catch (SQLException e){ }finally {
closeResource(connection,statement,resultSet);
}
} //插入数据
@Test
public void testInsert(){
Connection connection = null;
PreparedStatement statement = null;
try {
connection = (Connection)dataSource.getPooledConnection();
String password = DigestUtils.md5Hex("123");
String sql = "INSERT INTO user(username,password,brief)"+"VALUES(?,?,?)";
statement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
statement.setString(1,"test3");
statement.setString(2,password);
statement.setString(3,"还是帅!");
int rows = statement.executeUpdate();
Assert.assertEquals(1,rows);
} catch (SQLException e) {
e.printStackTrace();
}finally {
closeResource(connection,statement);
} } @Test
public void testLogin(){
String username = "hhy'"; //String username = "java' OR 1 = 1"; sql注入
//SELECT * FROM user WHERE username = 'java' OR 1 = 1 AND password = '1234' sql注入 //String username = "java'--"; --表示注释
String password = "123";
Connection connection = null;
Statement statement = null;
ResultSet resultSet =null;
try {
connection = (Connection)dataSource.getPooledConnection();
String sql = "SELECT * FROM user WHERE username = '"+username+"" +
" AND password = '"+password+"'";
//System.out.println(sql);
statement = connection.createStatement();
resultSet = statement.executeQuery(sql);
if (resultSet.next()){
System.out.println("登陆成功");
}
else {
System.out.println("登录失败");
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
closeResource(connection,statement,resultSet);
}
} public void closeResource(Connection connection, Statement statement){
if (connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} public void closeResource(Connection connection,Statement statement,ResultSet resultSet){
closeResource(connection,statement);
if (resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

加载数据源
获取数据库连接
准备SQL语句
关闭连接

注册用户:inset

用户登录:select

更新用户信息:update

1、2、4是公有过程,我们将其置为父类,子类只需要继承父类,那么这三个功能就都有了,子类只需要扩展自己的第三步 父类名称为BasedDao
加载DruidDataSource数据源,首先获取其配置文件
Properties properties = CommUtils.loadProperties("datasource.properties");

BasedDao类,获取数据源、连接、关闭资源:

package com.hhy.java.client.dao;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.alibaba.druid.pool.DruidPooledConnection;
import com.hhy.java.util.CommUtils; import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties; /**
* @Information:封装基础dao操作,获取数据源、连接、关闭资源等
* @Author: HeHaoYuan
* @Date: Created at 19:11 on 2019/8/16
* @Package_Name: com.hhy.com.java.client.dao
*/
public class BasedDao {
private static DruidDataSource dataSource;//阿里一个数据源
static{
Properties properties = CommUtils.
loadProperties("datasource.properties");
try {
dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
} protected DruidPooledConnection getConnection(){
try {
return (DruidPooledConnection) dataSource.getPooledConnection();
} catch (SQLException e) {
System.out.println("数据库连接失败");
e.printStackTrace();
}
return null;
} protected void closeResources(Connection connection, Statement statement){
if (connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} //关闭数据源的重载函数
protected void closeResources(Connection connection, Statement statement, ResultSet resultSet){
closeResources(connection,statement);
if (resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} }

注册账号,客户端将信息提交到服务器,进行tcp连接,注册成功后,服务端将信息返回到客户端(登录界面),那么tcp连接就断掉了
比较复杂(牵扯到TCP连接的注销模块),现在我们把注册功能放到客户端,但是应该放到服务器,注册成功后信息就在MySQL服务器本地(也可用MySQL第三方服务器),但实际应该放到服务端,而我们把注册模块放到了客户端,为了简化开发

封装一个类(用户注册和登录模块),写 “用户的注册” 模块,注册本质上就是在数据库中插入数据,再次类中另外写一个方法,需要外界传递给本类用户的注册信息,通过statement的setString将其安置到数据库中。。。

关键语句: connention.getpreparestatement (采用preparestatement防止SQL注入)    preparestatement.setstring(1,"")(预编译,帮助自动生成代码)
登录失败返回null,否则为空 返回成功后的结果集(用户名,密码,brief),应该有从结果集到对象的过程,private User getUser(ResultSet resultSet){}

package com.hhy.java.client.dao;

import com.hhy.java.client.entity.User;
import org.apache.commons.codec.digest.DigestUtils; import java.sql.*; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 19:26 on 2019/8/16
* @Package_Name: com.hhy.com.java.client.dao
*/
public class AccountDao extends BasedDao{
//用户注册需要那些对象 insert //是否注册成功
public boolean userReg(User user){
Connection connection = null;
PreparedStatement statement = null;
try {
connection = getConnection();
//预编译
String sql = "INSERT INTO user(username,password,brief)"+"VALUES(?,?,?)";//需要外界传递
statement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
statement.setString(1,user.getUsername());
statement.setString(2,DigestUtils.md5Hex(user.getPassword()));
statement.setString(3,user.getBrief());
int rows = statement.executeUpdate();
if (rows == 1){
return true;
}
}
catch (SQLException e){
System.err.println("用户注册失败");
e.printStackTrace();
}finally {
closeResources(connection,statement);
}
return false;
} public User userlogin(String username,String password){
Connection connection = null;//两个业务不能用同一个connection,两个事物
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = getConnection();
String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
statement = connection.prepareStatement(sql);
statement.setString(1,username);
statement.setString(2,DigestUtils.md5Hex(password));
resultSet = statement.executeQuery();
if (resultSet.next()){
User user = getuser(resultSet);
return user;
}
}catch (SQLException e){
System.out.println("用户登录失败");
e.printStackTrace(); }finally {
closeResources(connection,statement,resultSet);
}
return null;
} private User getuser(ResultSet resultSet) throws SQLException {
User user = new User();
user.setId(resultSet.getInt("id"));
user.setUsername(resultSet.getString("username"));
user.setPassword(resultSet.getString("password"));
user.setBrief(resultSet.getString("brief"));
return user;
}
}

既然需要进行用户的注册,那么就需要封装一个用户的User类,提供getter和setter方法:user 表 为什么用Integer不用int 为什么用数组不用动态数组?因为前者默认为null,后者默认为int,数据库中默认用包装类

package com.hhy.java.client.entity;

import lombok.Data;

import java.util.Set;

/**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 19:34 on 2019/8/16
* @Package_Name: com.hhy.java.client.entity
*/ @Data
public class User {
private Integer id;//int默认值为0,Integer为null,所以数据库中默认基本类型用包装类
private String username;
private String password;
private String brief;
private Set<String> usernames; public Set<String> getUsernames() {
return usernames;
} public void setUsernames(Set<String> usernames) {
this.usernames = usernames;
} public Integer getId() {
return id;
} public void setId(Integer id) {
this.id = id;
} public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
} public String getBrief() {
return brief;
} public void setBrief(String brief) {
this.brief = brief;
}
}

单元测试:用户是否能够正确完成注册,其实就是进行get和set方法:

package com.hhy.java.client.dao;

import com.hhy.java.client.entity.User;
import org.junit.Assert;
import org.junit.Test; import static org.junit.Assert.*; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 20:10 on 2019/8/16
* @Package_Name: com.hhy.java.client.dao
*/
public class AccountDaoTest {
private AccountDao accountDao = new AccountDao(); @Test
public void userReg() {
//注册
User user = new User();
user.setUsername("aaa");
user.setPassword("456");
user.setBrief("帅还是帅");
boolean flag = accountDao.userReg(user);
Assert.assertTrue(flag);
} @Test
public void userlogin() {
//查询注册
String username = "aaa";
String password = "456";
User user = accountDao.userlogin(username,password);
System.out.println(user);
Assert.assertNotNull(user);
}
}

写完注册功能后,我们开始写登录的业务,此时就需要写服务端和客户端,ServerSocket 服务端的类,相当于建立一个基站,用accept()侦听客户端的连接,它会一直阻塞,

直到有新的客户端和它连接

客户端 Socket类,只需要传入服务端的ip和端口号即可,服务端若不传,默认本地端口号(127.0.0.1),再只需传一个端口号即可

有了json字符串帮助,给服务器发送个对象过去,让其解析为json字符串(序列化)

可以规定与服务器通信的方式,http由来(格式一样,只不过具体字段不一样)
类似 我们也创造一种通信方式

字段:
type:1 表示用户注册到服务器 哪种行为
content:username 具体发送的内容
to: 给谁发,需要有值,不需要为空

将上述包装为类,服务器与客户端传递信息的载体 vo类

既然是客户端与服务端进行通信,我们需要封装一个类进行文本输入输出:

package com.hhy.java.vo;

/**
* @Information:客户端和服务端都会用到的类,将其封装,type(行为的类型 1表示注册,表示私聊)、
* content(发送到服务器的具体内容)、to(给谁发,私聊告知服务器要将信息发给哪个客户)
* 服务器与客户端传递信息的载体
* @Author: HeHaoYuan
* @Date: Created at 0:01 on 2019/8/17
* @Package_Name: com.hhy.java.vo
*/
public class MessageVO {
private String type;
private String content;
private String to; public String getType() {
return type;
} public void setType(String type) {
this.type = type;
} public String getContent() {
return content;
} public void setContent(String content) {
this.content = content;
} public String getTo() {
return to;
} public void setTo(String to) {
this.to = to;
}
}

服务端类:

ServerSocket 服务端的类,相当于建立一个基站,如何侦听客户端连接?accept()方法,它会一直阻塞,
知道有新的客户端和它连接 聊天室服务端:ServerSocket:服务端基站  IP不传,默认本地建立基站,只需要传递端口号

服务端通过run方法接收,获取i/o,不断监听客户端的连接

从客户端发来的是一个字符串,然后将其反序列化为对象Object
通过之前规定的通信协议字段进行解析
发回的相应内容为在线的用户名变为json字符串再发回去 类似http的响应

服务端注册群:1、注册群   2、想客户端发送群set
                         

package com.hhy.java.server;

import com.hhy.java.util.CommUtils;
import com.hhy.java.vo.MessageVO;
import com.mysql.fabric.xmlrpc.Client; import javax.swing.*;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* @Information:处理每个客户端连接服务器端的类
* @Author: HeHaoYuan
* @Date: Created at 0:05 on 2019/8/17
* @Package_Name: com.hhy.java.server
*/
public class MultiThreadServer {
private static final String IP;
private static final int port;
//缓存当前服务器所有在线的客户端信息 keyString是用户名
private static Map<String,Socket> clients = new ConcurrentHashMap<>();//线程安全的HashMap
//缓存当前服务器注册的所有群名称以及群好友
private static Map<String,Set<String>> groups = new ConcurrentHashMap<>(); //类加载时初始化
static {
Properties pros = CommUtils.loadProperties("socket.properties");
IP = pros.getProperty("address");
port = Integer.parseInt(pros.getProperty("port"));
} //处理每个客户端连接的内部类,新建新的连接 去处理,否则accept会一直阻塞
private static class ExecuteClient implements Runnable{
private Socket client;
private Scanner in;
private PrintStream out; public ExecuteClient(Socket client) {
this.client = client;
try {
this.in = new Scanner(client.getInputStream());
this.out = new PrintStream(client.getOutputStream(),
true,"UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
} //服务端接受客户端发来的信息
@Override
public void run() {
while (true){
if(in.hasNext()){
String jsonStrFromClient = in.nextLine();
//反序列化为MessageVo
MessageVO msgFromClient = (MessageVO)
CommUtils.json2object(jsonStrFromClient,MessageVO.class);
if (msgFromClient.getType().equals("1")){
//新用户注册到客户端
String userName = msgFromClient.getContent();
//将当前在线所有用户的用户名发回客户端 Http的响应
MessageVO meg2Client = new MessageVO();
meg2Client.setType("1");
meg2Client.setContent(CommUtils.object2Json(clients.keySet())); out.println(CommUtils.object2Json(meg2Client));
//将新上线的用户信息发回给当前已在线的所有用户,先发信息再存,否则会将自己消息发给自己
sendUserLogin("新上线通知:"+userName);
//将当前新用户注册到服务端缓存
clients.put(userName,client);
System.out.println(userName+"上线了!");
System.out.println("当前聊天室共有"+clients.size()+"人");
}
else if (msgFromClient.getType().equals("2")){
//用户私聊
String friendName = msgFromClient.getTo();
Socket clientSocket = clients.get(friendName);
try {
PrintStream out = new PrintStream(clientSocket.getOutputStream(),
true,"UTF-8");
//发送内容
MessageVO msg2Client = new MessageVO();
msg2Client.setType("2");
msg2Client.setContent(msgFromClient.getContent());
System.out.println("收到私聊信息,内容为"+msgFromClient.getContent());
out.println(CommUtils.object2Json(msg2Client));
} catch (IOException e) {
e.printStackTrace();
}
}
else if(msgFromClient.getType().equals("3")){
//注册群
String groupName = msgFromClient.getContent();
//该群的所有群成员
Set<String> friends = (Set<String>) CommUtils.
json2object(msgFromClient.getTo(),Set.class);
groups.put(groupName,friends);
System.out.println("有新的群注册成功,群名称为"+
groupName+",一共有"+groups.size()+"个群");
}
else if (msgFromClient.getType().equals("4")){
//群聊信息
System.out.println("服务器收到的群聊信息为:"+msgFromClient.getContent());
String groupName = msgFromClient.getTo();
Set<String> names = groups.get(groupName);
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()){
String socketName = iterator.next();
Socket client = clients.get(socketName);
try {
PrintStream out = new PrintStream(client.getOutputStream(),true,
"UTF-8");
MessageVO messageVO = new MessageVO();
messageVO.setType("4");
messageVO.setContent(msgFromClient.getContent());
//群名-[] 服务器第一次向拉进来的好友需要特别发送群名称和群成员
//因为他都不知道自己被拉进群,所以需要发送
messageVO.setTo(groupName+"-"+CommUtils.object2Json(names));
out.println(CommUtils.object2Json(messageVO));
System.out.println("服务端发送的群聊信息为"+messageVO.getContent());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
} //向所有在线用户发送新用户上线信息
private void sendUserLogin(String msg){
for (Map.Entry<String,Socket> entry : clients.entrySet()){
Socket socket = entry.getValue();//取出当前实体的每一个socket,再调用其输出流
try {
PrintStream out = new PrintStream(socket.getOutputStream(),
true,"UTF-8");
out.println(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
} } public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
//创建线程池
ExecutorService executors = Executors.newFixedThreadPool(50);
for (int i = 0;i < 50;i++){
System.out.println("等待客户连接...");
Socket client = serverSocket.accept();
System.out.println("有新的连接,端口号为"+client.getPort());
executors.submit(new ExecuteClient(client));//具体连接交给子线程去处理,服务器再继续侦听新的连接
}
}
}

客户端类:客户端类:Socket 传入服务端ip、端口号,服务端若不传,默认本地端口号,只需传一个端口号 

package com.hhy.java.client.entity.service;
import com.hhy.java.util.CommUtils; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Properties; /**
* @Information:当用户点击登录时,客户端进行页面跳转,连接还在,页面不能直接的关闭 客户端连接的输入输出流
* @Author: HeHaoYuan
* @Date: Created at 12:39 on 2019/8/17
* @Package_Name: com.hhy.java.client.entity.service
*/
public class Connect2Service {
private static final String IP;
private static final int port;
static {
Properties pros = CommUtils.loadProperties("socket.properties");
IP = pros.getProperty("address");
port = Integer.parseInt(pros.getProperty("port"));
} private Socket client;
private InputStream in; //获取客户端的输入输出流
private OutputStream out; public Connect2Service() {
try {
client = new Socket(IP,port);
//建立连接成功的时候,输入输出流可以得到
in = client.getInputStream();
out = client.getOutputStream();
} catch (IOException e) {
System.out.println("与服务器建立连接失败");
e.printStackTrace();
}
}
//获取方法 拿输入流获取服务器发来的信息
public InputStream getIn(){
return in;
}
//拿输出流给服务器发信息
public OutputStream getOut(){
return out;
}
}

Swing-JDK1.2界面编程包
JPanel-组件中存放其他组件的基础,相当于前端的div
JLabel-标签
JTextField-输入框
JButton-按钮,右键选择 Create Listener,可以用来触发点击后的事件

JPasswordField:密码文本框

JScrollPane:滑动盘子,在滑动好友、群组列表时会用到这个结构

多行文本控制(用于多行聊天)JTextArea
鼠标点击事件:MouseListener- >MousePressed

界面问题:每一个界面相当于一个线程(有自己对应的主方法),最小化后并未关闭,只有x才是关闭,所有弹起的界面都不能是主方法,必须通过其他界面来调用它,我们将其中一个主界面设置为主方法,其他方法的主方法变为构造,放到主界面类里面。就可生成其他界面对象

点击页面(按钮),就会触发事件 createListener ActionListenListener FocusListener,鼠标放到触发,MouseListener鼠标点击事件

JOptionPane消息弹框 frame(基于什么),message:注册成功,title:提示信息,JOptionPane.INFORMATION_MESSAGE

注册密码是,由于不能直接读加密,所以String.valueof(passwordText.getPassword)

接下来图画好后,右键点击Form ,Jump Source到对应图框的类里面,写类信息

第一个:登录页面的编写,之后在这里的主函数中    启动 用户登录界面 对话框

login 失败 还是 成功?

/失败 保留登录界面,提示用户重新输入信息

//成功 成功后 与服务器建立连接,将用户名与用户Socket注册到服务端

//加载用户列表页面 提示已登录用户新用户上线
有同时监听服务端发来的用户上线信息并且更新用户列表

点击登陆后,和服务器建立连接,登陆后此连接保存,在不同页面跳转的时候,此连接一直传递

package com.hhy.java.util;

import com.hhy.java.client.dao.AccountDao;
import com.hhy.java.client.entity.User;
import com.hhy.java.client.entity.service.Connect2Service;
import com.hhy.java.client.entity.service.FriendsList;
import com.hhy.java.client.entity.service.userReg;
import com.hhy.java.vo.MessageVO; import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;
import java.util.Set; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 17:34 on 2019/8/16
* @Package_Name: com.java.util
*/
public class TestGUI {
private JPanel GUITestPanel;
private JPanel qqPanel;
private JPasswordField passwordField;
private JTextField nameField;
private JLabel username;
private JLabel password;
private JLabel My_chatroom;
private JButton register;
private JButton login;
private JFrame frame; //单例模式
private AccountDao accountDao = new AccountDao(); //默认创建无参构造
public TestGUI() {
frame = new JFrame("用户登录");
//最外层盘子 ,防止无限递归
frame.setContentPane(GUITestPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.pack();
frame.setVisible(true); //注册按钮
register.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//弹出注册页面
new userReg();
}
}); //登录按钮
login.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//登录页面 //校验用户信息
String username =nameField.getText();
String password = String.valueOf(passwordField.getPassword());
//查询数据库,根据user返回值是否为null
User user = accountDao.userlogin(username,password);
if (user != null){
//成功 ,加载用户列表
JOptionPane.showMessageDialog(frame,"登录成功!","成功信息",
JOptionPane.INFORMATION_MESSAGE);
frame.setVisible(false);
//与服务器建立连接,将当前用户的用户名与密码发到服务端
Connect2Service connect2Service = new Connect2Service();
MessageVO msg2Server = new MessageVO();
msg2Server.setType("1");//注册
msg2Server.setContent(username);
//把要发的字符串序列化为json对象
String json2Server = CommUtils.object2Json(msg2Server);
//要发送信息就要获取连接的输出流 autoFlush自动刷新缓存区,编码为utf-8
try {
PrintStream out = new PrintStream(connect2Service.getOut(),
true,"UTF-8");
out.println(json2Server);//将当前用户名信息发给服务端
//读取服务端发回的所有在线用户信息
Scanner in = new Scanner(connect2Service.getIn());
if (in.hasNext()){
String msgFromServerstr = in.nextLine();
MessageVO msgFromServer = (MessageVO) CommUtils.json2object(
msgFromServerstr,MessageVO.class);
Set<String> users = (Set<String>) CommUtils.
json2object(msgFromServer.getContent(),Set.class);
System.out.println("所有在线用户为:"+users);
//加载用户列表界面
//将当前用户名、所有在线好友、与服务器建立连接传递到好友列表界面
new FriendsList(username,users,connect2Service);
}
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
}
else {
//失败,停留当前登录页面,提示用户信息错误
JOptionPane.showMessageDialog(frame,"登录失败!","错误信息",
JOptionPane.ERROR_MESSAGE);
}
}
});
} public static void main(String[] args) {
new TestGUI();
}
}

点击一次JButton就是触发一次事件,所以将除了主对话框外  其他事件包装为类,写好构造方法,去掉其他类自己的主函数,再在刚才写的主页面里面启动整个事件即可,有了其他类事件的构造方法,

就可执行其相应的业务方法

用户注册包装类,需要注意的是里面的逻辑,若注册成功将关闭注册对话框,否则一直停留:

package com.hhy.java.client.entity.service;

import com.hhy.java.client.dao.AccountDao;
import com.hhy.java.client.entity.User; import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 21:28 on 2019/8/16
* @Package_Name: com.hhy.java.client.entity.service
*/
public class userReg { private JTextField account;
private JPasswordField pass;
private JTextField myself;
private JLabel username;
private JLabel password;
private JLabel introduce;
private JPanel first;
private JPanel second;
private JPanel third;
private JPanel whole;
private JButton 注册按钮;
//关于数据库增删改查用户的类
private AccountDao accountDao = new AccountDao(); public userReg() {
JFrame frame = new JFrame("用户注册");
frame.setContentPane(first);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.pack();
frame.setVisible(true); //点击注册按钮,将信息持久化到db中,成功弹出提示框 注册按钮.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//弹出提示框 //正确的提示框
//JOptionPane.showMessageDialog(frame,"注册成功!","提示信息",JOptionPane.INFORMATION_MESSAGE); String username = account.getText();
String password = String.valueOf(pass.getPassword());
String brief = myself.getText();
//将输入信息包装为user类,保存到数据库中
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setBrief(brief);
//调用dao对象
if (accountDao.userReg(user)){
//成功后返回登录页面
JOptionPane.showMessageDialog(frame,"注册成功!","成功信息",JOptionPane.INFORMATION_MESSAGE);
frame.setVisible(false);//将当前注册页面不可见的方法
}
else {
//弹出提示框
//仍保留当前注册页面
JOptionPane.showMessageDialog(frame,"注册失败!","错误信息",JOptionPane.ERROR_MESSAGE);
} }
});
}
}

好友列表GUI生成后,再次用构造方法生成

 

用户登录成功后,弹出的好友列表包装类,需要注意的是,登陆成功后,登录页面就会消失,进而弹出好友列表对话框:

在好友登陆成功弹窗提示时,要先弹窗再注册(刷新好友列表),新用户先存到map里,否则会提醒自己上线的信息,所以要先发上线信息

请求响应

客户端
客户端将用户名发送到服务端
同时接收服务端发回的所有在线用户信息
服务端
在服务端加载所有所有在线用户信息
服务端将所有在线用户信息发一个上线通知给客户端
将新用户通过(Map.Entry<String,Socket>保存到缓存中

当客户端收到服务端上线用户消息的时候加载在线好友信息

当好友上线的时候,如何弹窗提醒?
后台监听,服务器将用户的私聊界面唤醒

从登录界面到好友列表界面需不需要传参?
需要,传递username(本账号名称)、所有在线好友、与服务器建立连接传递到好友列表界面

package com.hhy.java.client.entity.service;

import com.hhy.java.util.CommUtils;
import com.hhy.java.vo.MessageVO; import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 15:35 on 2019/8/17
* @Package_Name: com.hhy.java.client.entity.service
*/
public class FriendsList {
private JPanel friendsPanel;
private JScrollPane friendsList;
private JButton creategroupButton;
private JLabel groupLabel;
private JScrollPane groupPanel;
private JLabel friendLabel;
private JFrame frame; private String username;
//存储当前所有好友列表 private Set<String> users;
//存储当前所有的群名称以及相应群成员
private Map<String,Set<String>> groupList = new ConcurrentHashMap<>();
private Connect2Service connect2Service; //缓存所有私聊界面
private Map<String,PrivateChatGUI>privateChatGUIMap = new ConcurrentHashMap<>();
//缓存所有群聊界面
private Map<String,GroupChatGUI> groupChatGUIMap = new ConcurrentHashMap<>(); //好友列表后台任务,不断监听服务台发送的信息,不断刷新自己的好友列表
//好友上线信息、用户私聊、群聊
private class DaemonTask implements Runnable{
private Scanner in = new Scanner(connect2Service.getIn());
@Override
public void run() {
//收到服务器发来的信息 json字符串:type1 content。。。 to。。。 {“type”,content。。。}
//newLogin:username 如何让判断json字符串,开头是大括号
while (true){
//收到服务器发来的消息
if (in.hasNextLine()){
String strFromServer = in.nextLine();
//此时服务器发来json字符串
if (strFromServer.startsWith("{")){
//json -> Object
MessageVO messageVO = (MessageVO) CommUtils.json2object(strFromServer,MessageVO.class);
if (messageVO.getType().equals("2")){
//服务器发来的私聊信息
String friendName = messageVO.getContent().split("-")[0];
String msg = messageVO.getContent().split("-")[1];
//判断此私聊是否是第一次创建
if (privateChatGUIMap.containsKey(friendName)){
PrivateChatGUI privateChatGUI = privateChatGUIMap.get(friendName);
privateChatGUI.getFrame().setVisible(true);
privateChatGUI.readFromServer(friendName+"说:"+msg);
}
else {
PrivateChatGUI privateChatGUI = new PrivateChatGUI(friendName,
username,connect2Service);
privateChatGUIMap.put(friendName,privateChatGUI);
privateChatGUI.readFromServer(friendName+"说:"+msg);
}
}
else if (messageVO.getType().equals("4")){
//收到服务器发来的群聊信息
//type:4
//content:sender-msg
//to:groupName-[1,2,3...]
String groupName = messageVO.getTo().split("-")[0];
String senderName = messageVO.getContent().split("-")[0];
String groupMsg = messageVO.getContent().split("-")[1];
//若此群名称在此群列表
if (groupList.containsKey(groupName)){
if (groupChatGUIMap.containsKey(groupName)){
//群聊界面弹出
GroupChatGUI groupChatGUI = groupChatGUIMap.get(groupName);
groupChatGUI.getFrame().setVisible(true);
groupChatGUI.readFromServer(senderName+"说:"+groupMsg);
}
else{
Set<String> names = groupList.get(groupName);
GroupChatGUI groupChatGUI = new GroupChatGUI(groupName,
names,username,connect2Service);
groupChatGUIMap.put(groupName,groupChatGUI);
groupChatGUI.readFromServer(senderName+"说:"+groupMsg);
}
}
else{
//若群成员第一次收到群聊信息
//1.将群名称以及群成员保存到当前客户端群聊列表
Set<String> friends = (Set<String>) CommUtils.json2object(messageVO.getTo().
split("-")[1],Set.class);
groupList.put(groupName,friends);//保存到当前列表
loadGroupList();//刷新
//2.弹出群聊界面
GroupChatGUI groupChatGUI = new GroupChatGUI(groupName,
friends,username,connect2Service);
groupChatGUIMap.put(groupName,groupChatGUI);
groupChatGUI.readFromServer(senderName+"说:"+groupMsg); } }
}
else {
//newLogin:username 新上线通知:
if (strFromServer.startsWith("新上线通知:")){
String newFriendName = strFromServer.split(":")[1];
//在前面的用户对话框加上新来的好友名称
users.add(newFriendName);
//弹框提醒用户上线
JOptionPane.showMessageDialog(frame,newFriendName+"上线了!",
"上线提醒",JOptionPane.INFORMATION_MESSAGE);
//刷新好友列表
loadUsers();
}
}
}
}
}
} //私聊标签点击事件
private class PrivateLabelAction implements MouseListener{
//点击的哪个标签,friendname
private String labelName; public PrivateLabelAction(String labelName){
this.labelName = labelName;//通过构造方法传递标签事件
} //鼠标点击执行事件
@Override
public void mouseClicked(MouseEvent e) {
//缓存已经创建好的私聊列表,即对象的复用,而不是重新new
//判断好友列表私聊界面缓存是否已经有指定的标签
if (privateChatGUIMap.containsKey(labelName)) {
PrivateChatGUI privateChatGUI = privateChatGUIMap.get(labelName);
privateChatGUI.getFrame().setVisible(true);
}else {
//否则是第一次点击,创建私聊界面,把需要的参数传递进去
PrivateChatGUI privateChatGUI = new PrivateChatGUI(labelName,username,connect2Service);
privateChatGUIMap.put(labelName,privateChatGUI);//若是第一次经好友标签缓存到界面
} } @Override
public void mousePressed(MouseEvent e) { } @Override
public void mouseReleased(MouseEvent e) { } @Override
public void mouseEntered(MouseEvent e) { } @Override
public void mouseExited(MouseEvent e) { }
} //群聊点击事件
private class GroupLabelAction implements MouseListener{
private String groupName; public GroupLabelAction(String groupName) {
this.groupName = groupName;
} @Override
public void mouseClicked(MouseEvent e) {
//点击的时候判断缓存中有没有,如果有无需再new,直接把窗口弹出
if (groupChatGUIMap.containsKey(groupName)){
GroupChatGUI groupChatGUI = groupChatGUIMap.get(groupName);
groupChatGUI.getFrame().setVisible(true);
}
//否则创建者的
// 第一次点击
else {
//群名集合
Set<String> names = groupList.get(groupName);
GroupChatGUI groupChatGUI = new GroupChatGUI(
groupName,names,username,connect2Service
);
groupChatGUIMap.put(groupName,groupChatGUI);
}
} @Override
public void mousePressed(MouseEvent e) { } @Override
public void mouseReleased(MouseEvent e) { } @Override
public void mouseEntered(MouseEvent e) { } @Override
public void mouseExited(MouseEvent e) { }
} //从登录页面跳到列表页面需要传参
public FriendsList(String username,Set<String>users,
Connect2Service connect2Service){
this.username = username;
this.users = users;
this.connect2Service = connect2Service; frame = new JFrame(username);
frame.setContentPane(friendsPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400,300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
loadUsers();
//启动后台线程不断监听服务器发来的消息
Thread daemonThread = new Thread(new DaemonTask());
daemonThread.setDaemon(true);
daemonThread.start();
//创建群组
creategroupButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
new CreateGroupGUI(username,users,connect2Service,FriendsList.this);
}
});
} //加载所有在线用户信息
public void loadUsers(){
JLabel[] userLabels = new JLabel[users.size()];
JPanel friends = new JPanel();
friends.setLayout(new BoxLayout(friends,BoxLayout.Y_AXIS));
//迭代users内容展示
Iterator<String> iterator = users.iterator();
int i = 0;
while (iterator.hasNext()){
String userName = iterator.next();
userLabels[i] = new JLabel(userName);
//添加鼠标点击好友标签事件 即添加标签点击事件
userLabels[i].addMouseListener(new PrivateLabelAction(userName));
friends.add(userLabels[i]);
i++;
}
//重新加载布局,重新实例化
friendsList.setViewportView(friends);
//设置滚动条垂直滚动
friendsList.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
friends.revalidate();
friendsList.revalidate(); }
//刷新群组界面
public void loadGroupList(){
//存储所有群名称标签JPanel
JPanel groupNamePanel = new JPanel();
groupNamePanel.setLayout(new BoxLayout(groupNamePanel,BoxLayout.Y_AXIS));
JLabel[] labels = new JLabel[groupList.size()];
//Map遍历
Set<Map.Entry<String,Set<String>>> entries = groupList.entrySet();
Iterator<Map.Entry<String,Set<String>>> iterator = entries.iterator();
int i = 0;
while (iterator.hasNext()){
Map.Entry<String,Set<String>> entry = iterator.next();
labels[i] = new JLabel(entry.getKey());
labels[i].addMouseListener(new GroupLabelAction(entry.getKey()));
groupNamePanel.add(labels[i]);
i++;
}
groupPanel.setViewportView(groupNamePanel);
groupPanel.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
groupPanel.revalidate();//刷新 }
public void addGroup(String groupName,Set<String> friends){
groupList.put(groupName,friends);
}
}

 私聊:

package com.hhy.java.client.entity.service;

import com.hhy.java.util.CommUtils;
import com.hhy.java.vo.MessageVO; import javax.swing.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException; public class PrivateChatGUI {
private JPanel PrivateChatPanel;
private JTextField send2Server;
private JTextArea readFromServer; private String friendName;
private String myName;
private Connect2Service connect2Service;
private JFrame frame;
private PrintStream out; public PrivateChatGUI(String friendName,String myName,Connect2Service connect2Service) {
this.friendName = friendName;
this.myName = myName;
this.connect2Service = connect2Service; try {
this.out = new PrintStream(connect2Service.getOut(), true,
"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} frame = new JFrame("与" + friendName + "私聊中...");
frame.setContentPane(PrivateChatPanel); //窗口关闭操作 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//此处应该设置为隐藏窗口
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.setSize(400, 400);
frame.setVisible(true); //捕捉输入框的键盘输入
send2Server.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
StringBuilder sb = new StringBuilder();
sb.append(send2Server.getText());
//1、当捕捉到按下Enter
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
//2、将当前信息发给服务端
String msg = sb.toString();
MessageVO messageVO = new MessageVO();
messageVO.setType("2");
messageVO.setContent(myName+"-"+msg);
messageVO.setTo(friendName);
PrivateChatGUI.this.out.println(CommUtils.object2Json(messageVO));
//3、将自己发送的信息展示到当前的私聊页面
readFromServer(myName+"说:"+msg);
send2Server.setText("");//输入框还原,为了输入
}
}
});
}
//展示聊天界面
public void readFromServer(String msg) {
readFromServer.append(msg+"\n");
} public JFrame getFrame() {
return frame;
}
}

创建群聊:创建群聊给服务器传参 :自己 好友列表 连接(每次向服务器提交必备)

1、判断哪些好友被选择
2、获取输入群名称
3、向服务器打包发送群名称、好友列表(后续加上了包括自己)
4、将当前创群界面隐藏掉,刷新客户端群列表

package com.hhy.java.client.entity.service;

import com.hhy.java.util.CommUtils;
import com.hhy.java.vo.MessageVO;
import com.sun.crypto.provider.JceKeyStore; import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 22:07 on 2019/8/19
* @Package_Name: com.hhy.java.client.entity.service
*/
public class CreateGroupGUI {
private JPanel CreateGroupGUI;
private JPanel friendLabelPanel;
private JLabel groupName;
private JTextField groupNameText;
private JButton submitButton; private String myName;
private Set<String> friends;
private Connect2Service connect2Service;
private FriendsList friendsList; public CreateGroupGUI(String myName,Set<String> friends,Connect2Service connect2Service,
FriendsList friendsList) {
this.myName = myName;
this.friends = friends;
this.connect2Service = connect2Service;
this.friendsList = friendsList;
JFrame frame = new JFrame("创建群组");
frame.setContentPane(CreateGroupGUI);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(400,300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
//将在线好友以checkBox展示到界面中 动态遍历
friendLabelPanel.setLayout(new BoxLayout(friendLabelPanel,BoxLayout.Y_AXIS));
JCheckBox[] checkBoxes = new JCheckBox[friends.size()];
Iterator<String> iterator = friends.iterator();
int i = 0;
while (iterator.hasNext()){
String labelName = iterator.next();
checkBoxes[i] = new JCheckBox(labelName);
friendLabelPanel.add(checkBoxes[i]);
i++;
}
friendLabelPanel.revalidate();//刷新 //点击提交按钮提交信息到服务端
submitButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//1、先判断哪些checkBox被选中(那些好友被选中加入群聊)
//CheckBox的顶层父类
Set<String> selectedFriends = new HashSet<>();
Component[] comps = friendLabelPanel.getComponents();
//遍历所有组件,看哪些Component被选中
for (Component comp : comps){//包括很多:box,field,button,label
JCheckBox checkBox = (JCheckBox)comp;//用于强转,由于也不知道哪些工具
if (checkBox.isSelected()){
String labelName = checkBox.getText();
selectedFriends.add(labelName);
}
}
selectedFriends.add(myName); //2、获取群名输入框的群名称
String groupName = groupNameText.getText(); //3、将群名+选中好友信息发送到服务端
//type:3
//content:群名 groupName
//to:[user1,user2,user3..]
MessageVO messageVO = new MessageVO();
messageVO.setType("3");
messageVO.setContent(groupName);
messageVO.setTo(CommUtils.object2Json(selectedFriends));
try {
PrintStream out =
new PrintStream(connect2Service.getOut(),true,"UTF-8");
out.println(CommUtils.object2Json(messageVO));
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
//4.将当前创建群界面隐藏掉,刷新好友列表界面的群列表
frame.setVisible(false);
//addGroupInfo
//loadGroup
friendsList.addGroup(groupName,selectedFriends);
friendsList.loadGroupList();
}
});
} }

群聊:

群聊中:
客户端
1.选择在线好友创建群聊,将创建群聊信息发送给服务端
2.点击群聊列表的标签,唤醒群聊界面
3.唤醒群聊界面后,进行群聊信息的发送

第一次收到群消息的客户端首先将群名保存到当前客户端的群列表中,
然后弹出群聊界面,读取别人发来的消息

服务端
1.接收用户的群注册信息,将群名与群成员注册到服务端缓存
2.接收用户发来的群消息,根据群名称转发信息到相应的群中

发送消息,输入框不断监听键盘输入事件,当回车后,发送给服务端

服务端先遍历根据群名获取全部好友,然后爱各项每个好友out信息

package com.hhy.java.client.entity.service;

import com.hhy.java.util.CommUtils;
import com.hhy.java.vo.MessageVO; import javax.swing.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.Set;
import java.util.SplittableRandom; /**
* @Information:
* @Author: HeHaoYuan
* @Date: Created at 13:01 on 2019/8/20
* @Package_Name: com.hhy.java.client.entity.service
*/
public class GroupChatGUI {
private JPanel GroupChatPanel;
private JTextArea readFromServer;
private JTextField send2Server;
private JPanel friendsPanel;
private JFrame frame; private String groupName;
private Set<String> friends;
private String myName;
private Connect2Service connect2Service; public GroupChatGUI(String groupName,Set<String> friends,String myName,Connect2Service connect2Service){
this.groupName = groupName;
this.friends = friends;
this.myName = myName;
this.connect2Service = connect2Service;
frame = new JFrame(groupName);
frame.setContentPane(GroupChatPanel);
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setSize(400,400);
frame.setVisible(true);
//TODO 加载群中的好友列表
//竖向展示
friendsPanel.setLayout(new BoxLayout(friendsPanel,BoxLayout.Y_AXIS));
Iterator<String> iterator = friends.iterator();
while (iterator.hasNext()){
String labelName = iterator.next();
JLabel jLabel = new JLabel(labelName);
friendsPanel.add(jLabel);
} send2Server.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
StringBuilder sb = new StringBuilder();
sb.append(send2Server.getText());
//捕捉回车事件
if (e.getKeyCode() == KeyEvent.VK_ENTER){
String str2Server = sb.toString();
//给服务器发哪些东西
//type:4
//content:myName-msg
//to:groupName MessageVO messageVO = new MessageVO();
messageVO.setType("4");
messageVO.setContent(myName+"-"+str2Server);
messageVO.setTo(groupName);
try {
PrintStream out = new PrintStream(connect2Service.getOut(),true,
"UTF-8");
out.println(CommUtils.object2Json(messageVO));
System.out.println("客户端发送的群聊信息为:"+messageVO.getContent());
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
}
}
});
}
public void readFromServer (String msg){
readFromServer.append(msg+"\n");
} public JFrame getFrame() {
return frame;
}
}

思考问题?

如何在外部类的匿名内部类里获取匿名内部类对象?  外部类.this

mini QQ(项目一)的更多相关文章

  1. 团队项目--关于QQ项目的运行和总结

    目前为止该QQ项目实现如下功能:添加好友到好友列表,可以把好友在不同分类中移动,同时支持离线查找添加好友,离线更换头像,离线发送消息,保存所有好友聊天记录,发送窗口抖动,查看对方信息,更改/添加信息等 ...

  2. QQ项目

    QQ第一部分: 1.数据库 每一个QQ账户必须有  a. state:是否上线的状态  b. IP:正在上线的主机的IP  c. port:UDP端口号(用这个和别的好友通讯)  注:TCP连接时,在 ...

  3. QQ项目(续)

    1.项目查找好友的原理 sql:select * from qquser where account in(select friendAccount from friend where userAcc ...

  4. 如何从“点子”落地到“执行”?—完整解析1个手游传播类mini项目的进化

    本文来自网易云社区 作者:林玮园 从点子到落地,是不确定到确定的过程,是从模糊概念到具体现实的实现过程.无论什么点子,在落地变现的过程中都会有很多疑问产生. 首先,不确定点子本身是否成立.点子的背后是 ...

  5. MVC项目中如何判断用户是在用什么设备进行访问

    使用UAParser在C#MVC项目中如何判断用户是在用什么设备进行访问(手机,平板还是普通的电脑) 现在我们开发的很多web应用都要支持手机等移动设备.为了让手机用户能有更加好的用户体验,我们经常为 ...

  6. 使用UAParser在C#MVC项目中如何判断用户是在用什么设备进行访问(手机,平板还是普通的电脑)

    现在我们开发的很多web应用都要支持手机等移动设备.为了让手机用户能有更加好的用户体验,我们经常为手机设备专门准备一套前端的页面.这样当用户使用普通电脑来访问的时候,我们的应用就向用户展示普通电脑的页 ...

  7. 团队项目NABCD模型的需求分析

    团队项目NABCD模型的需求分析 NABCD模型的介绍 Need(需求)-现在市场上未被满足但又急需满足的客户需求是什么?Approach(方法)-要满足这种需求,我能够提出什么独特的方法吗?Bene ...

  8. Swift实战-小QQ(第1章):QQ登录界面

    1.新建小QQ项目 2.将所需用到的图片资源(resource)文件夹,添加到项目中.并新建一个登录页面:LoginViewController.swift 3.修改LoginViewControll ...

  9. Linux如何用QQ?Linux下QQ使用的几种方案

    在linux下如何使用QQ?在ubuntu11.10中如何使用QQ?或许有初涉linux的人这样问,我们可以看看ubuntusoft总结出来的几种在linux系统下用QQ的方法.前面的几种主要的方法都 ...

随机推荐

  1. Hive的数据倾斜

    目录 什么是数据倾斜 Hadoop框架的特性 主要表现 容易数据倾斜的情况 产生数据清洗的原因 业务场景 空值产生的数据倾斜 不同数据类型关联产生数据倾斜 大小表关联查询产生数据倾斜 一.什么是数据倾 ...

  2. Mysql之架构篇

    1.主从复制解决方案 这是MySQL自身提供的一种高可用解决方案,数据同步方法采用的是MySQL replication技术.MySQL replication就是从服务器到主服务器拉取二进制日志文件 ...

  3. Codeforces F. Maxim and Array(构造贪心)

    题目描述: Maxim and Array time limit per test 2 seconds memory limit per test 256 megabytes input standa ...

  4. SpringBoot注解分析解释

    使用注解的优势: 1.采用纯java代码,不在需要配置繁杂的xml文件 2.在配置中也可享受面向对象带来的好处 3.类型安全对重构可以提供良好的支持 4.减少复杂配置文件的同时亦能享受到springI ...

  5. python27期day19:面向对象

    1.class GirlFriend(object): #定义女朋友类: eyes = 2 #类属性(静态属性),是属于当前类的 #当前类的所有对象,所共有的特征 sex = "女" ...

  6. 【使用篇二】SpringBoot整合Servlet(1)

    两种方式: 通过注解扫描完成 Servlet组件的注册 通过方法完成 Servlet组件的注册 一.通过注解扫描完成 Servlet 组件的注册 1. 编写Servlet类 /** * SpringB ...

  7. VIJOS-P1625 精卫填海

    JDOJ 1587 VIJOS-P1625 精卫填海 https://neooj.com/oldoj/problem.php?id=1587 洛谷 P1510 精卫填海 https://www.luo ...

  8. java数组的N种打印方式

    int[] array = {1,2,3,4,5}; (1)传统的for循环方式 ;i<array.length;i++) { System.out.println(a[i]); } (2)fo ...

  9. [LeetCode] 670. Maximum Swap 最大置换

    Given a non-negative integer, you could swap two digits at most once to get the maximum valued numbe ...

  10. uniApp上传图片

    项目中用到了上传图片的功能,记录一下.增强记忆. 要上传图片首先就要先选择图片,或者是先拍照,此时先调用的是 chooseImage 接口,此接口可选择拍照也可以从相册中选择. 它有几个参数,具体可以 ...