1. 同步vs异步/阻塞vs非阻塞
① 同步VS异步
同步
同步和异步关注的是消息通信机制,所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了
换句话说,就是由调用者主动等待这个调用的结果
异步
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调
② 阻塞VS非阻塞
阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
非阻塞
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关
③ 同步阻塞BIO
想想之前基于BIO做的多人聊天室的例子,client
和服务器建立了socket
连接后,client
在调用read()
方法时,stream
里没有数据可读,线程停止向下执行,直至stream
有数据
既然是我调用了read()
以后我什么都不干了,就等你的数据,就算对方没有准备好发送数据我也一直等待不做自己的事情,那么可想而知是阻塞的
而对于同步的概念则体现在stream
有没有数据是需要我自己来判断的,而不是让别人来通过回调告诉我
④ 同步非阻塞NIO
对于NIO来说,我们可以设置非阻塞,但是他还是同步的,client
采用循环的方式去判断当前的状态,依旧是自己判断
由于同步/异步,阻塞/非阻塞描述的不是同一个层面上的东西,所以可以两两组合,也就是最上面的那张表
2. BIO
模型
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后,为每个客户端创建一个新的线程,对请求进行处理,处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答模型
传统的BIO
模型
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死掉了
伪异步I/O
模型
为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程,实现一个或多个线程处理多个客户端请求的模型(服务端的线程个数和客户端并发访问数呈M:N的关系,N可以远远大于M,但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“
3. BIO
多人聊天室
服务端:
- 主线程Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后,为每个客户端创建一个新的线程接收用户的输入;
- 为了能把收到的消息发送给进入房间,也就是建立了连接的其他用户,我们还需要一个集合存放创建的所有连接
客户端:
- 首先要可以和服务端创建连接
- 客户端要能够输入,而且要接收消息
- 由于输入是阻塞的(发送也是阻塞的),所有在客户端需要两条线程,一条用于接收消息,一条发送消息
3.1 服务端
首先是服务端的编码,像上面分析的服务端包括两个部分
① 一个主线程接收所有的连接并且维护创建的的连接
② 为每个连接新启一个线程处理他的业务
① ChatServer
先来看主线程,在主线程中维护一个map用来保存所有的连接,其中key是客户端的端口号,value是服务端和客户端之间建立的io输出流
private Map<Integer, Writer> connectedClient;//key:port value:用于写入客户端的Writer
再为Map提供增加用户和删除用户的方法
要注意线程安全的问题,这里直接使用同步方法
//添加用户
public synchronized void addClient(Socket socket) throws IOException {
if(socket!=null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
connectedClient.put(port, writer);
System.out.println("客户端"+port+"进入房间");
}
}
//移除用户
public synchronized void removeClient(Socket socket) throws IOException {
if(socket!=null){
int port = socket.getPort();
if(connectedClient.get(port)!=null){
connectedClient.get(port).close();
connectedClient.remove(port);
System.out.println("客户端"+port+"离开房间");
}
}
}
接收到用户发送的消息,我们轮询整个map,把消息发送给除自己以外的用户
//向在线用户发送消息(除了发送消息的人)
public synchronized void forwardMessage(Socket socket, String fsdMsg) throws IOException {
for (Integer id : connectedClient.keySet()) {
if(!id.equals(socket.getPort())){//除了自己
Writer writer = connectedClient.get(id);
writer.write(fsdMsg);
writer.flush();
}
}
}
主要逻辑:绑定监听端口,如果有用户来建立了连接则为他新开一个线程,并把用户添加到map中
public void start(){//主要逻辑
//绑定监听端口
try {
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("启动服务器,监听端口"+DEFAULT_PORT+"...");
while (true){
Socket socket = serverSocket.accept();
//为每一个用户创建一个线程
new Thread(new ChatHandler(this,socket)).start();
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(serverSocket!=null){
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
完整代码
public class ChatServer {
private final int DEFAULT_PORT = 8888;
private final String QUIT = "quit";//退出关键词
private ServerSocket serverSocket;
private Map<Integer, Writer> connectedClient;//key:port value:用于写入客户端的Writer
public ChatServer() {
connectedClient = new HashMap<Integer, Writer>();
}
//添加用户
public synchronized void addClient(Socket socket) throws IOException {
if(socket!=null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
connectedClient.put(port, writer);
System.out.println("客户端"+port+"进入房间");
}
}
//移除用户
public synchronized void removeClient(Socket socket) throws IOException {
if(socket!=null){
int port = socket.getPort();
if(connectedClient.get(port)!=null){
connectedClient.get(port).close();
connectedClient.remove(port);
System.out.println("客户端"+port+"离开房间");
}
}
}
//向在线用户发送消息(除了发送消息的人)
public synchronized void forwardMessage(Socket socket, String fsdMsg) throws IOException {
for (Integer id : connectedClient.keySet()) {
if(!id.equals(socket.getPort())){//除了自己
Writer writer = connectedClient.get(id);
writer.write(fsdMsg);
writer.flush();
}
}
}
public synchronized boolean readyToQuit(String msg){
if (msg.equals(QUIT)){
return true;
}
return false;
}
public void start(){//主要逻辑
//绑定监听端口
try {
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("启动服务器,监听端口"+DEFAULT_PORT+"...");
while (true){
Socket socket = serverSocket.accept();
//为每一个用户创建一个线程
new Thread(new ChatHandler(this,socket)).start();
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(serverSocket!=null){
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
chatServer.start();
}
}
② ChatHandler
这一部分是对每一个新连接的线程新起的线程代码,里面就是使用了ChatServer
的各种方法把自己加入map,并发送消息
public class ChatHandler implements Runnable{//在服务端对于每一个客户对应一个ChatHandler线程
private ChatServer server;
private Socket socket;
public ChatHandler(ChatServer server, Socket socket) {
this.server = server;
this.socket = socket;
}
public void run() {
BufferedReader reader = null;
try {
//存储新上线客户
server.addClient(socket);
//读取用户发送的消息
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = null;
while ((msg=reader.readLine())!=null){//流被关闭读取的是null
String newmsg = "客户端["+socket.getPort()+"]: "+msg+"/n";
System.out.println(newmsg);
//转发消息给聊天室的在线用户
server.forwardMessage(socket, newmsg);
if(server.readyToQuit(msg)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
server.removeClient(socket);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.2 客户端
客户端要可以和服务端创建连接,并向服务端发送消息,接收消息;为了在发送或接收的时候不阻塞,为发送和接收创建的是不同的线程
① ChatClient
public class ChatClient {
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;
//发送消息给服务器
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.equals(msg);
}
//主要逻辑
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 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 {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ChatClient chatClient = new ChatClient();
chatClient.start();
}
}
② UserInputHandler
处理用户的输入
public class UserInputHandler implements Runnable{//处理用户的输入
private ChatClient chatClient;
public UserInputHandler(ChatClient chatClient) {
this.chatClient = chatClient;
}
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();
}
}
}