我们想实现聊天室怎么做呢?
首先我们知道,聊天是客户端对客户端之间,而他们之间需要一个服务器作为中介,上篇文章我们实现了点对点传输socket网络编程-UDP辅助TCP实现点对点传输,在服务器端我们直接打印数据的
@Override
public void run() {
super.run();
try {
// 得到输入流,用于接收数据
BufferedReader socketInput = new BufferedReader(new InputStreamReader(inputStream));
do {
// 客户端拿到一条数据
String str = socketInput.readLine();
if (str == null) {
System.out.println("客户端已无法读取数据!");
// 退出当前客户端
ClientHandler.this.exitBySelf();
break;
}
// 打印到屏幕
System.out.println(str);
} while (!done);
} catch (Exception e) {
if (!done) {
System.out.println("连接异常断开");
ClientHandler.this.exitBySelf();
}
} finally {
// 连接关闭
CloseUtils.close(inputStream);
}
}
思路:修改这行代码,让其数据回掉到之前的TCPServer中
群聊的实现
- ClientHandler的修改,新增一个接口回掉到TCPServer,接口名字onNewMessageArrived
public class ClientHandler {
private final Socket socket;
private final ClientReadHandler readHandler;
private final ClientWriteHandler writeHandler;
private final ClientHandlerCallback clientHandlerCallback;
private final String clientInfo;
public ClientHandler(Socket socket, ClientHandlerCallback closeNotify) throws IOException {
this.socket = socket;
this.readHandler = new ClientReadHandler(socket.getInputStream());
this.writeHandler = new ClientWriteHandler(socket.getOutputStream());
this.clientHandlerCallback = closeNotify;
clientInfo = "IP地址:" + socket.getInetAddress() +
" P:" + socket.getPort();
System.out.println("新客户端连接:" + clientInfo);
}
public String getClientInfo() {
return clientInfo;
}
public void exit() {
readHandler.exit();
writeHandler.exit();
CloseUtils.close(socket);
System.out.println("客户端已退出:" + socket.getInetAddress() +
" P:" + socket.getPort());
}
public void send(String str) {
writeHandler.send(str);
}
public void readToPrint() {
readHandler.start();
}
private void exitBySelf() {
exit();
clientHandlerCallback.onSelfClosed(this);
}
public interface ClientHandlerCallback {
void onSelfClosed(ClientHandler handler);
//收到消息通知
void onNewMessageArrived(ClientHandler handler, String msg);
}
class ClientReadHandler extends Thread {
private boolean done = false;
private final InputStream inputStream;
ClientReadHandler(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
super.run();
try {
// 得到输入流,用于接收数据
BufferedReader socketInput = new BufferedReader(new InputStreamReader(inputStream));
do {
// 客户端拿到一条数据
String str = socketInput.readLine();
if (str == null) {
System.out.println("客户端已无法读取数据!");
// 退出当前客户端
ClientHandler.this.exitBySelf();
break;
}
// 打印到屏幕
//System.out.println(str);
clientHandlerCallback.onNewMessageArrived(ClientHandler.this, str);
} while (!done);
} catch (Exception e) {
if (!done) {
System.out.println("连接异常断开");
ClientHandler.this.exitBySelf();
}
} finally {
// 连接关闭
CloseUtils.close(inputStream);
}
}
void exit() {
done = true;
CloseUtils.close(inputStream);
}
}
class ClientWriteHandler {
private boolean done = false;
private final PrintStream printStream;
private final ExecutorService executorService;
ClientWriteHandler(OutputStream outputStream) {
this.printStream = new PrintStream(outputStream);
this.executorService = Executors.newSingleThreadExecutor();
}
void exit() {
done = true;
CloseUtils.close(printStream);
executorService.shutdownNow();
}
void send(String str) {
if(done){
return;
}
executorService.execute(new WriteRunnable(str));
}
class WriteRunnable implements Runnable {
private final String msg;
WriteRunnable(String msg) {
this.msg = msg;
}
@Override
public void run() {
if (ClientWriteHandler.this.done) {
return;
}
try {
ClientWriteHandler.this.printStream.println(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
ClientHandler是服务器的内部的实现,所以需要回到服务器TCPServer中
- 修改TCPServer代码:因为方便其他人观看,所以我都是直接拷贝
思路:添加回掉,但是接受消息不能阻塞线程,所以,我们需要开个线程,我们对所有客户端进行遍历,然后排出自己,对其他客户端发送消息
public class TCPServer implements ClientHandler.ClientHandlerCallback {
private final int port;
private ClientListener mListener;
private List clientHandlerList = new ArrayList<>();
private final ExecutorService forwardingThreadExecutor;
public TCPServer(int port) {
this.port = port;
this.forwardingThreadExecutor = Executors.newSingleThreadExecutor();
}
public boolean start() {
try {
ClientListener listener = new ClientListener(port);
mListener = listener;
listener.start();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
public void stop() {
if (mListener != null) {
mListener.exit();
}
synchronized (TCPServer.this) {
for (ClientHandler clientHandler : clientHandlerList) {
clientHandler.exit();
}
clientHandlerList.clear();
}
forwardingThreadExecutor.shutdownNow();
}
public void broadcast(String str) {
synchronized (TCPServer.this) {
for (ClientHandler clientHandler : clientHandlerList) {
clientHandler.send(str);
}
}
}
@Override
public synchronized void onSelfClosed(ClientHandler handler) {
clientHandlerList.remove(handler);
}
@Override
public void onNewMessageArrived(final ClientHandler handler, final String msg) {
//消息打印
System.out.println("Received:" + handler.getClientInfo() + ":" + msg);
//不能阻塞,异步处理
//异步提交
forwardingThreadExecutor.execute(new Runnable() {
@Override
public void run() {
synchronized (TCPServer.this) {
for (ClientHandler clientHandler : clientHandlerList) {
//不能发给自己
if (clientHandler.equals(handler)) {
continue;
}
//对其他客户端发送消息
clientHandler.send(msg);
}
}
}
});
}
private class ClientListener extends Thread {
private ServerSocket server;
private boolean done = false;
private ClientListener(int port) throws IOException {
server = new ServerSocket(port);
System.out.println("服务器信息:" + server.getInetAddress() + " P:" + server.getLocalPort());
}
@Override
public void run() {
super.run();
System.out.println("服务器准备就绪~");
// 等待客户端连接
do {
// 得到客户端
Socket client;
try {
client = server.accept();
} catch (IOException e) {
continue;
}
try {
// 客户端构建异步线程
ClientHandler clientHandler = new ClientHandler(client, TCPServer.this);
// 读取数据并打印
clientHandler.readToPrint();
//添加同步
synchronized (TCPServer.this) {
clientHandlerList.add(clientHandler);
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端连接异常:" + e.getMessage());
}
} while (!done);
System.out.println("服务器已关闭!");
}
void exit() {
done = true;
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务器状态与测试用例构建
状态:繁忙
- 每个客户端都需要服务器进行双通等待
- 客户端双通:客户端发送数据到服务器的接受通道
- 服务器双通:服务器回送消息的发送通道
- 每条通道因为阻塞只能异步线程实现
服务器线程数量
- 一个客户端:双通需要两个线程
- n个客户端:2n条线程
- 服务器实际数量:2n+
修改客户端
之前的客户端并不能控制内部TCPClient
- client代码
public class Client {
public static void main(String[] args) {
ServerInfo info = UDPSearcher.searchServer(10000);
System.out.println("Server:" + info);
if (info != null) {
TCPClient tcpClient = null;
try {
tcpClient = TCPClient.startWith(info);
if (tcpClient == null) {
return;
}
write(tcpClient);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (tcpClient != null) {
tcpClient.exit();
}
}
}
}
private static void write(TCPClient client) throws IOException {
// 构建键盘输入流
InputStream in = System.in;
BufferedReader input = new BufferedReader(new InputStreamReader(in));
do {
// 键盘读取一行
String str = input.readLine();
// 发送到服务器
client.send(str);
if ("bye".equalsIgnoreCase(str)) {
break;
}
} while (true);
}
}
- 修改TCPClient:主要让其可控
public class TCPClient {
private final Socket socket;
private final ReadHandler readHandler;
private final PrintStream printStream;
public TCPClient(Socket socket, ReadHandler readHandler) throws IOException {
this.socket = socket;
this.readHandler = readHandler;
this.printStream = new PrintStream(socket.getOutputStream());
}
public void exit() {
readHandler.exit();
CloseUtils.close(printStream);
CloseUtils.close(socket);
}
public void send(String msg) {
printStream.println(msg);
}
public static TCPClient startWith(ServerInfo info) throws IOException {
Socket socket = new Socket();
// 超时时间
socket.setSoTimeout(3000);
// 连接本地,端口2000;超时时间3000ms
socket.connect(new InetSocketAddress(Inet4Address.getByName(info.getAddress()), info.getPort()), 3000);
System.out.println("已发起服务器连接,并进入后续流程~");
System.out.println("客户端信息:" + socket.getLocalAddress() + " 端口号:" + socket.getLocalPort());
System.out.println("服务器信息:" + socket.getInetAddress() + " 端口号:" + socket.getPort());
try {
ReadHandler readHandler = new ReadHandler(socket.getInputStream());
readHandler.start();
return new TCPClient(socket, readHandler);
} catch (Exception e) {
System.out.println("连接异常");
CloseUtils.close(socket);
return null;
}
}
static class ReadHandler extends Thread {
private boolean done = false;
private final InputStream inputStream;
ReadHandler(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
super.run();
try {
// 得到输入流,用于接收数据
BufferedReader socketInput = new BufferedReader(new InputStreamReader(inputStream));
do {
String str;
try {
// 客户端拿到一条数据
str = socketInput.readLine();
} catch (SocketTimeoutException e) {
continue;
}
if (str == null) {
System.out.println("连接已关闭,无法读取数据!");
break;
}
// 打印到屏幕
System.out.println(str);
} while (!done);
} catch (Exception e) {
if (!done) {
System.out.println("连接异常断开:" + e.getMessage());
}
} finally {
// 连接关闭
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
void exit() {
done = true;
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
测试用例
public class ClientTest {
private static boolean done=false;
public static void main(String[] args) throws IOException {
ServerInfo info = UDPSearcher.searchServer(10000);
System.out.println("Server:" + info);
if (info == null) return;
//当前连接的数量
int size = 0;
final List tcpClientList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
try {
TCPClient tcpClient = TCPClient.startWith(info);
if (tcpClient == null) {
System.out.println("连接异常");
continue;
}
tcpClientList.add(tcpClient);
System.out.println("连接成功:" + (++size));
} catch (IOException e) {
e.printStackTrace();
System.out.println("连接异常");
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.in.read();
Runnable runnable = new Runnable() {
@Override
public void run() {
while (!done) {
for (TCPClient tcpClient : tcpClientList) {
tcpClient.send("Hello Peakmain!!!!!");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
System.in.read();
done=true;
try {
//等待线程结束
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (TCPClient tcpClient : tcpClientList) {
tcpClient.exit();
}
}
}
}
CPU:取决于数据的频繁性,数据的转发复杂性
内存:取决于客户端的数量,客户发送的数据的大小
线程:取决于连接的客户端数量
服务器优化方案分析
- 减少线程数量
- 增加线程执行繁忙状态
- 客户端buffer复用机制