今天来分享一个简易小程序,也算是我个人的第一个小项目,具体介绍如下
一、项目名称
基于多线程的简易聊天室
二、实现功能
客户端连接到服务器,并与服务器之间进行交流,服务器可处理客户端的注册与退出事件;多个客户端之间可以私聊,群聊。
三、所用知识
接口,集合框架中相关类,Socket编程,多线程相关知识
四、使用工具:
idea的maven工具
五、项目背景
当时学完Java的多线程部分知识之后,觉得多线程这么厉害的东东,将来必成大器,又想到当时老师说的多线程在并发方面的应用,突然灵光乍现,我们平时用的QQ聊天,不是也用到了多线程吗,何不自己实现一个简易版的聊天室,由此,聊天室即将诞生。
六、实现具体思路
要实现客户端与服务器之间的交互,因此需要有两大模块:服务器模块和客户端模块。两大模块之间具体实现与联系用下图来表示:
这里提一下: 套接字socket是IP地址和端口号的组合,它可以标记一台主机的唯一一个进程,因此我们可以通过socket来实现客户端与服务器的连接。
七、代码实现
客户端
客户端任务
import java.io.IOException;
import java.net.Socket;
/**
* 任务:连接服务器,处理客户端的发送和接收任务
* 类型:普通类(主类)
* */
public class MultiThreadClient {
public static void main(String[] args) {
try {
int port = 6666;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
System.out.println("使用默认端口号" + port);
}
}
String host = "127.0.0.1";
if (args.length > 1) {
host = args[1];
}
//创建客户端的套接字
Socket socket = new Socket(host, port);
//向服务器发数据
new WriteDataToServerThread(socket).start();
//从服务器读数据
new ReadDataFromServerThread(socket).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端读线程
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 任务:从服务端读数据
* 类型:线程(继承自Tread)
* */
public class ReadDataFromServerThread extends Thread {
private final Socket client;
//构造方法传参
public ReadDataFromServerThread(Socket client){
this.client=client;
}
@Override
public void run(){
try{
InputStream clientInput=client.getInputStream();
Scanner sc=new Scanner(clientInput);
while(sc.hasNext()) {
String message=sc.next();
System.out.println("来自服务器的消息:"+message);
if(message.equals("bye")){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端写线程
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* 任务:向服务端写数据
* 类型:线程(继承自Thread)
* */
public class WriteDataToServerThread extends Thread{
private final Socket client;
//构造方法传参
public WriteDataToServerThread(Socket client){
this.client=client;
}
@Override
public void run(){
try{
OutputStream clientOutput=client.getOutputStream();//获取客户端的输入字符流
OutputStreamWriter writer=new OutputStreamWriter(clientOutput);//将字符流变为字节流
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("请输入消息:");
String message=sc.nextLine(); //获取输入信息
writer.write(message+"\n");
writer.flush(); //刷新
if(message.equals("bye")){
client.close();
break;
}
}
}catch(IOException e){
}
}
}
服务器端
服务端处理过程
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 任务:创建线程池,动态获取客户端地址,循环等待客户端连接,并不断提交任务
* 类型:普通类
* */
public class MultiThreadServer {
public static void main(String[] args) {
//准备线程池
final ExecutorService excutorService=Executors.newFixedThreadPool(10);
try {
int port=6666;
if(args.length>0){
try{
port=Integer.parseInt(args[0]); //将字符串参数解析为带符号的十进制整数。
}catch(NumberFormatException e){
System.out.println("使用默认端口号"+port);
}
}
String host="127.0.0.1";
if(args.length>1){
host=args[1] ;
}
ServerSocket serverSocket=new ServerSocket(port);
System.out.println("等待客户端连接...");
while(true) {
Socket client = serverSocket.accept();//等待客户端连接
Future<?> submit = excutorService.submit(new ExcuteClient(client));//提交任务
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端处理客户端任务的具体实现类
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
/**
* 任务:服务端处理客户端连接的任务
* 1,注册
* 2,私聊
* 3,群聊
* 4,退出
* 5,显示当前在线用户
* 类型:Runnable接口的实现类
*/
public class ExcuteClient implements Runnable {
// 使用static 是因为每个对象都是new的,Map有多个不共享,所以需要static修饰
// 多线程聊天室,为避免并发修改异常,需要安全的ConcurrentHashMap,
private static final Map<String,Socket> ONLINE_USER_MAP=new ConcurrentHashMap<>();
private final Socket client;
public ExcuteClient(Socket client){
this.client=client;
}
@Override
public void run(){
try {
//1,获取客户端输入
InputStream clientInput=this.client.getInputStream();
Scanner sc=new Scanner(clientInput);
while(true){
String line=sc.nextLine();
/**
* 定义一个用户的输入格式如下:
* 1, 注册:userName:<name>
* 2, 私聊:private:<name>:<message>
* 3, 群聊:group:<message>
* 4, 退出:bye
* 根据以上四种格式对输入进行拆分,根据不同的输入处理不同的业务
*/
//处理注册业务
if(line.startsWith("userName")){
String userName=line.split("\\:")[1];//字符串拆分得到注册者姓名
this.register(userName,client); //调用具体的注册逻辑处理
continue;
}
//处理私聊业务
if(line.startsWith("private")){
String[] segments=line.split("\\:");//按 :将输入分段
String userName=segments[1]; //取出要私聊的用户
String message=segments[2]; //取出要私聊的内容
this.privateChat(userName,message); //调用具体的私聊逻辑处理
continue;
}
//处理群聊业务
if(line.startsWith("group")){
String message=line.split("\\:")[1]; //取出要群聊的内容
this.groupChat(message); //调用具体的群聊逻辑处理
continue;
}
//处理退出业务
if(line.equals("bye")){
this.quit();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 注册 的具体逻辑实现
private void register(String userName,Socket client){
System.out.println(userName+"加入到聊天室"+client.getRemoteSocketAddress());
ONLINE_USER_MAP.put(userName,client); //将注册的客户信息保存到 MAP 里
printOnlineUser();
sendMessage(this.client,userName+"注册成功!");
}
// 私聊 的具体逻辑实现
private void privateChat(String userName,String message){
String currentUserName=this.getCurrentUserName(); //获得当前用户
Socket target= ONLINE_USER_MAP.get(userName); // 获得当前用户要私聊的目标对象
if(target!=null){
this.sendMessage(target,currentUserName+"对你说:"+message);//将当前用户要私聊的内容发给目标对象
}
}
// 群聊 的具体逻辑实现
private void groupChat(String message){
for(Socket socket:ONLINE_USER_MAP.values()){ //遍历所有在线的当前用户
//不给自己发
if(socket.equals(this.client)){
continue;
}
this.sendMessage(socket,this.getCurrentUserName()+"说:"+message);//将消息发给所有用户
}
}
// 退出 的具体逻辑实现
private void quit(){
String currentUserName=this.getCurrentUserName(); //获得当前用户
System.out.println("用户:"+currentUserName+"下线");
Socket socket=ONLINE_USER_MAP.remove(currentUserName); //将当前用户从 MAP 里删除
this.sendMessage(socket,"bye"); //*******
printOnlineUser();
}
//获得当前用户名
private String getCurrentUserName(){
String currentUserName="";
for(Map.Entry<String,Socket> entry:ONLINE_USER_MAP.entrySet()) {
if (this.client.equals(entry.getValue())) {
currentUserName = entry.getKey();
break;
}
}
return currentUserName;
}
// 发送消息
private void sendMessage(Socket socket,String message){
try {
OutputStream clientOutput=socket.getOutputStream();//获取客户端的输入
OutputStreamWriter writer=new OutputStreamWriter(clientOutput);//将输入的字符流转成字节流
writer.write(message+"\n");//将字节流的内容写出
writer.flush(); //*********刷新
} catch (IOException e) {
e.printStackTrace();
}
}
private static void printOnlineUser(){
System.out.println("当前在线人数:"+ONLINE_USER_MAP.size()+"用户列表如下:");
for(Map.Entry<String,Socket> entry: ONLINE_USER_MAP.entrySet()){
System.out.println(entry.getKey());
}
}
}
聊天室的整个项目代码就这么多,但是,编码结束并不意味着项目结束,还有一个大块头需要处理,那就是-----项目测试
八、项目测试
测试真是一个耗时又耗体力的活,测试之前,先得分析我们的需求
需求分析:
服务器端:
(1)服务器可以接收到客户端的连接;
(2)服务器可以根据客户端的输入格式明白自己要做什么事,并进行任务分配与调度
(3)服务器的处理结果能够正确返回给正确的客户端
(4)服务器能正常退出关闭
客户端:
(1)客户端可以连接到服务器
(2)客户端连接到服务器后可以向服务器发送数据来表明自己要完成的任务
(3)客户端可以接受到服务器的正确返回结果
(4)客户端能正常退出关闭
制定测试计划:
在基于需求的基础上,我大概列了需要测试的点如下图所示:
由于其实现的功能较少,没有界面也没有登陆相关信息,因此不涉及到安全测试和界面测试。
上述的测试用例写的比较少,基于目前水平限制,还有好多测试地方暂时也不太会做,因此相应的测试用例也没编写进去,等以后掌握了更多测试技能,会更加全面地改进并测试该项目。
测试结果:
(这里只说测试出现异常的地方)
(1)客户端输入异常的字符格式发现服务端并不能对其进行识别,因此对该类消息没有作出回应(错误推测法)
(2)在测试一次发送消息的最大长度时,最长一次发送了25093个字符,消息依然能发送成功,未找到其上边界,由于我们正常发消息的长度远远小于25093,因此默认为对发送消息的长度没有限制。
(3)发现的一个小bug,当输入的消息中含有空格时,服务器会默认为是两条消息将其发给同一个人(虽然与预期不符,但并不影响正常功能)
(4)根据线程池所容纳的最大线程数量用边界值分析法创建多个客户端的时候,发现当前在线人数超过线程池所容纳的最大线程数时,新用户将不能成功注册,除非有一个客户下线,新用户才能注册成功。
(5)在对程序的易用性进行测试时发现,当某个客户下线后,服务器端并不会保存该用户的相关信息,下次上线需重新注册,不易于使用。
九、项目总结
所遇难题:刚开始有了基本的思路框架之后,并知道如何去编写程序实现两个进程之间的通信,后来通过对网络知识的学习了解,认识到了Socket这个东西,知道它可以实现两个进程间的通信,于是对Socket的使用做了一定了解后,才开始了编程实现,其余的程序方面出现的问题后都经过自己调试程序、请教同学老师一一得到了解决。
收获:通过小程序的编写,实现了基本的群聊与私聊的功能,加深了对集合框架与多线程部分知识的理解,学会了如何用Socket编程来实现不同进程之间的通信,同时也体会到了编程的乐趣。
聊天室的小程序今天就先写到这里吧,既然是“陋室”,那肯定还有许多要改进的地方,希望各位看官朋友们能多多提出你们的宝贵的意见哦。