一、介绍
网络编程的本质是进程间通信,通信的基础是IO模型。
IO模型分为三种,依次是BIO、NIO、AIO
不了解同步异步,阻塞和非阻塞的概念的可以看这里
Java中的BIO编程模型,是一中阻塞式的编程模型,它使用同步阻塞的方式进行网络通信。在BIO模型中,服务端采用一个线程来处理一个客户端的连接请求,并且通过创建一个新的线程来处理每个客户端的请求。
二、代码实现
我们通过一个聊天室来看一下BIO模型的实现过程,这里用到网络编程的内容
服务器端分为两个类ChatServer 和 ChatHandler
ChatServer负责接收客户端连接,为每个客户端分配一个线程;ChatHandler负责服务器的主要功能,接受消息并转发,代码如下:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//服务器端
public class ChatServer {
//服务器默认端口号
private final int DEFAULT_PORT = 8888;
//QUIT,负责判断,当接收到客户端发来的quit时,关闭这个客户端的连接
private final String QUIT = "quit";
private ServerSocket serverSocket;
//这里使用hashmap存储获取到每个客户端的端口号和每个客户端的字符流,用来群聊转发数据
private Map<Integer,Writer> connectedClients;
//这里使用线程池,为每个连接进来的客户端分配一个线程
private ExecutorService executorService;
public ChatServer() {
connectedClients = new HashMap<>();
executorService = Executors.newFixedThreadPool(5);
}
/**
* 添加客户
* @param socket
* @throws IOException
*/
//在每个操作hashmap的方法前加了synchronized关键字,保证hashmap的线程安全
public synchronized void addClient(Socket socket) throws IOException {
if(socket != null) {
int port = socket.getPort();
Writer writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
connectedClients.put(port,writer);
System.out.println("客户端【" + port + "】已连接到服务器");
}
}
/**
* 移除客户
* @param socket
* @throws IOException
*/
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 + "】已断开连接");
}
}
/**
* 消息转发
* @param socket
* @param fwdMsg
* @throws IOException
*/
public synchronized void forwardMessage(Socket socket, String fwdMsg) throws IOException{
for(Integer id : connectedClients.keySet()) {
if(!id.equals(socket.getPort())) { //不是发送者
Writer writer = connectedClients.get(id);
writer.write(fwdMsg);
writer.flush();
}
}
}
/**
* 关闭serverSocket连接
* @param serverSocket
*/
public synchronized void close(ServerSocket serverSocket) {
if (serverSocket != null) {
try {
serverSocket.close();
System.out.println("关闭serverSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 开启服务器
*/
public void start() {
//绑定监听端口
try {
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("服务器已启动,监听在8888端口");
while (true) {
// 等待客户端连接
Socket socket = serverSocket.accept();
// 创建ChatHandler线程
executorService.execute(new ChatHandler(this,socket));
// new Thread(new ChatHandler(this,socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close(serverSocket);
}
}
public boolean readyToQUit(String msg) {
if(QUIT.equals(msg)) {
return true;
} else {
return false;
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
chatServer.start();
}
}
import java.io.*;
import java.net.Socket;
public class ChatHandler implements Runnable{
//调用聊天服务器中的方法
private ChatServer chatServer;
//拿到每个客户端的套接字
private Socket socket;
public ChatHandler(ChatServer chatServer, Socket socket) {
this.chatServer = chatServer;
this.socket = socket;
}
@Override
public void run() {
try {
// 存储新上线用户
chatServer.addClient(socket);
// 读取用户发过来的消息
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = null;
while ((msg = reader.readLine()) != null) {
String fwdMsg = "客户端【" + socket.getPort() + "】:" + msg;
System.out.println(fwdMsg);
//转发消息
chatServer.forwardMessage(socket,fwdMsg + "\n");
//检查用户是否退出
if(chatServer.readyToQUit(msg)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
chatServer.removeClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端分为两个类,ChatClient与UserInputHandler
注意:由于BIO是同步阻塞的,所以要给客户端发消息和收消息各分配一条线程
这里我们用主线程负责收消息,新开一条线程负责发消息
ChatClient 负责连接服务器,UserInputHandler负责处理用户输入的数据并发给客户端
import java.io.*;
import java.net.Socket;
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 ChatClient() {
}
// 发送消息给服务器
public void send(String msg) throws IOException {
if(!socket.isInputShutdown()) {
writer.write(msg + "\n");
writer.flush();
}
}
// 从客户端接收消息
public String receive() throws IOException{
String revMsg = null;
if(!socket.isInputShutdown()) {
revMsg = reader.readLine();
}
return revMsg;
}
// 检查用户是否准备退出
public boolean readyToQuit(String msg) {
return QUIT.equals(msg);
}
public void close() {
if(writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//启动客户端
public void start() {
try {
//创建socket
socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT);
//获取输入输出流
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 chatClient = new ChatClient();
chatClient.start();
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class UserInputHandler implements Runnable{
private ChatClient chatClient;
public UserInputHandler(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public void run() {
//从控制台获取输入信息
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
while (true) {
String msg = reader.readLine();
System.out.println("【发送成功】" + msg);
//把消息发给服务器
chatClient.send(msg);
if(chatClient.readyToQuit(msg)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、总结
BIO模型的优点是编程简单,易于理解与实现。缺点是由于需要给每个客户端都分配一条线程,当连接数较多时会占用太大的系统资源,会导致系统性能的下降;当连接长时间没有数据传输时,一方会一直处于阻塞状态,会导致系统资源的浪费。
因此对于高并发的场景,NIO(Non-blocking I/O)编程模型,它能够使用单线程来处理多个客户端连接,并且采用同步非阻塞方式进行网络通信,从而提高系统的并发性能。