上篇简单实现了回音壁,但是这也太不够意思了,如果可以多人发言那就稍微有点意思了。
这里先来说说java BIO 编程模型。
BIO也就是我们经常说的java IO,在这种传统的模式下,如果要实现服务端服务,
1.首先他要等待客户端连接吧?
执行serverSocket.accept();
会一直阻塞在这句话这里,直到有客户端连接才会做其他的事情。
2.那么如何做到服务多个客户端呢?
我们采用新开一个线程来处理与这个客户端的交互,在这里,我采用ChatHandler来负责处理与客户端的交互,之后主线程继续在一个循环里面等待新的客户端连接。
咳咳,书上是这么说的,我也是这么做的。
首先定义IP地址 和要监听的端口:
private int DEFAULT_PORT = 8888;
private final String QUIT = "quit";
private ServerSocket serverSocket;
那么如果真的有多个用户,如何保存呢?这里采用最简单粗暴的map接口存储,以客户端的端口为Key,客户端为value值。
private Map<Integer,Writer> connectedClients;
在构造函数里面,主要负责初始化。
public ChatServer(){
connectedClients = new HashMap<>();
}
启动函数非常的简单,就是等待客户端连接,如果有就分配一个线程启动ChatHandler来处理,否则就继续的等待。
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 (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
在ChatHandler主要做的事情如下 :
将这个客户端加入我们的聊天室内,也就是map中,接收这个客户端发来的消息 转发给其他的客户端,并且判断客户端发来的消息是不是表示要退出,如果退出,则ChatHandler结束服务。
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;
while ((msg = reader.readLine())!=null){
System.out.println("客户端【" +socket.getPort()+"】:"+msg);
//转发消息给其他的在线用户
server.forwardMsg(socket,"客户端【" +socket.getPort()+"】:"+msg+"\n");
//检查用户发送的消息是不是下线消息
if (server.readToQuit(msg)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//不管怎么了,也是要移除这个掉线用户的
try {
server.removeClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
暂时在这里,addClient
,forwardMsg
,removeClient
都是暂时还没有实现的方法。
这里回过头来,来实现以上没有实现的方法。
添加客户端的方法:
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 forwardMsg(Socket socket,String fwdMsg) throws IOException {
for (Integer port : connectedClients.keySet()) {
if (!port.equals(socket.getPort())){
Writer writer = connectedClients.get(port);
writer.write(fwdMsg);
writer.flush();
}
}
}
服务端的代码大概就是 如此,那么客户端呢?
客户端的实现比较简单,既然服务端是检测客户端的输入流输出流,那么客户端只要关注操作这两个就好了。
private final String DEFAULT_SERVER_HOST = "127.0.0.1";
private final int DEFAULT_SERVER_PORT = 8888;
private final String QUIT = "quit";
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
同理,在客户端等待用户的输入,也是一个阻塞调用,我们总不能一个客户端就等着用户输入,啥也不做吧?这里同样的是采用一个新开线程UserInputHandler
来监听用户的输入。
所以start
函数,要做的事情就这么多。
public void start(){
try {
socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
//创建IO流
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
//处理用户的输入 TODO
new Thread(new UserInputHandler(this)).start();
//读取服务器转发的消息
String msg = null;
while ((msg = receive()) !=null){
System.out.println(msg);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
close();
}
}
在UserInputHandler
里面不仅仅监听用户的输入,还要负责将消息发送给服务器。
同时还要判断用户是否要退出客户端。
public class UserInputHandler implements Runnable {
private ChatClient chatClient;
public UserInputHandler(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public void run() {
try {
BufferedReader consoleReader = new BufferedReader(
new InputStreamReader(System.in)
);
while (true){
String input = consoleReader.readLine();
//向服务器发送消息
chatClient.send(input);
if (chatClient.readyToQuit(input)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
接下来实现还没有完成的代码:
receive
方法:接收服务器的消息
public String receive() throws IOException {
String msg = null;
if (!socket.isInputShutdown()){
msg = reader.readLine();
}
return msg;
}
send
方法,发消息给服务器:
//发送消息给服务器
public void send(String msg) throws IOException {
if (!socket.isOutputShutdown()){
writer.write(msg+"\n");
writer.flush();
}
}
主要代码就是这样,可以改进的地方可以使用线程池代替单独创建线程。
跑起试试:
附赠BIO编程模型一张图:
下篇:JavaIO之NIO,BIO的复制文件的简单比较(3)