一、项目简介
简单实现多个客户端之间的通信功能。
二、项目功能
- 用户上线,通知服务器注册并统计群聊人数;
- 所有用户可以进行群聊,以及可以指定用户进行私聊;
- 用户下线,通知服务器,服务器更新信息。
三、具体设计
首先,聊天室的项目是建立在Socket编程基础上,Socket编程的操作流程如下:
1.先建立连接,直到目标服务器的IP地址与端口号
2.获取本连接的输入输出流
3.通过IO进行数据的读取与写入
4.关闭流
在此项目中需要有Socket类,一个是服务器ServerSocket,另一个是客户端Socket。
服务器操作流程:
1.建立服务端Socket,等待客户连接,通过构造方法实现-建立基站
ServetSocket server = new ServerSocket(port);
2. 等待客户端的连接
Socket cilent = server.accept(); //一直阻塞直到有客户端连接,返回客户端Socket
3.取得输入、输出流进行通信(此方法是由客户端提供)
//取得输入输出流
readFromCilent = new Scanner(cilent.getInputStream());
writeWsgToCilent = new PrintStream(cilent.getOutputStream());
4.关闭流,将基站关闭
server.close();
readFromCilent.close();
writeWsgToCilent.close();
客户端操作流程:
1.连接到指定的服务器,通过构造方法实现
cilent = new Socket("127.0.0.1",6666);
2. 取得输入输出流
//取得输入输出流
readFromServer = new Scanner(cilent.getInputStream());
writeWsgToServer = new PrintStream(cilent.getOutputStream(),true,"UTF-8");
3.关闭流
cilent.close();
readFromServer.close();
writeWsgToServer.close();
服务器:
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = null;
Scanner readFromCilent = null;
PrintStream writeWsgToCilent = null;
try {
server = new ServerSocket(6666);
//等待客户端的连接
System.out.println("等待客户端的连接");
Socket cilent = server.accept();
System.out.println("新的客户端的端口号为:"+cilent.getPort());
//取得输入输出流
readFromCilent = new Scanner(cilent.getInputStream());
writeWsgToCilent = new PrintStream(cilent.getOutputStream());
System.out.print("客户端发送的消息为:");
if(readFromCilent.hasNext()){
System.out.println(readFromCilent.nextLine());
}
readFromCilent.useDelimiter("\n");
} catch (IOException e) {
e.printStackTrace();
} finally{
server.close();
readFromCilent.close();
writeWsgToCilent.close();
}
}
}
客户端:
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws IOException {
Socket cilent = null;
Scanner readFromServer = null;
PrintStream writeWsgToServer = null;
try {
//与服务端建立连接
cilent = new Socket("127.0.0.1",6666);
//取得输入输出流
readFromServer = new Scanner(cilent.getInputStream());
writeWsgToServer = new PrintStream(cilent.getOutputStream(),true,"UTF-8");
//向服务端发送消息
writeWsgToServer.println("I am Cilent");
readFromServer.useDelimiter("\n");
System.out.print("服务端发送的消息为:");
if(readFromServer.hasNext()){
System.out.println(readFromServer.nextLine());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
cilent.close();
readFromServer.close();
writeWsgToServer.close();
}
}
}
以上为单线程通信实现,如果当服务器和客户端同时进行对方的读取,就会产生问题。
单线程通信问题:
1.容易造成双方卡死的现象--类比电话占线
2.发送一次数据后服务器与客户端均退出,不能持久通信
3.不能同时进行数据的读取与写入,因为是顺序操作
解决--进行读写分离,作为两个不相关的线程
4.服务器只能处理一个客户端的连接
解决--每当有一个客户端的连接进来,就创建一个线程,处理此客户端请求
多线程通信:
客户端:读写分离,读作为一个线程,写作为一个线程
服务端:存储所有连接的客户端,使用ConcurrentHashMap来存储所有注册的客户端信息
private static Map<String,Socket> clientMap = new ConcurrentHashMap<>();
用户注册:
userName:zhangsan
群聊实现:
G:hello i...
私聊实现:
P:zhangsan-hello i am
退出:
zhangsan:byebye
服务器:
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collection;
import java.util.Map;
import java.util.Scanner;
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;
public class ThreadServer {
//使用ConcurrentHashMap来存储所有注册的客户端信息
private static Map<String,Socket> clientMap = new ConcurrentHashMap<>();
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(6666);
ExecutorService service = Executors.newFixedThreadPool(20);
for(int i=0; i<20; i++){
System.out.println("等待客户端连接...");
Socket client = serverSocket.accept();
System.out.println("有新的客户端连接,端口号为:"+client.getPort());
service.submit(new ExecuteClientRequest(client));
}
}
//具体处理与每个客户端通信的内部类
static class ExecuteClientRequest implements Runnable{
private Socket client;
public ExecuteClientRequest(Socket client) {
this.client = client;
}
@Override
public void run() {
//获取输入流,不断地读取用户发来的信息
Scanner readFromClient = null;
try {
readFromClient = new Scanner(client.getInputStream());
readFromClient.useDelimiter("\n");
while(true){
if(readFromClient.hasNextLine()){
String str = readFromClient.nextLine();
//进行\r的过滤流程
//win下进行换行的过滤
Pattern pattern = Pattern.compile("\r");
Matcher matcher = pattern.matcher(str);
str = matcher.replaceAll("");
if(str.startsWith("userName")){
//用户注册流程
//userName:zhangsan
String userName = str.split(":")[1];
userRegister(userName,client);
continue;
} else if(str.startsWith("G")){
//群聊流程
//G:hello i...
String msg = str.split(":")[1];
groupChat(msg);
continue;
} else if(str.startsWith("P")){
//私聊流程
//P:zhangsan-hello i am...
String tempMsg = str.split(":")[1];
String userName = tempMsg.split("-")[0];
String msg = tempMsg.split("-")[1];
privateChat(userName,msg);
} else if(str.contains("byebye")){
//用户退出流程
//zhangsan:byebye
String userName = str.split(":")[0];
userExist(userName);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 用户注册方法
* @param userName 用户名
* @param client 对应的Socket
*/
private void userRegister(String userName,Socket client){
//将用户信息保存到服务器中
clientMap.put(userName,client);
//取到当前注册到服务器的所有人的个数
int size = clientMap.size();
System.out.println("当前聊天室内共有"+size+"人");
String userOnLine = userName+"上线了!";
groupChat(userOnLine);
}
/**
* 群聊流程
* @param msg 要发送的群聊信息
*/
private void groupChat(String msg){
//取出所有连接的客户端,依次拿到输出流进行遍历输出
Collection<Socket> clients = clientMap.values();
for(Socket client : clients){
//取出此客户端的输出流
try {
PrintStream out = new PrintStream(client.getOutputStream(),true,"UTF-8");
out.println("群聊信息为:"+msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 私聊流程
* @param userName 私聊的用户名
* @param msg 私聊的信息
*/
private void privateChat(String userName,String msg){
Socket client = clientMap.get(userName);
try {
PrintStream out = new PrintStream(client.getOutputStream(),true,"UTF-8");
out.println("私聊信息为:"+msg);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 用户退出
* @param userName 退出的用户名
*/
private void userExist(String userName){
clientMap.remove(userName);
System.out.println("当前聊天室人数为:"+clientMap.size());
String groupChatMsg = userName+"已下线";
groupChat(groupChatMsg);
}
}
}
客户端:
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 读取服务器发来信息的线程
*/
class ReadFromServerThread implements Runnable{
private Socket client;
//通过构造方法传入通信的Socket
public ReadFromServerThread(Socket client) {
this.client = client;
}
@Override
public void run() {
Scanner in = null;
try {
//获取客户端的输入流,读取服务器发来的信息
in = new Scanner(client.getInputStream());
//碰到回车在换行
in.useDelimiter("\n");
//不断获取服务器信息
while(true){
if(in.hasNextLine()){
System.out.println("从服务器发来的消息为:"+in.nextLine());
}
//此客户端退出
if(client.isClosed()){
System.out.println("客户端已关闭");
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
in.close();
}
}
}
class WriteToServerThread implements Runnable{
private Socket client;
public WriteToServerThread(Socket client) {
this.client = client;
}
@Override
public void run() {
//获取键盘输入,向服务器发送信息
Scanner scanner = new Scanner(System.in);
scanner.useDelimiter("\n");
PrintStream out = null;
try {
//获取客户端输出流,向服务器发送信息
out = new PrintStream(client.getOutputStream(),true,"UTF-8");
while(true){
System.out.println("请输入要发送的消息...");
String strToServer;
if(scanner.hasNextLine()){
strToServer = scanner.nextLine();
out.println(strToServer);
//客户端退出标志
if(strToServer.contains("byebye")){
System.out.println("关闭客户端");
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
scanner.close();
out.close();
}
}
}
public class ThreadClient {
public static void main(String[] args) {
try {
//建立与服务器的连接
Socket client = new Socket("127.0.0.1",6666);
//创建读写线程与服务器通信
Thread readFromServer = new Thread(new ReadFromServerThread(client));
Thread writeToServer = new Thread(new WriteToServerThread(client));
readFromServer.start();
writeToServer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}