从零开始:后端工程师的前端开发指南

前端技术分类

1. 页面渲染技术

  • HTML/HTML5: 语义化标签、Web组件、Canvas/SVG
  • CSS/CSS3: 选择器、盒模型、Flexbox/Grid布局、动画与过渡
  • 预处理器/后处理器: Sass/Less/Stylus、PostCSS
  • CSS架构: BEM、OOCSS、原子CSS (Tailwind)

2. 编程语言与核心技术

  • JavaScript: 语法基础、原型链、闭包、ES6+特性
  • TypeScript: 类型系统、接口、泛型、类型推断
  • WebAssembly: 高性能编译目标、C/C++/Rust集成

3. 前端框架与视图层

  • 组件化框架: React、Vue、Angular、Svelte
  • 状态管理: Redux、Vuex、MobX、Recoil、Pinia
  • SSR/SSG技术: Next.js、Nuxt.js、Gatsby、Astro
  • Islands架构: Astro、Qwik

4. 工程化与构建系统

  • 模块化规范: CommonJS、AMD、ESM
  • 构建工具: Webpack、Vite、Rollup、esbuild
  • 转译器: Babel、SWC
  • 包管理器: npm、yarn、pnpm
  • Monorepo工具: Lerna、Turborepo、pnpm workspace

5. 网络与数据交互

  • HTTP客户端: XMLHttpRequest、Fetch API、Axios
  • API范式: REST、GraphQL、tRPC
  • 实时通信: WebSocket、Server-Sent Events
  • 离线存储: LocalStorage、IndexedDB、Service Worker缓存

6. 测试与质量保障

  • 测试框架: Jest、Vitest、Mocha
  • UI测试: React Testing Library、Vue Testing Library
  • E2E测试: Cypress、Playwright
  • 代码质量: ESLint、Prettier、TypeScript

7. 性能优化与用户体验

  • 性能指标: Core Web Vitals、Lighthouse
  • 渲染优化: 虚拟DOM、懒加载、代码分割
  • 缓存策略: HTTP缓存、Service Worker
  • PWA技术: Manifest、Service Worker、推送通知

8. 跨平台与多端开发

  • 移动跨平台: React Native、Flutter
  • 桌面应用: Electron、Tauri
  • 小程序开发: 微信小程序、uni-app

后端工程师的前端学习路线图

第一阶段:基础知识

1. HTML & CSS基础

  • 目标:掌握页面结构和样式基础
  • 学习内容
    • HTML语义化标签
    • CSS选择器、盒模型
    • Flexbox和Grid布局
    • 响应式设计基础
  • 后端优势:已有的HTML基础知识可快速上手
  • 练习项目:创建个人简历页面

2. JavaScript核心

  • 目标:理解JavaScript语言特性
  • 学习内容
    • ES6+语法(箭头函数、解构、Promise等)
    • 异步编程模型(回调、Promise、async/await)
    • DOM操作和事件处理
  • 后端优势:利用已有的编程思维,专注于语言差异
  • 练习项目:简单的表单验证和数据处理

第二阶段:工程化与开发环境

  • 目标:掌握前端工程化基础
  • 学习内容
    • npm/yarn包管理
    • 模块化开发(ES Modules)
    • 构建工具入门(Vite优先)
    • Git工作流
  • 后端优势:工程化概念相通,重点在于工具差异
  • 练习项目:搭建一个模块化的前端项目骨架

第三阶段:框架入门

  • 目标:掌握一个主流前端框架
  • 框架选择
    • React:适合喜欢函数式编程的后端开发者
    • Vue:适合希望快速上手的开发者
  • 学习内容
    • 组件化开发模式
    • 状态管理基础
    • 生命周期和钩子函数
    • 路由管理
  • 后端优势:组件化思想类似于后端模块化
  • 练习项目:待办事项应用

第四阶段:TypeScript与API交互

  • 目标:增强类型安全和后端交互能力
  • 学习内容
    • TypeScript基础类型和接口
    • API调用(Fetch/Axios)
    • 数据处理和状态管理进阶
  • 后端优势:类型系统与后端语言相似,API设计经验可复用
  • 练习项目:与真实API交互的数据展示应用

第五阶段:前端测试与部署

  • 目标:掌握前端质量保障和部署流程
  • 学习内容
    • 单元测试和组件测试
    • CI/CD流程
    • 静态网站部署
  • 后端优势:测试概念相通,CI/CD经验可复用
  • 练习项目:为已有项目添加测试和自动部署

第六阶段:进阶专题(按需学习)

  • 状态管理进阶:Redux/Vuex等
  • SSR/SSG技术:Next.js/Nuxt.js
  • 性能优化:代码分割、懒加载、缓存策略
  • CSS进阶:预处理器、CSS-in-JS、Tailwind CSS

BIO/NIO/AIO的异同

在计算机系统中,I/O(输入/输出)操作是程序与外部世界(如文件、网络等)交互的重要手段。现代操作系统提供了三种不同的I/O模型:BIO(阻塞I/O)、NIO(非阻塞I/O)和AIO(异步I/O),它们在设计思想、工作机制和适用场景上有着显著差异。本文将深入剖析这三种I/O模型的核心概念、实现原理以及各自的优缺点,以帮助读者理解不同I/O模型在系统设计中的应用。

BIO (Blocking I/O,阻塞I/O)

概念与原理

BIO是传统的I/O编程模型,其特点是简单直观但效率较低。在BIO模型中,当一个线程发起I/O操作后,该线程会被阻塞直到I/O操作完成。这种模型对应于POSIX标准中的最基本的I/O操作方式。

工作机制

BIO的工作机制基于线程阻塞原理。当应用程序调用read()等系统调用时,线程会从用户态切换到内核态。如果请求的数据尚未准备好,内核会将线程置为睡眠状态并移出运行队列,释放CPU资源给其他线程使用。

当数据准备就绪(如网络数据包到达)时,设备会触发中断,通知CPU数据已到达。内核将数据放入缓冲区,并将等待的线程重新唤醒,加入运行队列。线程获得CPU时间片后,将数据从内核缓冲区复制到用户空间,然后返回继续执行。

一次完整的BIO操作通常涉及两次用户态与内核态的切换:

  1. 第一次切换:应用程序调用read()等系统调用时,从用户态切换到内核态
  2. 第二次切换:系统调用完成后,从内核态切换回用户态

如果数据未就绪导致阻塞,在线程被唤醒后继续执行系统调用时,实际上不会产生额外的用户态与内核态切换,因为线程仍然在内核态执行。但这个过程会涉及上下文切换:当线程阻塞时让出CPU,当数据就绪后重新获得CPU执行权。这种上下文切换虽然不是用户态与内核态的切换,但同样会带来性能开销。

在服务器应用中,BIO通常采用”一个连接一个线程”的模式。每当接收到新的客户端连接,服务器就会创建或分配一个线程专门处理该连接的所有I/O操作。这导致高并发场景下线程数量剧增,系统资源消耗严重。

底层实现

BIO底层依赖操作系统的阻塞式系统调用。以网络读取为例,实现过程如下:

  1. 应用程序通过系统调用请求读取数据,控制权转移给内核
  2. 内核检查是否有数据可读,若无,将线程加入等待队列并挂起
  3. 网络数据到达时,网卡产生中断,内核处理数据并放入socket缓冲区
  4. 内核唤醒等待的线程,将其标记为可运行状态
  5. 线程恢复执行,将数据从内核缓冲区复制到用户空间
  6. 系统调用返回,线程继续在用户态执行

这种实现方式在并发连接较少时表现良好,但随着连接数增加,线程资源消耗(内存、上下文切换开销等)会迅速增长,导致系统性能下降。这就是所谓的”C10K问题”——传统BIO模型难以处理万级并发连接。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// BIO服务器示例
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// 阻塞直到有客户端连接
Socket clientSocket = serverSocket.accept();
// 为每个客户端创建一个新线程
new Thread(() -> {
try (InputStream in = clientSocket.getInputStream()) {
// 读取数据(阻塞操作)
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}

优缺点

优点

  • 设计简单,编程模型直观
  • 代码结构清晰,易于理解
  • 适合连接数较少且固定的应用

缺点

  • 性能低下,线程阻塞导致资源浪费
  • 可伸缩性差,线程数量增加会导致系统负担加重
  • 高并发场景下,频繁的线程创建和销毁会带来大量开销

NIO (Non-blocking I/O,非阻塞I/O)

概念与原理

NIO是一种非阻塞I/O模型,它允许线程在等待I/O操作完成的同时执行其他任务。NIO通过选择器(Selector)实现多路复用,使一个线程能够管理多个通道(Channel)的I/O操作,从而提高资源利用率。

工作机制

NIO的工作机制基于I/O多路复用和事件驱动。应用程序首先将通道注册到选择器上,指定关注的事件类型(如读、写、连接等)。然后通过选择器查询哪些通道已经准备好执行I/O操作。

在实际运行时,应用线程调用selector.select()方法等待事件发生。此方法可能会短暂阻塞,直到至少有一个通道准备就绪,或者超时。与BIO不同,这种阻塞是主动控制的,且一次阻塞可以等待多个I/O事件。

当有通道就绪时,select()方法返回,应用程序获取就绪通道的集合,然后遍历处理这些通道上的I/O事件。处理完成后,线程再次调用select()等待下一批事件。这种模式下,一个线程能够处理多个连接,大大提高了系统的并发处理能力。

用户态和内核态的切换方面,NIO在执行select()操作时会从用户态切换到内核态,然后返回时再切换回用户态。与BIO不同的是,一次select()调用可以处理多个通道的事件,减少了切换次数。当通道就绪后,读写操作也各需要一次用户态/内核态切换,但由于只处理已就绪的通道,不会发生阻塞等待。

底层实现

NIO底层依赖操作系统的I/O多路复用机制,如Linux的epoll、BSD的kqueue和Windows的IOCP等。以epoll为例,其实现过程如下:

  1. 应用程序创建epoll实例,相当于Java中的Selector对象
  2. 将需要监听的文件描述符(套接字)注册到epoll实例,指定关注的事件类型
  3. 调用epoll_wait()等待事件发生,这一步可能会短暂阻塞
  4. 当有事件发生时,内核将就绪的文件描述符和对应事件放入就绪列表
  5. epoll_wait()返回就绪事件的数量,应用程序遍历就绪列表处理I/O事件
  6. 处理完成后,再次调用epoll_wait()等待新的事件

这种机制避免了BIO中”一个连接一个线程”的资源消耗问题,实现了用较少的线程处理大量连接的目标。尤其是在连接数量多但活跃度不高的场景(如聊天服务器),NIO的优势更为明显。

需要注意的是,虽然NIO避免了不必要的阻塞,但应用程序需要自行处理数据的读取和缓冲,增加了编程复杂度。同时,由于采用轮询机制,若没有I/O事件发生,select()调用也会消耗CPU资源。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// NIO服务器示例
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);

Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
// 非阻塞或阻塞较短时间
selector.select();

Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();

if (key.isAcceptable()) {
// 处理新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
}
}
}
}

优缺点

优点

  • 单线程可以处理多个连接,提高资源利用率
  • 减少线程上下文切换的开销,提升系统性能
  • 适合高并发、大量连接但流量较小的场景

缺点

  • 编程复杂度高,学习曲线陡峭
  • 需要开发者自行处理数据的读写边界
  • 可能导致CPU负载较高,因为需要不断轮询就绪的通道

AIO (Asynchronous I/O,异步I/O)

概念与原理

AIO代表异步I/O,是最接近理想I/O模型的一种实现。在AIO模式下,应用程序发起I/O操作后立即返回,继续执行其他任务,当I/O操作完全完成(包括数据准备和数据从内核复制到用户空间)后,系统会通知应用程序。这是真正的非阻塞异步I/O模型。

工作机制

AIO的工作机制完全基于事件和回调。应用程序发起I/O请求时,会同时注册一个回调函数。之后,无论I/O是否完成,应用程序都可以执行其他任务,不需要等待或轮询。

当I/O操作完全完成后,操作系统会触发注册的回调函数,通知应用程序处理结果。这整个过程不需要应用程序主动查询I/O状态,实现了I/O操作与程序执行的完全分离。

从用户态和内核态切换的角度看,AIO在初始提交I/O请求时会有一次从用户态到内核态的切换。之后应用程序可以继续在用户态执行其他代码。当I/O操作完成时,内核会通过回调机制通知应用程序,此时会再发生一次从内核态到用户态的切换。与BIO和NIO相比,AIO的一个重要优势是完成数据从内核缓冲区到用户缓冲区的复制不需要应用程序参与。

底层实现

AIO的底层实现依赖于操作系统提供的异步I/O支持。在Windows系统上,AIO通过IOCP(I/O完成端口)实现;在Linux系统上,早期通过AIO系统调用实现,但功能有限,近期的高版本内核则提供了更强大的io_uring机制。

以Windows的IOCP为例,其实现过程如下:

  1. 应用程序创建I/O完成端口,并将套接字与之关联
  2. 应用程序发起异步I/O请求,同时提供完成回调函数或例程
  3. 应用程序继续执行其他任务,不需要等待I/O完成
  4. 当I/O操作完成时,系统将完成事件放入完成端口队列
  5. 应用程序中的工作线程从完成端口获取事件并处理
  6. 处理完成后,工作线程可以继续获取下一个完成事件

这种机制完全消除了应用程序对I/O状态的轮询,实现了更高效的资源利用。特别是在大文件传输、数据库访问等I/O密集型应用中,AIO能够显著提高系统吞吐量。

需要注意的是,由于AIO依赖操作系统的支持,在不同平台上的实现质量和性能可能有所差异。特别是在Linux系统上,传统的AIO支持较弱,只有近期的内核版本才提供了更完善的异步I/O支持。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// AIO服务器示例
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));

serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// 继续接受下一个连接
serverChannel.accept(null, this);

// 处理当前连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer buffer) {
if (bytesRead > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
// 可以继续读取或执行其他操作
}
}

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}

@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});

优缺点

优点

  • 真正的异步操作,I/O效率最高
  • 应用程序无需轮询,资源消耗更低
  • 适合I/O密集型应用,特别是读写操作耗时较长的场景

缺点

  • 编程模型更复杂,回调函数可能导致代码可读性下降
  • 依赖操作系统的异步I/O支持,在某些平台上实现不够完善
  • 调试和错误处理较为困难

三种I/O模型的对比

阻塞特性对比

  • BIO:完全阻塞,一个线程处理一个连接
  • NIO:非阻塞,但需要应用程序主动轮询I/O状态
  • AIO:完全非阻塞,异步通知I/O完成事件

编程复杂度

  • BIO:最简单,适合初学者
  • NIO:较复杂,需要理解Buffer、Channel、Selector等概念
  • AIO:最复杂,需要掌握回调和异步编程模式

性能对比

  • BIO:性能最低,不适合高并发场景
  • NIO:性能中等,适合连接数多但处理时间短的场景
  • AIO:理论上性能最高,适合I/O密集型应用

适用场景

  • BIO:连接数较少、业务处理简单的应用
  • NIO:高并发、需要管理大量连接的服务器应用
  • AIO:文件操作、数据库访问等I/O密集型应用

操作系统层面的I/O模型

从操作系统角度看,可以将I/O模型分为五类:

  1. 阻塞I/O(Blocking I/O):进程发起I/O系统调用后阻塞,直到I/O操作完成
  2. 非阻塞I/O(Non-blocking I/O):进程发起I/O系统调用后立即返回,需要轮询检查I/O是否完成
  3. I/O多路复用(I/O Multiplexing):通过select/poll/epoll等机制监控多个文件描述符
  4. 信号驱动I/O(Signal-driven I/O):进程发起I/O请求后继续执行,I/O就绪时接收信号通知
  5. 异步I/O(Asynchronous I/O):进程发起I/O请求后继续执行,I/O操作完全完成后接收通知

其中,Java的BIO对应阻塞I/O模型,NIO对应I/O多路复用模型,而AIO对应异步I/O模型。

I/O多路复用技术对比

Linux系统中的select、poll和epoll都属于I/O多路复用模型,它们的目标都是监控多个文件描述符,但实现机制和性能特点有所不同:

  1. select

    • 最早的I/O多路复用实现
    • 监控的文件描述符数量有限制(通常为1024)
    • 每次调用需要将整个文件描述符集合从用户态复制到内核态
    • 返回时需要遍历整个集合以找出就绪的描述符
    • 时间复杂度为O(n),随监控的描述符数量增加性能下降明显
  2. poll

    • select的改进版
    • 没有文件描述符数量的固定限制
    • 同样需要在每次调用时复制文件描述符集合
    • 也需要遍历整个集合查找就绪的描述符
    • 时间复杂度仍为O(n),大量描述符时性能同样会下降
  3. epoll

    • Linux特有的高性能I/O多路复用机制
    • 使用事件通知机制,只返回就绪的文件描述符
    • 描述符只在添加到监控集合时从用户态复制到内核态一次
    • 没有最大文件描述符数量的限制(除系统资源外)
    • 时间复杂度接近O(1),性能基本不随监控的描述符数量增加而下降
    • 在高并发场景下性能远优于select和poll

这三种机制都属于I/O多路复用模型,在Java的NIO实现中都可能被使用,具体使用哪种取决于操作系统平台。Java NIO会根据不同平台选择最优的多路复用实现,在Linux上优先使用epoll,在BSD系统上使用kqueue,在Windows上使用完成端口等。

实际应用举例

BIO应用场景

  • 简单的客户端应用
  • 连接数有限的小型服务器
  • 学习和教学环境

NIO应用场景

  • Netty网络应用框架
  • Tomcat 7+版本的连接器
  • Redis的Java客户端

AIO应用场景

  • 大型文件处理系统
  • 高性能数据库连接池
  • 实时消息处理系统

总结

理解BIO、NIO和AIO的区别对于设计高性能的网络应用至关重要。BIO提供了简单直观的编程模型但性能有限;NIO通过非阻塞I/O和多路复用提高了资源利用率;AIO则通过真正的异步操作进一步优化了I/O效率。根据应用场景的特点和需求,选择合适的I/O模型能够有效提升系统性能和可伸缩性。

在实际开发中,我们常常会结合使用这些I/O模型,或者通过成熟的框架如Netty间接使用它们,以便在保持代码简洁的同时获得高性能。

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

“JAVA反射机制详解”

什么是反射?

反射是与正射相对应的,正射就是在使用某个类之前就已经知道了这个类的类型等等,可以直接使用new关键字调用构造方法进行初始化,之后就可以正常使用这个类的的对象了
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java 语言的反射机制。

反射的特点

  1. 运行时类信息访问:反射允许程序在运行时获取类的完结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等等
  2. 动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过Class类的newInstance()方法或Constructor对象的newnstance()方法实现的。
  3. 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法
    实现,允许你传入对象实例和参数值来执行方法。
    4.访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过Field类的qet()和set()方法完成的

反射的缺点

破坏封装:由于反射允许访问私有字段和私有方法,所以可能会破坏封装而导致安全问题
性能开销:由于反射涉及到动态解析,因此无法执行Java 虚拟机优化,再加上反射的写法的确要复杂得多,所以性能要比“正射”差很多,在一些性能敏感的程序中应该避免使用反射。

反射的应用场景

  1. 通用框架中加载配置文件:类似spring、springboot等,为了保持通用性,通过不同的配置文件来加载不同的对象,比如数据库驱动等等
  2. 动态代理:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,动态代理的底层实现就是利用了反射
  3. 注解:注解本身不会直接影响程序的执行,但可以通过反射机制在运行时获取和处理这些注解

反射的基本使用

在 Java 中,Class 对象是一种特殊的对象,它代表了程序中的类和接口。Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。

获取类的Class对象

共有三种方法:

  1. 通过类的全名获取Class对象
    1
    Class clazz = Class.forName("com.xxx");
  2. 通过 Class 对象获取构造方法 Constructor 对象
    1
    Constructor constructor = clazz.getConstructor();
  3. 通过 Constructor 对象初始化反射类对象
    1
    Object object = constructor.newInstance();
  4. 获取要调用的方法的 Method 对象
    1
    2
    Method setNameMethod = clazz.getMethod("setName", String.class);
    Method getNameMethod = clazz.getMethod("getName");
  5. 通过 invoke() 方法执行
    1
    2
    setNameMethod.invoke(object, "zhangsan");
    getNameMethod.invoke(object)

反射底层机制

使用反射,必须要获得反射类的Class对象,每一个类只有一个Class对象,无论这个类生成了多少个对象。这个Class对象是由JAVA虚拟机生成的,由它来获取这个类的所有结构信息
也就是说,java.lang.Class 是所有反射 API 的入口
但是方法的反射调用都是通过Method对象的invoke()方法完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException {
// 如果方法不允许被覆盖,进行权限检查
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
// 检查调用者是否具有访问权限
checkAccess(caller, clazz, obj, modifiers);
}
}
// 获取方法访问器(从 volatile 变量中读取)
MethodAccessor ma = methodAccessor;
if (ma == null) {
// 如果访问器为空,尝试获取方法访问器
ma = acquireMethodAccessor();
}
// 使用方法访问器调用方法,并返回结果
return ma.invoke(obj, args);
}

invoke() 方法实际上是委派给 MethodAccessor 接口来完成的

MethodAccessor 接口有三个实现类,其中的 MethodAccessorImpl 是一个抽象类,另外两个具体的实现类 NativeMethodAccessorImplDelegatingMethodAccessorImpl 继承了这个抽象类。
NativeMethodAccessorImpl:通过本地方法来实现反射调用;
DelegatingMethodAccessorImpl:通过委派模式来实现反射调用;

invoke() 方法在执行的时候,会先调用 DelegatingMethodAccessorImpl,然后调用 NativeMethodAccessorImpl,最后再调用实际的方法

为什么不直接调用本地实现呢?

之所以采用委派实现,是为了能够在本地实现和动态实现之间切换。动态实现是另外一种反射调用机制,它是通过生成字节码的形式来实现的。如果反射调用的次数比较多,动态实现的效率就会更高,因为本地实现需要经过 Java 到 C/C++ 再到 Java 之间的切换过程,而动态实现不需要;但如果反射调用的次数比较少,反而本地实现更快一些。而这个临界点默认是15次,可以通过 -Dsun.reflect.inflationThreshold 参数类调整

反射常用的API

Class.forName(),参数为反射类的完全限定名。

类名 + .class,只适合在编译前就知道操作的 Class。

获取构造器、字段、方法等

getConstructor():返回反射类的特定 public 构造方法,可以传递参数,参数为构造方法参数对应 Class 对象;缺省的时候返回默认构造方法。
getDeclaredConstructor():返回反射类的特定构造方法,不限定于 public 的。
getConstructors():返回类的所有 public 构造方法。
getDeclaredConstructors():返回类的所有构造方法,不限定于 public 的。
获取字段和方法都是类似,换成Field或者Method就行

注意,如果反射访问私有字段和(构造)方法的话,需要使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制

注:参考javabetterxiaolincoding,侵删

“Redis常见面试题(持续更新)”

缓存三兄弟

缓存穿透

查询一个不存在的数据,redis查询不到,再查询mysql也查询不到,也不会直接写入缓存,导致每次请求都查询数据库
有人恶意通过发送大量不存在的请求,导致数据库宕机

解决方案一:缓存

redis缓存空数据,查询返回的数据为空,仍然把这个空结果进行缓存
优点:简单
缺点:消耗内存,可能会发生不一致的问题

解决方案二:布隆过滤器

布隆过滤器,在查询redis前,先查询布隆过滤器,检索一个元素是否在一个集合中
布隆过滤器原理:
存储数据:通过对数据进行若干个哈希函数计算得到哈希值,将bitmap对应位置的值置为一
查询数据:使用相同的哈希函数获取哈希值,判断对应位置是否都为一,如果不全为一说明不存在
注意:“不存在一定不存在,存在不一定存在
误判率:作为一个参数可以被设置,数组越小误判率越大,数组越大误判率越小,但响应的内存占用越大
优点:占用内存少,没有多余key
缺点:实现复杂,存在误判

缓存击穿

给某一个热点key设置了过期时间,当key过期时,恰好这时这个时间点对这个key有大量的并发请求,可能导致压垮数据库

解决方案一:互斥锁

互斥锁,当线程查询redis失败后,不立刻去请求数据库,而是使用如Redis的setnx去设置一个互斥锁,当获取锁成功时才进行查询,写入缓存完成后在释放锁
优点:可以保证redis和数据库的强一致性
缺点,性能差

解决方案二:自动续期

缓存击穿是由于热点key过期导致的,所以可以使用一个job给热点key自动更新缓冲,重新设置过期时间为30分钟

解决方案三:永不过期

对于很多热门key,其实是可以不用设置过期时间,让其永久有效的。比如参与秒杀活动的热门商品,由于这类商品id并不多,在缓存中我们可以不设置过期时间。在秒杀活动开始前,我们先用一个程序提前从数据库中查询出商品的数据,然后同步到缓存中,提前做预热
等压力过去之后,再手动删除这些无用的缓存

缓存雪崩

同时有多个key失效或者redis服务宕机,导致大量请求到达数据库,给数据库带来巨大压力

解决方案一:过期时间加随机数

为了解决缓存雪崩问题,我们首先要尽量避免缓存同时失效的情况发生,可以在过期时间上加上一个随机数,这样即使在高并发的情况下,多个请求同时设置过期时间,由于有随机数的存在,也不会出现太多相同的过期key。

解决方案二:高可用

针对缓存服务器down机的情况,在前期做系统设计时,可以做一些高可用架构。比如:如果使用了redis,可以使用哨兵模式,或者集群模式,避免出现单节点故障导致整个redis服务不可用的情况。
使用哨兵模式之后,当某个master服务下线时,自动将该master下的某个slave服务升级为master服务,替代已下线的master服务继续处理请求。

用JavaSocket编程开发聊天室

用JavaSocket编程开发聊天室

实验要求:

  1. 用Java图形用户界面编写聊天室服务器端和客户端, 支持多个客户端连接到一个服务器。每个客户端能够输入账号。
  2. 可以实现群聊(聊天记录显示在所有客户端界面)。
  3. 完成好友列表在各个客户端上显示。
  4. 可以实现私人聊天,用户可以选择某个其他用户,单独发送信息。
  5. 服务器能够群发系统消息,能够强行让某些用户下线。
  6. 客户端的上线下线要求能够在其他客户端上面实时刷新。
    本人目前大二,这是期末课程设计做得一个完整的玩具项目,但是由于水平和时间等问题,这个项目的设计和架构还是有些问题,比如说发送消息和指令都是使用Socket发送的,仅仅使用一些特殊字符来区别消息和指令,如果用户端直接发送和指令相同的字符串,则会导致bug。因此本项目的健壮性和可拓展性都较差,仅能作为课设使用。
    分为客户端和服务器端。
    服务器端功能:
  • 可以实现查看所有在线用户
  • 可以强制下线在线用户
  • 可以发送系统消息
  • 用户正常登录和退出会通知所有在线用户

客户端功能:

  • 输入服务器、端口和用户名即可登录
  • 可以发生群聊消息
  • 右侧显示所有在线用户
  • 双击右侧在线用户可发送私信

项目地址:
javaSocket聊天室
客户端Client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package client;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
private Socket socket;
private DataOutputStream outputStream;
private PrintWriter out;
private BufferedReader in;
private String serverAddress;
private int port;
private String username;
private OnMessageReceivedListener listener;

public Client(String serverAddress, int port, String username, OnMessageReceivedListener listener) throws IOException {
this.serverAddress = serverAddress;
this.port = port;
this.username = username;
this.listener = listener;
initConnection();
}



private void initConnection() throws IOException {
if (socket != null && !socket.isClosed()) {
return;
}
socket = new Socket(serverAddress, port);
outputStream = new DataOutputStream(socket.getOutputStream());
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
}

public void sendMessage(String message) {
if (outputStream != null && !socket.isClosed()) {
try {
out.println(message);
out.flush();
} catch (Exception e) {
handleSendError(e);
}
} else {
System.err.println("Socket is not properly initialized or is closed.");
}
}

public void sendPrivateMessage(String recipient, String message) {
sendMessage("/pm " + recipient + " " + message);
}

private void handleSendError(Exception e) {
e.printStackTrace();
try {
socket.close();
} catch (IOException closeException) {
closeException.printStackTrace();
}
listener.onConnectionLost();
}

public void connect() {
try {
initConnection();
sendMessage(username); // 发送用户名以登录

Thread readerThread = new Thread(() -> {
String message;
try {
while ((message = in.readLine()) != null) {
listener.onMessageReceived(message);
}
} catch (IOException e) {
listener.onConnectionLost();
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
readerThread.start();
} catch (UnknownHostException e) {
System.err.println("Server not found: " + e.getMessage());
listener.onConnectionLost();
} catch (IOException e) {
System.err.println("Error connecting to server: " + e.getMessage());
listener.onConnectionLost();
}
}

public void disconnect() {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

public String getUsername() {
return username;
}

public interface OnMessageReceivedListener {
void onMessageReceived(String message);
void onConnectionLost();
void onUpdateOnlineUsers(String userListData);
void onForceLogout();
}
}

客户端界面ClientGUI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package client;

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

public class ClientGUI extends Component {
private JTextField serverAddressField, portField, usernameField, messageField;
private JButton connectButton, sendButton;
private JTextArea chatArea;
private JList<String> onlineList;
private DefaultListModel<String> onlineListModel;
private Client client;

public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ClientGUI().initializeUI());
}

private void initializeUI() {
JFrame frame = new JFrame("Chat Client");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);

chatArea = new JTextArea();
chatArea.setEditable(false);
JScrollPane scrollPane = new JScrollPane(chatArea);
frame.add(scrollPane, BorderLayout.CENTER);

JPanel southPanel = new JPanel();
southPanel.setLayout(new BorderLayout());

JPanel inputPanel = new JPanel();
inputPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
messageField = new JTextField(30);
sendButton = new JButton("Send");
inputPanel.add(messageField);
inputPanel.add(sendButton);
southPanel.add(inputPanel, BorderLayout.CENTER);

JPanel connectPanel = new JPanel();
connectPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
serverAddressField = new JTextField("localhost", 10);
portField = new JTextField("12345", 5);
usernameField = new JTextField("User", 10);
connectButton = new JButton("Connect");
connectPanel.add(new JLabel("Server: "));
connectPanel.add(serverAddressField);
connectPanel.add(new JLabel(" Port: "));
connectPanel.add(portField);
connectPanel.add(new JLabel(" Username: "));
connectPanel.add(usernameField);
connectPanel.add(connectButton);
southPanel.add(connectPanel, BorderLayout.SOUTH);

frame.add(southPanel, BorderLayout.SOUTH);

onlineListModel = new DefaultListModel<>();
onlineList = new JList<>(onlineListModel);
JScrollPane onlineScrollPane = new JScrollPane(onlineList);
onlineScrollPane.setPreferredSize(new Dimension(150, 0));
frame.add(onlineScrollPane, BorderLayout.EAST);

createEvents();

frame.setVisible(true);
}

private void createEvents() {
connectButton.addActionListener(e -> {
String serverAddress = serverAddressField.getText();
int port = Integer.parseInt(portField.getText());
String username = usernameField.getText();
connectButton.setEnabled(false);
try {
client = new Client(serverAddress, port, username, new GUIListener());
client.connect();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "连接失败,请检查输入信息。", "连接错误", JOptionPane.ERROR_MESSAGE);
connectButton.setEnabled(true);
}
});

sendButton.addActionListener(e -> {
String message = messageField.getText();
if (!message.isEmpty()) {
client.sendMessage(message);
displaySentMessage(message);
messageField.setText("");
}
});

onlineList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
String selectedUser = onlineList.getSelectedValue();
if (selectedUser != null && client != null) {
String privateMessage = JOptionPane.showInputDialog(ClientGUI.this,
"输入要发送给 " + selectedUser + " 的消息:", "发送私信", JOptionPane.PLAIN_MESSAGE);
if (privateMessage != null && !privateMessage.trim().isEmpty()) {
client.sendPrivateMessage(selectedUser, privateMessage);
displaySentMessage("[私信给 " + selectedUser + "]: " + privateMessage); // 显示发送的私聊消息
}
}
}
}
});
}

private void displaySentMessage(String message) {
SwingUtilities.invokeLater(() -> {
chatArea.append(client.getUsername() + ": " + message + "\n");
chatArea.setCaretPosition(chatArea.getDocument().getLength());
});
}

private class GUIListener implements Client.OnMessageReceivedListener {
@Override
public void onMessageReceived(String message) {
SwingUtilities.invokeLater(() -> {
if (message.startsWith("/users ")) { // 检查消息是否以/users开头
onUpdateOnlineUsers(message.substring(7)); // 去掉"/users "前缀,然后更新在线用户列表
}else if(message.equals("/forceLogout")){
onForceLogout();
}else if(message.equals("/server/ERROR: 用户名已被占用,请选择其他用户名。")){
JOptionPane.showMessageDialog(ClientGUI.this, "用户名已被占用,请选择其他用户名。", "连接错误", JOptionPane.ERROR_MESSAGE);
connectButton.setEnabled(true);
client.disconnect();
}else if(message.equals("/server/SUCCESS: 连接成功")){
JOptionPane.showMessageDialog(ClientGUI.this, "连接成功", "连接状态", JOptionPane.INFORMATION_MESSAGE);
}
else {
chatArea.append(message + "\n");
chatArea.setCaretPosition(chatArea.getDocument().getLength());
}
});
}

@Override
public void onConnectionLost() {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(ClientGUI.this, "连接丢失,请检查网络或重新登录。", "连接错误", JOptionPane.ERROR_MESSAGE);
connectButton.setEnabled(true);
});
}

@Override
public void onUpdateOnlineUsers(String userListData) {
SwingUtilities.invokeLater(() -> {
String[] usernames = userListData.split(","); // 假设用户列表是以逗号分隔的用户名
onlineListModel.clear(); // 清空现有在线用户列表
for (String username : usernames) {
onlineListModel.addElement(username.trim()); // 添加每个用户名到在线用户列表模型中
}
});
}

@Override
public void onForceLogout(){
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(null, "抱歉!您已被服务器强制下线!", "强制下线", JOptionPane.WARNING_MESSAGE);
System.exit(0); // 关闭客户端程序
});
}
}
}

Common包下Message类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package common;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Message {
private String sender;
private String recipient;
private String content;
private LocalDateTime timestamp;

// 构造函数
public Message(String sender, String recipient, String content) {
this.sender = sender;
this.recipient = recipient;
this.content = content;
this.timestamp = LocalDateTime.now(); // 当前时间作为发送时间
}

public Message(String sender, String content) {
this(sender, "Everyone", content);
}

// Getter 和 Setter 方法
public String getSender() {
return sender;
}

public void setSender(String sender) {
this.sender = sender;
}

public String getRecipient() {
return recipient;
}

public void setRecipient(String recipient) {
this.recipient = recipient;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public LocalDateTime getTimestamp() {
return timestamp;
}

// 格式化时间戳的字符串表示
public String getFormattedTimestamp() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return timestamp.format(formatter);
}

// 重写toString方法,便于打印或显示消息详情
@Override
public String toString() {
return String.format("[%s] %s -> %s: %s",
getFormattedTimestamp(),
sender,
recipient,
content);
}
}

服务器端Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package server;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import common.Message;


public class Server {
private static final int PORT = 12345; // 服务器端口
private final List<ServerThread> clients = new ArrayList<>(); // 存储所有连接的客户端线程
private ServerGUI gui;

public Server(ServerGUI gui) {
this.gui = gui;
}
public static void main(String[] args) {
ServerGUI gui = new ServerGUI();
Server server = new Server(gui);
gui.setServer(server);
server.startServer();
}

public void startServer() {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("服务器启动,正在监听端口: " + PORT);

while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待客户端连接
ServerThread serverThread = new ServerThread(socket, this);
serverThread.start(); // 启动线程处理客户端请求
System.out.println("新客户端连接: " + socket.getInetAddress());
}
} catch (IOException e) {
e.printStackTrace();
System.err.println("服务器启动失败");
}
}

public synchronized void addClient(ServerThread client) {
clients.add(client);
updateOnlineUsers();
gui.updateUserList(clients);
}

public synchronized boolean removeClient(ServerThread client) {
boolean removed = clients.remove(client);
if (removed) {
updateOnlineUsers();
gui.updateUserList(clients);
}
return removed;
}

public synchronized void broadcast(Message message, ServerThread excludeClient) {
for (ServerThread client : clients) {
if (client != excludeClient) {
client.send(message);
}
}
}

public synchronized void updateOnlineUsers() {
StringBuilder userList = new StringBuilder("/users ");
for (ServerThread client : clients) {
userList.append(client.getUsername()).append(",");
}
String userListMessage = userList.toString();
if (userListMessage.endsWith(",")) {
userListMessage = userListMessage.substring(0, userListMessage.length() - 1);
}
for (ServerThread client : clients) {
client.sendRawMessage(userListMessage);
}
}

public List<ServerThread> getClients() {
return clients;
}

public void forceLogout(String username) {
for (ServerThread client : clients) {
if (client.getUsername().equals(username)) {
client.interrupt(); // 中断客户端线程以强制下线
removeClient(client); // 从列表中移除客户端
client.forceLogout();
gui.updateUserList(clients);
break;
}
}
}
public synchronized boolean isUsernameTaken(String username) {
for (ServerThread client : clients) {
if (client.getUsername().equals(username)) {
return true;
}
}
return false;
}

public synchronized void sendSystemMessage(String content) {
Message systemMessage = new Message("Server", "Everyone", content);
broadcast(systemMessage, null);
}
}

客户端界面ServerGUI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package server;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;

public class ServerGUI extends JFrame {
private JList<String> userList;
private DefaultListModel<String> userListModel;
private Server server;

public ServerGUI() {
setTitle("Chat Server");
setSize(400, 300);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);

userListModel = new DefaultListModel<>();
userList = new JList<>(userListModel);
JScrollPane scrollPane = new JScrollPane(userList);

JButton forceLogoutButton = new JButton("Force Logout");
forceLogoutButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String selectedUser = userList.getSelectedValue();
if (selectedUser != null) {
server.forceLogout(selectedUser);
}
}
});

// 系统消息输入框和发送按钮
JTextField systemMessageField = new JTextField();
JButton sendSystemMessageButton = new JButton("发送系统消息");
sendSystemMessageButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String message = systemMessageField.getText();
if (message != null && !message.trim().isEmpty()) {
server.sendSystemMessage(message);
systemMessageField.setText(""); // 清空输入框
}
}
});

JPanel panel = new JPanel(new BorderLayout());

panel.add(scrollPane, BorderLayout.CENTER);
panel.add(forceLogoutButton, BorderLayout.SOUTH);

// 系统消息面板
JPanel systemMessagePanel = new JPanel(new BorderLayout());
systemMessagePanel.add(systemMessageField, BorderLayout.CENTER);
systemMessagePanel.add(sendSystemMessageButton, BorderLayout.EAST);

// 添加到主窗口
add(panel, BorderLayout.CENTER);
add(systemMessagePanel, BorderLayout.SOUTH);

setVisible(true);
}

public void updateUserList(List<ServerThread> clients) {
SwingUtilities.invokeLater(() -> {
userListModel.clear();
for (ServerThread client : clients) {
userListModel.addElement(client.getUsername());
}
});
}

public void setServer(Server server) {
this.server = server;
}
}

客户端ServerThread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package server;

import common.Message;

import java.io.*;
import java.net.Socket;

public class ServerThread extends Thread {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private String username;
private Server server;
private volatile boolean running = true;

public ServerThread(Socket socket, Server server) {
this.socket = socket;
this.server = server;
try {
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
} catch (IOException e) {
e.printStackTrace();
System.err.println("Error initializing streams for client: " + e.getMessage());
}
}

@Override
public void run() {
try {
this.username = in.readLine();
if (server.isUsernameTaken(this.username)) {
sendRawMessage("/server/ERROR: 用户名已被占用,请选择其他用户名。");
socket.close();
return;
}

sendRawMessage("/server/SUCCESS: 连接成功");
System.out.println("账号" + this.username + "已经登录");
server.addClient(this); // 注意不要在构造函数中重复调用 addClient
server.broadcast(new Message(username, "Server", username + " has joined the chat!"), this);

String inputLine;
while (running && (inputLine = in.readLine()) != null) {
if (inputLine.startsWith("/pm ")) {
handlePrivateMessage(inputLine);
}
else {
server.broadcast(new Message(username, "Everyone", inputLine), this);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
boolean removed = server.removeClient(this);
if (removed) {
server.broadcast(new Message(username, "Server", username + " has left the chat!"), null);
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public void send(Message message) {
if (out != null && !out.checkError()) {
out.println(message.toString());
out.flush();
}
}

public void sendRawMessage(String message) {
try {
if (out != null && !out.checkError()) {
out.println(message);
out.flush();
} else {
System.err.println("Output stream is closed, cannot send message.");
}
} catch (Exception e) {
e.printStackTrace();
System.err.println("Error sending message: " + e.getMessage());
}
}

private void handlePrivateMessage(String inputLine) {
int firstSpace = inputLine.indexOf(" ");
int secondSpace = inputLine.indexOf(" ", firstSpace + 1);
if (secondSpace != -1) {
String recipient = inputLine.substring(firstSpace + 1, secondSpace);
String message = inputLine.substring(secondSpace + 1);
for (ServerThread client : server.getClients()) {
if (client.getUsername().equals(recipient)) {
client.send(new Message(username, recipient, message));
break;
}
}
}
}

public String getUsername() {
return username;
}
public void forceLogout() {
running = false; // 停止主循环
sendRawMessage("/forceLogout");
try {
socket.close(); // 关闭Socket以触发IOException并停止线程
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用方法

首先启动服务器端Server,再启动客户端界面ClientGUI,关于同时启动多个实例比较简单,大家可以自行搜索。

其他

个人主页:
张明宇的个人主页