Java网络编程-Socket编程初涉二(基于BIO模型的简易多人聊天室)
要求
我们要实现一个基于BIO模型的简易多人聊天室,不能像上一个版本一样(Java网络编程-Socket编程初涉一(简易客户端-服务器)),当服务器与某个客户端连接成功后,它们在进行数据交互过程中,其他客户端的连接请求,服务器是不会响应的,我们这里要使用BIO模型来改善这一点。
所谓多人聊天室,就是某个用户发送的消息,其他用户也是可以看见的。
什么是BIO模型?
多人聊天室的时序图
时序图如下图所示,先不纠结时序图是否标准,等下解释代码时,可以与时序图结合起来进行理解。
代码
服务器代码
先给出全部代码,然后分模块进行解释。
ChatServer类
是多人聊天室的服务器部分。
package bio.chatroom.server;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class ChatServer {
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private ServerSocket serverSocket;
// 把客户端的port当作客户端的id
private Map<Integer , Writer> connectedClients;
public ChatServer(){
connectedClients = new HashMap<>();
}
public synchronized void addClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
connectedClients.put(port , writer);
System.out.println("客户端["+port+"]已连接到服务器");
}
}
public synchronized void removeClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
if(connectedClients.containsKey(port)){
connectedClients.get(port).close();
connectedClients.remove(port);
System.out.println("客户端["+port+"]已断开连接");
}
}
}
public synchronized void forwardMessage(Socket socket , String fwdMsg) throws IOException {
// 发送消息的端口
int sendMessagePort = socket.getPort();
for(Integer port : connectedClients.keySet()){
if(!port.equals(sendMessagePort)){
Writer writer = connectedClients.get(port);
writer.write(fwdMsg);
writer.flush();
}
}
}
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public synchronized void close(){
if(serverSocket != null){
try {
serverSocket.close();
System.out.println("关闭了ServerSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void start(){
try {
// 创建ServerSocket,绑定和监听端口
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 (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
public static void main(String[] args) {
ChatServer server = new ChatServer();
server.start();
}
}
模块一
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private ServerSocket serverSocket;
// 把客户端的port当作客户端的id
private Map<Integer , Writer> connectedClients;
public ChatServer(){
connectedClients = new HashMap<>();
}
DEFAULT_PORT
是服务器创建的ServerSocket
需要绑定、监听的端口,QUIT
是用于判断用户是否准备退出,connectedClients
相当于存储在线用户的容器(实际上并没有存储用户),而是以用户的port
为key
,以服务器向该port
的用户发送消息的Writer
为value
,组成的Map
,这样方便服务器转发某个用户的消息给其他用户。
构造器ChatServer()
要初始化connectedClients
,因为服务器启动后,就可能马上需要存储上线的用户。
模块二
public synchronized void addClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
connectedClients.put(port , writer);
System.out.println("客户端["+port+"]已连接到服务器");
}
}
public synchronized void removeClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
if(connectedClients.containsKey(port)){
connectedClients.get(port).close();
connectedClients.remove(port);
System.out.println("客户端["+port+"]已断开连接");
}
}
}
public synchronized void forwardMessage(Socket socket , String fwdMsg) throws IOException {
// 发送消息的端口
int sendMessagePort = socket.getPort();
for(Integer port : connectedClients.keySet()){
if(!port.equals(sendMessagePort)){
Writer writer = connectedClients.get(port);
writer.write(fwdMsg);
writer.flush();
}
}
}
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public synchronized void close(){
if(serverSocket != null){
try {
serverSocket.close();
System.out.println("关闭了ServerSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
这些方法的作用也正如方法名一样。为了一定的线程安全,有些需要涉及增删改查的方法直接用synchronized
修饰(我们现在不用过分纠结高性能、线程安全等问题)。
addClient()
:存储新上线的用户。removeClient()
:移除退出的用户。forwardMessage()
:把某个用户的消息转发给其他的用户。readyToQuit()
:判断用户是否准备退出。close()
:关闭资源。
具体实现应该比较简单,相信大家都能看懂。
模块三
public void start(){
try {
// 创建ServerSocket,绑定和监听端口
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 (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
public static void main(String[] args) {
ChatServer server = new ChatServer();
server.start();
}
start()
方法是多人聊天室服务器的核心。
其实逻辑也很简单,看过Java网络编程-Socket编程初涉一(简易客户端-服务器)这篇博客的,应该很容易看懂,这里比之前不过是多创建了一个线程,基于BIO模型,新创建一个线程用于与用户的数据交互,主线程依旧是等待其他用户的连接请求,这也很好的改善了上一个版本的弊端。
new Thread(new ChatHandler(this , socket)).start();
接下来看看ChatHandler类
。
当服务器与客户端建立连接后,服务器会创建一个线程,用来与客户端进行数据交互,从而不干扰主线程的任务(等待客户端的连接请求),ChatHandler类
就是实现这个功能的类,所以实现了Runnable接口
,方便用于线程的创建,这里的逻辑也很简单,主要实现以下三个功能:
- 将新上线的用户添加到存储在线用户的容器中。
- 读取用户发送过来的消息,并将消息转发给其他的用户。
- 等用户退出后,将该用户在存储在线用户的容器里移除。
package bio.chatroom.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ChatHandler implements Runnable{
private ChatServer server;
private Socket socket;
public ChatHandler(ChatServer server , Socket socket){
this.server = server;
this.socket = socket;
}
@Override
public void run() {
try {
// 存储新上线用户
server.addClient(socket);
// 读取用户发送的消息
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String msg = null;
while((msg = reader.readLine()) != null){
String fwdMsg = "客户端["+socket.getPort()+"]:"+msg+"\n";
System.out.print(fwdMsg);
// 将消息转发给聊天室里在线的其他用户
server.forwardMessage(socket , fwdMsg);
// 检查用户是否准备退出
if(server.readyToQuit(msg)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
// 从服务器移除退出的用户
server.removeClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务器的代码就全部完成了,其实很简单吧,主要的逻辑就是下面三个:
- 创建ServerSocket,绑定、监听一个端口。
- 服务器主线程阻塞,并且等待用户连接请求。
- 有用户连接后,就创建一个新的线程用于与该用户进行数据交互,主线程依然等待用户连接请求,新开的线程,会将用户添加到存储在线用户的容器中,并且一直读取用户发送过来的消息,再将该消息转发给其他用户,直到该用户退出,当该用户退出时,就将该用户在存储在线用户容器中移除,再关闭资源。
客户端代码
先给出全部代码,然后分模块进行解释。
ChatClient类
是多人聊天室的客户端部分。
package bio.chatroom.client;
import java.io.*;
import java.net.Socket;
public class ChatClient {
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
// 发送消息给服务器
public void send(String msg) throws IOException {
if(!socket.isOutputShutdown()){
writer.write(msg+"\n");
writer.flush();
}
}
// 接收服务器的消息
public String receive() throws IOException {
String msg = null;
if(!socket.isInputShutdown()){
msg = reader.readLine();
}
return msg;
}
// 检查用户是否准备退出
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public void close(){
if(writer != null){
try {
writer.close();
System.out.println("关闭socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void start(){
try {
// 创建socket
socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);
// 创建IO流
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
// 处理用户的输入
new Thread(new UserInputHandler(this)).start();
// 读取服务器转发的消息
String msg = null;
while((msg = receive()) != null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
} finally{
close();
}
}
public static void main(String[] args) {
ChatClient client = new ChatClient();
client.start();
}
}
模块一
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
DEFAULT_SERVER_HOST
是服务器ip
(本地),DEFAULT_PORT
是服务器监听的端口,客户端根据这两个属性可以向服务器发送连接请求。QUIT
的作用和在服务器代码中的作用一样。socket
是客户端与服务器建立连接后创建的Socket
,reader
是用于客户端读取服务器回复的消息,writer
是用于客户端向服务器发送消息。
模块二
// 发送消息给服务器
public void send(String msg) throws IOException {
if(!socket.isOutputShutdown()){
writer.write(msg+"\n");
writer.flush();
}
}
// 接收服务器的消息
public String receive() throws IOException {
String msg = null;
if(!socket.isInputShutdown()){
msg = reader.readLine();
}
return msg;
}
// 检查用户是否准备退出
public boolean readyToQuit(String msg){
return QUIT.equalsIgnoreCase(msg);
}
public void close(){
if(writer != null){
try {
writer.close();
System.out.println("关闭socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
这些方法的作用也正如方法名一样。
send()
:客户端向服务器发送消息。receive()
:客户端接收服务器的消息。readyToQuit()
:判断用户是否准备退出。close()
:关闭资源。
实现也都非常简单,就不多说了。
模块三
public void start(){
try {
// 创建socket
socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);
// 创建IO流
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
// 处理用户的输入
new Thread(new UserInputHandler(this)).start();
// 读取服务器转发的消息
String msg = null;
while((msg = receive()) != null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
} finally{
close();
}
}
public static void main(String[] args) {
ChatClient client = new ChatClient();
client.start();
}
和上一个版本的客户端差不多,也是多创建了一个线程,用于监听用户在控制台的输入,而主线程就用于接收服务器的消息,主要逻辑如下:
- 先与服务器创建连接。
- 创建与服务器交互的IO流。
- 创建新线程,用于监听用户的控制台输入。
- 接收服务器的消息。
UserInputHandler类
,是客户端创建线程用于监听用户的控制台输入,所以它也实现了Runnable接口
,方便创建线程,当它监听到用户在控制台输入信息后,会将用户输入的信息发送给服务器,服务器当然也会将该信息转发给其他的用户。它还会判断用户是否准备退出,当用户退出后,这个线程也就差不多结束了,服务器也会将存储在线用户的容器中移除该用户。
package bio.chatroom.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class UserInputHandler implements Runnable{
private ChatClient client;
public UserInputHandler(ChatClient client){
this.client = client;
}
@Override
public void run() {
try {
// 等待用户输入消息
BufferedReader consoleReader = new BufferedReader(
new InputStreamReader(System.in)
);
while(true){
String input = consoleReader.readLine();
// 向服务器发送消息
client.send(input);
//检查用户是否准备退出
if(client.readyToQuit(input)){
break;
}
}
} catch (IOException e){
e.printStackTrace();
}
}
}
这里我们便完成了一个基于BIO模型的简易多人聊天室,大家可以自己去实现一下。
测试
测试是没问题的。
如果有说错的地方,请大家不吝赐教(记得留言哦~~~~)。