BIO模型
jdk1.4 之前对 IO 的支持比较简单,都是使用 BIO,就是 Block IO 的模型来进行编码。
上面看到,accept 是阻塞的,那么新来的客户端是怎么处理的?就是新建一个 Handler 的线程去处理,这样就不阻塞了当前的客户端了,所以第二个客户端就可以与服务器端发送消息。
acceptor线程就是专门用来接收客户端发起的连接请求。它不是用来处理输入输出的数据流,只是用accept函数接受发来的请求的。
chat room的UML建模之时序图
多人在线,每个人的发言都被转发给其它在线用户
- ChatHandler 是启动的一个新的线程,首先在这个线程里面添加客户端到客户端集合里面,这个时候客户端输入的是一个阻塞的函数,为了同时能够收到消息,新建一个 UserInputhandler的线程,专门处理用户输入的线程。如果有输入,就发送给服务器端发送消息
- 用户发送 “quit” 消息的时候,这个时候服务器端就知道某个客户端要退出了,首先要做的事情就是先移除客户端列表,然后客户端关闭 socket
- 最外面的 loop 循环的大框,就是服务器的一个主要的循环,一直在等待 accept 函数的执行,如果有就新建一个 Handler 去进行处理
所谓bio,一个client对应一个handler线程。
客户1——>服务端——>客户端2,3,4
服务端需要存储在线用户的集合。
客户端需要两条线程,一个等待控制台输入的消息,一个收到服务器发来的其它用户的信息。
实现服务端:ChatServer
- 不停的监听和接收客户端的消息:while循环、chatHandler线程
- 转发给在线的所有的客户端
- 需要保存所有的在线客户端列表:hashmap存放、端口作为id
- 当用户输入 quit 的时候,这个客户就不想留在服务器端,就需要将它从在线用户列表中移除
ChatServer.java:
构造函数、加入新上线的客户端、转发消息、检查是否退出、关闭、启动
public class ChatServer {
//默认服务器监听的端口
private int DEFAULT_PORT = 888;
private final String QUIT = "quit";
private ServerSocket serverSocket;
// 连接的客户端列表,value 是需要写消息的 writer
private Map<Integer, Writer> connectedClients;
public ChatServer() {
connectedClients = new HashMap();
}
// 添加客户端
public synchronized void addClient(Socket socket) throws IOException {
if(null != socket) {
int port = socket.getPort();
OutputStream out = scoket.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
connectedClients.put(port, writer); // 线程安全问题,用synchronized关键字解决
System.out.println("客户端[" + port + "已经连接到服务器");
}
}
// 移除客户端,还需要关闭
public synchronized void removeClient(Socket socket) throws IOException {
if(null != socket) {
int port = socket.getPort();
//找到socket最外层的writer删除,等于删除这个socket
if(connectedClients.containsKey(port)) {
Writer writer = connectedClients.get(port);
writer.close();
}
connectedClients.remove(port);
System.out.println("客户端[" + port + "已经断开连接");
}
}
// 转发消息给所有的客户端
public synchronized void forwardMessage(Socket socket, String fwdMsg)
throws IOException {
for(Map.Entry<Integer, Writer> entry: connectedClients.entrySet()) {
int port = entry.getKey();
//排除掉发送者:
if((socket.getPort != port)) {
Writer writer = entry.getValue();
writer.writer(fwdMsg);
writer.flush();
}
}
}
// 检查是否退出
public boolean readToQuit(String msg) {
return QUIT.equals(msg);
}
// 关闭
public synchronized void close() {
if(null != serverSocket) {
try {
serverSocket.close();
}catch(Exception e) {
e.printStackTrace();
}
}
}
// 启动逻辑
public void start() {
try {
// 绑定监听端口
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("启动服务器,监听端口" + DEFAULT_PORT + "...");
while(true) {
// 等待客户端的连接
Socket socket = serverSocket.accept();
// 创建 ChatHandler线程
new Thread(new ChatHandler(this, socket)).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
close();
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
chatServer.start();
}
}
实现监听器:ChatHandler
一个handler对象为一个客户端提供服务,里面只有一个run方法,由server开辟该线程。
- 需要 ChatServer 的对象:很多的操作都是依赖于 ChatServer 的connectedClients对象
- 需要客户端的 Socket 对象
- 前面的ChatServer 代码中添加readToQuit的方法代码
- 最后需要注意线程安全问题,需要注意一下
ChatHandler.java
public class ChatHandler implements Runable{
private ChatServer server;
private Socket socket;
public ChatHandler(ChatServer server, Socket socket) {
this. server = server;
this.socket = socket;
}
@Ovverride
public void run() {
try {
// 存储新上线用户
server.addClient(socket);
// 读取用户发送的消息
BufferedReader reader =
new BufferedReader(new InputStreamReader(socket.getInputStream()))
// readLine是一直阻塞的,除非收到换行符就会读取
String msg = null;
while ((msg = reader.readLine()) != null) {
String fwdMsg = "客户端[" + socket.getPort + "]:" + msg + "\n";
System.out.println(fwdMsg );
// 将消息转发给聊天室在线的其他用户
server.forwadMessge(socket, fwdMsg);
// 检查是否准备退出
if(server.readToQuit()) {
break; // 如果收到退出命令,那么就直接退出
}
}
}catch(IOException e) {
e.printStachTrace();
} finall{
try {
//把已经不需要继续进行通信活动的客户从列表删除。
//如果不移除,会发给已经下线的客户端。
server.removeClient(socket);
}catch(Exception e) {
e.printStachTrace();
}
}
}
}
实现客户端:ChatClient
先完成主线程:将消息发送给服务器,接收服务器返回的消息
ChatClient.java
public class ChatClient {
private String DEFAULT_SERVER_HOST = "127.0.0.1";
private int DEFAULT_SERVER_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
public ChatClinet() {
}
// 发送消息给服务器
public void send(String msg) throws IOException {
if(!socket.isOuputShutdown()) {
writer.write(msg + "\n");
writer.flush();
}
}
// 接收消息从服务器
public String receive() {
String msg = null;
if(!socket.IsInputShutdown()){
try{
msg = reader.readLine();
}catch(IOException e){
e.printStackTrace();
}
}
return msg;
}
// 检查是否退出
public boolean readyToQuit(String msg) {
return QUIT.equals(msg);
}
public void close() {
if(null != writer) {
try {
wirter.close();
} catch (Exceptio e) {
e.printStackTrace();
}
}
}
public void start() {
try {
// 创建 Socket
socket = new Socket( DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
// 创建 IO 流
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new BufferedWriter(new OutStreamWriter(socket.getOutputStream));
// 处理用户的输入(需要额外开线程来进行处理)
new Thread(new UserInputHanderl(this)).start();
// 时刻监听是否有从服务器其他用户发过来的消息
String msg = null;
while ((msg = receive()) != null) {
System.out.println(msg);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
close();
}
}
public static void main(String[] args) {
ChatClient chatClient = new ChatClient();
chatClient.start();
}
}