1. 使用JavaFX的Java项目中,继承Application后,必须要有start方法,此时无需main函数也能运行start方法中的内容。
并且会显示stage.show(),但是如果写了main方法,那么如果不写launch将不会运行start(),在如下模板的情况下。
2. 用集合存储id和socket表示在线状态状态,
这是集合的方法类
package server;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
public class ClientSocketCache {
private ConcurrentHashMap<String, Socket> clientSocketMap = new ConcurrentHashMap<>();
// 添加客户端Socket到缓存
public void addClientSocket(String clientId, Socket socket) {
clientSocketMap.put(clientId, socket);
}
// 从缓存中移除客户端Socket
public void removeClientSocket(String clientId) {
clientSocketMap.remove(clientId);
}
// 获取客户端Socket
public Socket getClientSocket(String clientId) {
return clientSocketMap.get(clientId);
}
// 检查客户端是否在线
public boolean isClientOnline(String clientId) {
return clientSocketMap.containsKey(clientId);
}
public void Ergodic() {
for (String clientId : clientSocketMap.keySet()) {
Socket socket = clientSocketMap.get(clientId);
System.out.println("Client ID: " + clientId + ", Socket: " + socket);
}
}
}
在主函数中创建,并开辟线程用于遍历,检测是否满足要求
package server;
import QQEmail.VerificationCodeManager;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Server {
public static List<Socket> socketList = Collections.synchronizedList(new ArrayList<>());
public static VerificationCodeManager manager = new VerificationCodeManager(); // 缓存暂存验证码(静态)
//存储在线状态相关的两条代码
public static ClientSocketCache clientSocketCache = new ClientSocketCache();
public static void main(String[] args) throws IOException, ClassNotFoundException {
new Thread(new TestThread()).start();//测试是否能正确存储登录信息
ServerSocket ss = new ServerSocket(6666);
while (true) {
// 此行代码会阻塞,将一直等待别人的连接
Socket s = ss.accept();
socketList.add(s);
// 每当客户端连接成功后启动一个ServerThread线程为该客户端服务
System.out.println("客户端连接" + s);
new Thread(new ServerThread(s)).start();
}
}
}
用于遍历、监听的线程类
package server;
import java.util.concurrent.TimeUnit;
public class TestThread implements Runnable {
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("调用了一次");//(Thread.currentThread().getName() + " is running");
Server.clientSocketCache.Ergodic();
}
}
}
在长线程开始时调用
Server.clientSocketCache.addClientSocket(String.valueOf(clientId), socket);
每个可能结束长线程的地方调用
Server.clientSocketCache.removeClientSocket(String.valueOf(clientId));
3. 错误原因记录,当方法调用的变量命名与类的属性相同时(举例都命名为为attribute),默认为方法调用的attribute,而this.attribute才是类中的属性。当某个属性该有的 “.方法” 消失不见时,很可能是出现了重名,可更改命名或加this.前缀
4. 当IDEA处于默认主题时,蓝色字体代表类的内部属性,表示一种调用的状态,而方法内定义的、已及传来的数据显示白色,代表局部的状态
5. 实时更新Server具体操作
1. 首先要将 id 和 oos 存集合,id 表示在线,存oos而不存socket是因为socket只能包装一次,多次包装的ObjectOutputStream和ObjectInputStream反序列化时会报错
这是一个集合类的模板
package server;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
public class ClientSocketCache {
private ConcurrentHashMap<String, ObjectOutputStream> clientOosMap = new ConcurrentHashMap<>();
// 检查客户端是否在线
public boolean isClientOnline(String clientId) {
return clientOosMap.containsKey(clientId);
}
// 添加客户端ObjectOutputStream到缓存
public void addClientOos(String clientId, ObjectOutputStream oos) {
clientOosMap.put(clientId, oos);
}
// 从缓存中移除客户端ObjectOutputStream
public void removeClientOos(String clientId) {
clientOosMap.remove(clientId);
}
// 获取客户端ObjectOutputStream
public ObjectOutputStream getClientOos(String clientId) {
return clientOosMap.get(clientId);
}
// 遍历所有客户端信息
public void ergodic() {
for (String clientId : clientSocketMap.keySet()) {
ObjectOutputStream oos = clientOosMap.get(clientId);
System.out.println("Client ID: " + clientId + ", "ObjectOutputStream: " + oos);
}
}
其中写在前面的定义相当于一个索引,后面则是关联存储的内容
2. 在Server定义一个静态的集合类,在进入长连接时将 id 和 oos 存集合(不重要)
public static ClientSocketCache clientSocketCache = new ClientSocketCache();
Server.clientSocketCache.addClientOos(String.valueOf(clientId), oos);
3. 这是一个实时更新思路的案例
首先我在服务端需要进行实时更新的地方(type)插入一串代码,这串代码的作用就是读取一些信息,明白服务端将要用这些信息干什么,然后进行一些JDBC之类的操作,额外打包一个Message发给相关的客户端,提醒客户端进行更新,接下来是一个示例(更新学生班级信息)。
1. 发送消息过来的长连接逻辑不变,依旧是处理完后发送给客户端进行UI更新,中间插入的StudentCoursesLoading.RealTime(message),即将信息带到类中进行处理。
if (message.getType() == ThreadLibrary.ModifyClassInformation) {
message = ModifyClassInformation.Course(message);
StudentCoursesLoading.RealTime(message);//这里是插入的代码
oos.writeObject(message);
}
2. 取走进行了更新的信息,通过“外键”找出所有相关的 id (受影响的用户)
AddCourseMessage addCourseMessage = (AddCourseMessage) message.getObject();
// 1. 加载JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 建立连接
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 3. SQL查询,从classindex表中找到所有与course_id匹配的id
String sqlSelectIds = "SELECT id FROM classindex WHERE course_id = ?";
PreparedStatement pstmt = conn.prepareStatement(sqlSelectIds);
pstmt.setInt(1, addCourseMessage.course_id);
ResultSet rs = pstmt.executeQuery();
3. 找到 相关的 id 后。每次找到一个 id 都检测是否在线(顺手而已,如果oos不像ois一样会阻塞的话可能没什么关系),之后根据需要打包信息,根据存储的oos不同丢给相应的客户端,客户端接收到信息自动完成实时更新。
// 4. 遍历结果集
while (rs.next()) {
int id = rs.getInt("id");
if (Server.clientSocketCache.isClientOnline(String.valueOf(id))){
Message realTimeMessage = CoursesLoading.Student(new Message(new CourseStudentMessage(id), ThreadLibrary.CoursesLoadingStudent));
ObjectOutputStream oos = Server.clientSocketCache.getClientOos(String.valueOf(id));
oos.writeObject(realTimeMessage);
}
}
6. 该实时更新要做的一些优化处理:
1. 因为集合中的 clientId 是索引,要具有唯一性,因此要限制登录不能登多个 id 相同的账号,在登陆前要做id是否已经在集合中的操作。
2. 要统一更新模板,最好都是像案例一样,实时更新只需要做简单的外键操作,找到数据后丢给之前的.....Message 和DataBaswClass处理,然后打包送到相应的客户端。
3. 在长线程周围搭建集合用于存储id和相应的oos时,需要注意在客户端断连时清除,主要在抛异常和主动退出while的部分清除。
4. 可设置一个线程类,设置20秒左右的休眠,观察客户端的各种操作后,数据能不能正确的从服务端清除。
7. 如果用 int 作为主键,那么必须用Integer
在集合框架(如 ConcurrentHashMap
)中,你不能直接使用原始数据类型作为键或值,因为集合框架的泛型要求对象必须是 Object
类型或者是 Object
的子类。
8. 判断是否需要实时更新课程的两种思路
*(模型基础)学生端每次点击一个课程都要与服务器进行交互,然后再渲染出课程页面。(注:最后的结果都是渲染StaticFXML.CoursePage为新的状态)
1. 在客户端设置静态变量(如Local_Data.course_id)记录登录的页面,有页面则记录页面的course_id,没有页面则记录course_id = 0,是否实时更新只需判断(IO两边的)course_id是否相等。
2. 在服务端再设置一个集合,存储 每次 相应 id 渲染交互的 course_id,通过判别用户上次传过来的course_id来判断是否要对它发送更新数据(担忧:可能数据并行,刚好没接到)
9. 根据测试,集合将会只存一个clientId,在有两个clientId相同的情况下,而它的机制是顶替,不是无法插入,也就是替换掉原有的clientId的数据。
所以只能登录一个账号有两种思路,无法登录或者顶号。顶号可能需要开辟一个新线程监听,以及缓存socket等,还有可能因为监听的速度太慢,导致被顶号前做了一些不好的操作。所以选择在每次长连接前(登录前)查找是否有了相应的clientId,有则无法登录,显示账号已登录。(可以优化忘记密码试探有没有clientId已经登录,有则让他退出)