本文章主要介绍基于多线程的Java聊天室的整体思路以及如何通过代码一步步的实现多线程的Java聊天室。
利用Socket编程,客户端与服务器端建立连接的步骤如下:
(1)服务器端通过java.net.ServerSocket类的构造方法实例化ServerSocket对象,选择的构造方法如下:
public ServerSocket (int port) throws IOException
该构造方法需要传入参数:端口号,从而创建绑定到指定端口的服务器套接字。
(2)客户端通过java.net.Socket类的构造方法创建一个流套接字并将其连接到指定主机上的指定端口号,该构造方法如下:
public Socket(String host,int port) throws UnknowHostException,IOException
(3)服务器端的ServerSocket对象调用accept()方法侦听请求连接指定端口号的该服务器端的客户端,并在接收到客户端请求后返回服务器端的流套接字,即Socket对象,从而服务器端与客户端成功建立连接。
(4)客户端与服务器端之间的通信操作,java.net.Socket类就是提供客户端与服务器端相互通信的套接字。
获取Socket套接字的输入流的方法为:
public InputStream getInputStream() throws IOException
获取Socket套接字的输出流的方法为:
public OutputStream getOutputStream() throws IOException
在获取了服务器端与客户端的输入输出流之后,进行信息输入输出即可进行通信操作。
(5)服务器端与客户端之间通信结束后,需要关闭套接字,调用close()方法即可。close()方法如下:
public void close() throws IOException
经过以上的分析,下面通过代码的方式创建服务器端与客户端之间的连接以及通信操作。
package chat.room.server;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class SingleServer {
public static void main(String[] args) throws IOException {
//1.创建服务器端的ServerSocket对象,等待客户端连接其端口
ServerSocket serverSocket=new ServerSocket(6666);
System.out.println("服务器的端口号为:6666,正在等待客户端的连接...");
//2.侦听并接收服务器端的连接,返回套接字Socket对象
Socket socket=serverSocket.accept();
//3.获取客户端的输入流,读取客户端输入的内容
Scanner scanner=new Scanner(socket.getInputStream());
scanner.useDelimiter("\n");
if(scanner.hasNext()){
System.out.println("客户端发来消息:"+scanner.next());
}
//4.获取客户端的输出流,向客户端输出内容
PrintStream printStream=new PrintStream(socket.getOutputStream());
printStream.println("客户端你好!我是服务器端:"+serverSocket.getLocalPort());
//5.关闭流
serverSocket.close();
}
}
package chat.room.client;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class SingleClient {
public static void main(String[] args) throws IOException {
//1.客户端连接服务器端,返回套接字Socket对象
Socket socket=new Socket("127.0.0.1",6666);
//2.获取服务器端的输出流,向服务器端输出内容
PrintStream printStream=new PrintStream(socket.getOutputStream());
printStream.println("我是客户端"+socket.getLocalPort());
//3.获取服务器端的输入流,读取服务器端的内容
Scanner scanner=new Scanner(socket.getInputStream());
scanner.useDelimiter("\n");
if(scanner.hasNext()){
System.out.println(scanner.next());
}
//4.关闭流
socket.close();
}
}
客户端方面,需要做的只有两件事:1. 向服务器端发送信息 2. 接收服务器端信息
故创建两个线程:
//1.客户端读取服务器端信息的线程
class ClientReadServer implements Runnable{
private Socket socket;
public ClientReadServer(Socket socket){
this.socket=socket;
}
@Override
public void run() {
//1.获取服务器端输入流
try {
Scanner scanner=new Scanner(socket.getInputStream());
while(scanner.hasNext()){
System.out.println(scanner.next());
}
scanner.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//2.客户端向服务器端发送信息的线程
class ClientSendServer implements Runnable{
private Socket socket;
public ClientSendServer(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
//1.获取服务器端的输出流
PrintStream printStream=new PrintStream(socket.getOutputStream());
//2.从键盘中输入信息
Scanner scanner=new Scanner(System.in);
while(true){
String msg=null;
if(scanner.hasNext()){
msg=scanner.next();
printStream.println(msg);
}
if(msg.equals("exit")){
scanner.close();
printStream.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class MultiClient {
public static void main(String[] args) throws IOException{
//1.客户端连接服务器端,返回套接字Socket对象
Socket socket=new Socket("127.0.0.1",6666);
//2.创建读取服务器端信息的线程和发送服务器端信息的线程
Thread read=new Thread(new ClientReadServer(socket));
Thread send=new Thread(new ClientSendServer(socket));
//3.启动线程
read.start();
send.start();
}
}
客户端输入格式为userName:用户名时,服务器端识别到客户端需要进行的是注册用户操作后,将客户端输入的用户名保存起来,并将该客户端的用户名作为ConcurrentHashMap的key值,客户端的Socket对象作为ConcurrentHashMap的value值,将其添加到全局变量的ConcurrentHashMap对象中。
/**
* 注册用户信息
* @param userName 用户名
* @param socket 用户客户端Socket对象
*/
private void userRegist(String userName,Socket socket){
map.put(userName,socket);
System.out.println("[用户名为"+userName+"][客户端为"+socket+"]上线了!");
System.out.println("当前在线人数为:"+map.size()+"人");
}
客户端输入格式为输入信息中包含exit字符串时,服务器端识别到客户端需要进行注销用户操作后,通过遍历ConcurrentHashMap对象的key值,key值所对应的value值与当前客户端的Socket对象相等时,则找到了需要注销的客户端的用户名,从而利用其key值将客户端从ConcurrentHashMap对象中删除。
/**
* 用户退出
* @param socket
*/
private void userExit(Socket socket){
//1.利用socket取得对应的Key值
String userName=null;
for(String key:map.keySet()){
if(map.get(key).equals(socket)){
userName=key;
break;
}
}
//2.将userName,Socket元素从map集合中删除
map.remove(userName,socket);
//3.提醒服务器该客户端已下线
System.out.println("用户:"+userName+"已下线!");
}
客户端输入格式为P:userName - 私聊信息时,服务器端识别到客户端需要进行的私聊操作,服务器端通过遍历ConcurrentHashMap对象的key值,当找到与userName相等的key值时,利用get()方法取得userName对应的需要私聊的客户端Socket对象,从而将获取私聊的客户端的输出流并将私聊信息进行输出。但是为了客户体验,私聊的客户端需要知道是哪个用户对自己进行私聊操作,故可遍历ConcurrentHashMap对象的value值获取当前客户端的用户名给私聊客户端。
/**
* 私聊流程(利用userName取得客户端的Socket对象,从而取得对应输出流,将私聊信息发送到指定客户端)
* @param socket 当前客户端
* @param userName 私聊的用户名
* @param msg 私聊的信息
*/
private void privateChat(Socket socket,String userName,String msg) throws IOException {
//1.取得当前客户端的用户名
String curUser=null;
Set> set=map.entrySet();
for(Map.Entry entry:set){
if(entry.getValue().equals(socket)){
curUser=entry.getKey();
break;
}
}
//2.取得私聊用户名对应的客户端
Socket client=map.get(userName);
//3.获取私聊客户端的输出流,将私聊信息发送到指定客户端
PrintStream printStream=new PrintStream(client.getOutputStream());
printStream.println(curUser+"私聊说:"+msg);
}
客户端输入格式为G:群聊信息时,服务器端识别到客户端需要进行的群聊操作,服务器端通过遍历ConcurrentHashMap对象的value值,获取客户端的Socket对象从而利用Socket对象取得输出流后将群聊信息发送给每一个客户端。同样为了客户体验,其他客户端需要知道是谁发送了群聊信息,故当前客户端在发送群聊信息的同时告诉其他客户端自己的用户名。
/**
* 群聊流程(将Map集合转换为Set集合,从而取得每个客户端Socket,将群聊信息发送给每个客户端)
* @param socket 发出群聊的客户端
* @param msg 群聊信息
*/
private void groupChat(Socket socket,String msg) throws IOException {
//1.将Map集合转换为Set集合
Set> set=map.entrySet();
//2.遍历Set集合找到发起群聊信息的用户
String userName=null;
for(Map.Entry entry:set){
if(entry.getValue().equals(socket)){
userName=entry.getKey();
break;
}
}
//3.遍历Set集合将群聊信息发给每一个客户端
for(Map.Entry entry:set){
//取得客户端的Socket对象
Socket client=entry.getValue();
//取得client客户端的输出流
PrintStream printStream=new PrintStream(client.getOutputStream());
printStream.println(userName+"群聊说:"+msg);
}
}
对于统计聊天室在线人数功能,用于服务器端将每一个注册用户名的客户端保存在ConcurrentHashMap对象中,故若统计聊天室在线人数只需调用ConcurrentHashMap对象的size()方法即可。该功能模块在客户端用户注册时已经实现,故不再单独实现。
在完成了所有的功能模块之后,首先需要创建服务器端的套接字ServerSocket对象,从而客户端能够进行连接;其次由于服务器端处理多个客户端时需要创建多个线程,故可创建固定大小的线程池处理多个客户端。具体代码的实现如下:
public class MultiServer {
public static void main(String[] args){
try {
//1.创建服务器端的ServerSocket对象,等待客户端连接
ServerSocket serverSocket=new ServerSocket(6666);
//2.创建线程池,从而可以处理多个客户端
ExecutorService executorService= Executors.newFixedThreadPool(20);
for(int i=0;i<20;i++){
System.out.println("欢迎来到我的聊天室......");
//3.侦听客户端
Socket socket=serverSocket.accept();
System.out.println("有新的朋友加入.....");
//4.启动线程
executorService.execute(new Server(socket));
}
//5.关闭线程池
executorService.shutdown();
//6.关闭服务器
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上就是基于多线程的Java聊天室!
下面将所有的代码进行整理,代码如下:
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
//1.客户端读取服务器端信息的线程
class ClientReadServer implements Runnable{
private Socket socket;
public ClientReadServer(Socket socket){
this.socket=socket;
}
@Override
public void run() {
//1.获取服务器端输入流
try {
Scanner scanner=new Scanner(socket.getInputStream());
while(scanner.hasNext()){
System.out.println(scanner.next());
}
scanner.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//2.客户端向服务器端发送信息的线程
class ClientSendServer implements Runnable{
private Socket socket;
public ClientSendServer(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
//1.获取服务器端的输出流
PrintStream printStream=new PrintStream(socket.getOutputStream());
//2.从键盘中输入信息
Scanner scanner=new Scanner(System.in);
while(true){
String msg=null;
if(scanner.hasNext()){
msg=scanner.next();
printStream.println(msg);
}
if(msg.equals("exit")){
scanner.close();
printStream.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class MultiClient {
public static void main(String[] args) throws IOException{
//1.客户端连接服务器端,返回套接字Socket对象
Socket socket=new Socket("127.0.0.1",6666);
//2.创建读取服务器端信息的线程和发送服务器端信息的线程
Thread read=new Thread(new ClientReadServer(socket));
Thread send=new Thread(new ClientSendServer(socket));
//3.启动线程
read.start();
send.start();
}
}
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Server implements Runnable{
private static Map map=new ConcurrentHashMap<>();
private Socket socket;
public Server(Socket socket){
this.socket=socket;
}
@Override
public void run() {
//1.获取客户端的输入流
try {
Scanner scanner=new Scanner(socket.getInputStream());
String msg=null;
while(true){
if(scanner.hasNextLine()){
//0.处理客户端输入的字符串
msg=scanner.nextLine();
Pattern pattern=Pattern.compile("\r");
Matcher matcher=pattern.matcher(msg);
msg=matcher.replaceAll("");
//1.注册用户流程,注册用户的格式为:userName:用户名
if(msg.startsWith("userName:")){
//将用户名保存在userName中
String userName=msg.split("\\:")[1];
//注册该用户
userRegist(userName,socket);
continue;
}
//2.群聊信息流程,群聊的格式为:G:群聊信息
else if(msg.startsWith("G:")){
//必须先注册才可以!
firstStep(socket);
//保存群聊信息
String str=msg.split("\\:")[1];
//发送群聊信息
groupChat(socket,str);
continue;
}
//3.私聊信息流程,私聊的格式为:P:userName-私聊信息
else if(msg.startsWith("P:")&&msg.contains("-")){
//必须先注册才可以!
firstStep(socket);
//保存需要私聊的用户名
String userName=msg.split("\\:")[1].split("-")[0];
//保存私聊的信息
String str=msg.split("\\:")[1].split("-")[1];
//发送私聊信息
privateChat(socket,userName,str);
continue;
}
//4.用户退出流程,用户退出格式为:包含exit
else if(msg.contains("exit")){
//必须先注册才可以!
firstStep(socket);
userExit(socket);
continue;
}
//其他输入格式均错误
else{
PrintStream printStream=new PrintStream(socket.getOutputStream());
printStream.println("输入格式错误!请按照以下格式输入!");
printStream.println("注册用户格式:[userName:用户名]");
printStream.println("群聊格式:[G:群聊信息]");
printStream.println("私聊格式:[P:userName-私聊信息]");
printStream.println("用户退出格式[包含exit即可]");
continue;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 第一步必须先注册!
* @param socket 当前客户端
*/
private void firstStep(Socket socket) throws IOException {
Set> set=map.entrySet();
for(Map.Entry entry:set){
if(entry.getValue().equals(socket)){
if(entry.getKey()==null){
PrintStream printStream=new PrintStream(socket.getOutputStream());
printStream.println("请先进行注册操作!");
printStream.println("注册格式为:[userName:用户名]");
}
}
}
}
/**
* 注册用户信息
* @param userName 用户名
* @param socket 用户客户端Socket对象
*/
private void userRegist(String userName,Socket socket){
map.put(userName,socket);
System.out.println("[用户名为"+userName+"][客户端为"+socket+"]上线了!");
System.out.println("当前在线人数为:"+map.size()+"人");
}
/**
* 群聊流程(将Map集合转换为Set集合,从而取得每个客户端Socket,将群聊信息发送给每个客户端)
* @param socket 发出群聊的客户端
* @param msg 群聊信息
*/
private void groupChat(Socket socket,String msg) throws IOException {
//1.将Map集合转换为Set集合
Set> set=map.entrySet();
//2.遍历Set集合找到发起群聊信息的用户
String userName=null;
for(Map.Entry entry:set){
if(entry.getValue().equals(socket)){
userName=entry.getKey();
break;
}
}
//3.遍历Set集合将群聊信息发给每一个客户端
for(Map.Entry entry:set){
//取得客户端的Socket对象
Socket client=entry.getValue();
//取得client客户端的输出流
PrintStream printStream=new PrintStream(client.getOutputStream());
printStream.println(userName+"群聊说:"+msg);
}
}
/**
* 私聊流程(利用userName取得客户端的Socket对象,从而取得对应输出流,将私聊信息发送到指定客户端)
* @param socket 当前客户端
* @param userName 私聊的用户名
* @param msg 私聊的信息
*/
private void privateChat(Socket socket,String userName,String msg) throws IOException {
//1.取得当前客户端的用户名
String curUser=null;
Set> set=map.entrySet();
for(Map.Entry entry:set){
if(entry.getValue().equals(socket)){
curUser=entry.getKey();
break;
}
}
//2.取得私聊用户名对应的客户端
Socket client=map.get(userName);
//3.获取私聊客户端的输出流,将私聊信息发送到指定客户端
PrintStream printStream=new PrintStream(client.getOutputStream());
printStream.println(curUser+"私聊说:"+msg);
}
/**
* 用户退出
* @param socket
*/
private void userExit(Socket socket){
//1.利用socket取得对应的Key值
String userName=null;
for(String key:map.keySet()){
if(map.get(key).equals(socket)){
userName=key;
break;
}
}
//2.将userName,Socket元素从map集合中删除
map.remove(userName,socket);
//3.提醒服务器该客户端已下线
System.out.println("用户:"+userName+"已下线!");
}
}
public class MultiServer {
public static void main(String[] args){
try {
//1.创建服务器端的ServerSocket对象,等待客户端连接
ServerSocket serverSocket=new ServerSocket(6666);
//2.创建线程池,从而可以处理多个客户端
ExecutorService executorService= Executors.newFixedThreadPool(20);
for(int i=0;i<20;i++){
System.out.println("欢迎来到我的聊天室......");
//3.侦听客户端
Socket socket=serverSocket.accept();
System.out.println("有新的朋友加入.....");
//4.启动线程
executorService.execute(new Server(socket));
}
//5.关闭线程池
executorService.shutdown();
//6.关闭服务器
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}