目录
1. 前言
之前写过单线程版本的聊天室,这次对之前的版本进行扩展与优化,将其升级为一个多线程版本的聊天室,更好的贴近一个真实的聊天工具。
在这个版本中,聊天室具有注册、私聊、群聊以及退出的功能。
2. 功能实现
- 服务器需要实现
- 用户注册
- 用户私聊
- 用户群聊
- 用户退出
- 客户端需要实现
- 发送信息
- 接收信息
3. 模块划分
- 服务器主线程:创建服务器、创建线程池
- 任务线程:进行业务处理
- 客户端主线程:创建客户端、分发任务(输入、输出)
- 两个任务线程:数据写入服务器、从服务器读取线程
4. 功能分析
该版本需要实现一个服务器同时处理多个客户端的请求 -> 利用多线程进行处理
-
4.1 前期分析
-
客户端和服务器的创建与连接类似于单线程版本
-
为了避免硬编码,利用参数确定端口号和地址(也可利用外部文件、交互式输入)
- 利用线程池约束线程创建的数量,防止负载过高。
- 线程池的选择
- 与用户交互相关,任务周期无法确定,不能用 CachesThreadPool
- 该版本需要多线程,不能用 SingleThreadPool
- 选择固定大小线程池 FixedThreadPool
- 线程池大小
- 通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的CPU个数(Ncpu)
- I/O密集型任务线程并应配置尽可能多的线程,如2*Ncpu个线程的线程池
- 让线程池专门去处理业务,客户端关闭,业务终止。
- 线程池的选择
- 利用循环实现多客户端的支持
- 当一个客户端发出请求时,服务器启动一个线程,去处理数据的传输。
- 数据的传输由一个读数据线程和一个写数据线程完成
-
-
4.2 具体实现
-
服务端实现:
1.循环监听客户端连接
2.维护所有在线的客户端,记录在线人数
3.注册功能:将客户端名称添加到服务器客户端集合中
4.群聊功能:接收客户端发送的消息,在发送给所有客户端(除过自己)
5.私聊功能:客户端与指定客户端间的数据传送
6.退出功能:从服务器客户端集合中移除客户端
-
客户端实现:
1.命令行的交互式输入输出
2.注册功能:创建Socket,给服务器发送注册消息
3.群聊功能:客户端发送和接收数据
4.私聊功能:客户端指定客户端,发送和接收数据
5.退出功能:向服务器发送退出指令
-
5. 使用技术
- Socket编程
- I/O
- 多线程
6. 代码
package com.qqy.chat.client.mul;
import java.io.IOException;
import java.net.Socket;
/**
* 客户端
* Author:qqy
*/
public class MulClient {
public static void main(String[] args) {
try {
String host="127.0.0.1";
int port=65521;
//先读取地址,再读取端口号
if(args.length==2){
host=args[0];
try{
port=Integer.parseInt(args[1]);
}catch (NumberFormatException e){
System.out.println("指定端口号格式错误,采用默认端口号"+port);
port=65521;
}
}
Socket clientSocket=new Socket(host,port);
System.out.println("端口号为"+clientSocket.getLocalPort()+"的客户端已创建");
System.out.println("已连接上端口号为"+clientSocket.getPort()+"的服务器...");
System.out.println("\n请按照提示信息进行操作:");
System.out.println("\t请求按行读取");
System.out.println("\t注册: register:<userName> 例如: register:lila");
System.out.println("\t群聊: groupChat:<message> 例如: groupChat:大家好");
System.out.println("\t私聊: privateChat:<userName>:<message> 例如: privateChat:nina:你好呀");
System.out.println("\t退出: bye\n");
new WriteDatatoServer(clientSocket).start();
new ReadDataFromServer(clientSocket).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.qqy.chat.client.mul;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端给服务端发送数据的线程
* Author:qqy
*/
public class WriteDatatoServer extends Thread {
private Socket clientSocket;
public WriteDatatoServer(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try {
OutputStream out = clientSocket.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(out);
Scanner scanner = new Scanner(System.in);
System.out.println("请输入需求—————— ");
while (true) {
String data=scanner.nextLine();
writer.write(data+"\n");
writer.flush();
if(data.equals("bye")){
break;
}
}
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.qqy.chat.client.mul;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端从服务端读取数据的线程
* Author:qqy
*/
public class ReadDataFromServer extends Thread {
private Socket clientSocket;
public ReadDataFromServer(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try {
InputStream in=clientSocket.getInputStream();
Scanner scanner=new Scanner(in);
while (true){
System.out.println("来自服务器的消息:\n\t"+scanner.nextLine());
System.out.println("\n请输入需求—————— ");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.qqy.chat.server.mul;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务器
* Author:qqy
*/
public class MulServer {
public static void main(String[] args) {
try {
int port=65521;
if(args.length==1){
try{
port=Integer.parseInt(args[0]);
}catch (NumberFormatException e){
System.out.println("指定端口号格式错误,采用默认端口号"+port);
port=65521;
}
}
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("端口号为" + serverSocket.getLocalPort() + "服务器已创建,等待客户端的连接...");
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
//利用循环实现多线程,以支持多用户
while (true) {
//客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("接收到客户端"+clientSocket.getRemoteSocketAddress()+"的连接");
/*
不在循环中直接进行业务处理
∵accept()是阻塞方法,若在循环中直接处理业务,每次循环不能很快结束,阻塞了其他客户端的连接
*/
//线程池分配线程
//每次有客户端连接到服务器的时候,就创建一个HandleClient的实例化对象来处理具体的业务
executorService.execute(new HandleClient(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.qqy.chat.server.mul;
import java.io.*;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
/**
* Author:qqy
*/
public class HandleClient implements Runnable {
private static final Map<String, Socket> ONLINE_CLIENT = new ConcurrentHashMap<>();
private final Socket client;
public HandleClient(Socket client) {
this.client = client;
}
@Override
public void run() {
try {
//获取客户端的输入
InputStream in = client.getInputStream();
//将字符流转换为字节流
Scanner scanner = new Scanner(in);
while (true) {
String data = scanner.nextLine();
if (data.startsWith("register")) {
if (data.split(":").length == 2) {
String userName = data.split(":")[1];
//若当前用户存在
if (ONLINE_CLIENT.containsKey(userName)) {
sendMsg(this.client, "该用户名已存在,请重新注册!!!", false);
} else{
register(userName);
}
} else {
sendMsg(this.client, "请输入需要注册的用户名!!!", false);
}
continue;
}
if (data.startsWith("groupChat")) {
String msg = data.split(":")[1];
groupChat(msg);
continue;
}
if (data.startsWith("privateChat")) {
String[] require = data.split(":");
String object = require[1];
String msg = require[2];
privateChat(object, msg);
continue;
}
if (data.equals("bye")) {
bye();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端退出
*/
private void bye() {
for (Map.Entry<String, Socket> entry : ONLINE_CLIENT.entrySet()) {
Socket target = entry.getValue();
if (target == this.client) {
ONLINE_CLIENT.remove(entry.getKey());
break;
}
}
printOnline();
}
/**
* 打印当前在线人数
*/
private void printOnline() {
System.out.println("当前在线人数:" + ONLINE_CLIENT.size());
System.out.println("在线用户为:");
for (String userName : ONLINE_CLIENT.keySet()) {
System.out.println(userName);
}
}
/**
* 私聊
*
* @param object 私聊对象
* @param msg 发送信息
*/
private void privateChat(String object, String msg) {
Socket target = ONLINE_CLIENT.get(object);
if (target == null) {
sendMsg(this.client, "用户" + object + "不存在!", false);
} else {
sendMsg(target, msg, true);
}
}
/**
* 群聊
*
* @param msg 发送信息
*/
private void groupChat(String msg) {
for (Map.Entry<String, Socket> entry : ONLINE_CLIENT.entrySet()) {
Socket target = entry.getValue();
if (target != this.client) {
sendMsg(target, msg+"(来自群聊)", true);
}
}
}
/**
* 用户注册
*
* @param userName 用户名
*/
private void register(String userName) {
//TODO 用户名相同如何处理
ONLINE_CLIENT.put(userName, this.client);
printOnline();
sendMsg(this.client, "恭喜" + userName + ",注册成功", false);
}
/**
* 获取用户名
*
* @return
*/
private String getUserName() {
for (Map.Entry<String, Socket> entry : ONLINE_CLIENT.entrySet()) {
Socket target = entry.getValue();
if (target == this.client) {
return entry.getKey();
}
}
return "";
}
private void sendMsg(Socket target, String msg, boolean flag) {
try {
OutputStream out = target.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(out);
if (flag) {
writer.write("<" + getUserName() + "说>" + "\t" + msg + "\n");
} else {
writer.write(msg + "\n");
}
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}