BIO(Blocking I/O),同步阻塞。是传统SocketIO的方式,它有两个阻塞点
serverSocket.accept()
socket.getInputStream().read(bytes);
BIO有两种情况:
- 单线程BIO:一次只能有一个客户端,一个客户处理完后,才能有下一个客户
-
单线程BIO
这是一份单线程BIO的示例代码
客户端代码
public class Client{
public static void main(String[] args) throws IOException{
//创建连接
Socket socket = new Socket("127.0.0.1", 9090);
//从控制台中获取一行
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
//把信息发送到服务器中
socket.getOutputStream().write(s.getBytes());
//关闭连接
socket.close();
}
}
服务端代码
public class Server{
public static void main(String[] args) throws IOException{
ServerSocket serverSocket = new ServerSocket(9090);
while(true) {
//等待客户端连接,注意这里是阻塞的
//只有一个客户端连接时,才会继续下去
Socket socket = serverSocket.accept(); //返回一个与客户连接的Socket对象socket
//有一个客户端连接了
System.out.println("有人连进来了");
byte[] bytes = new byte[1024];
//等待客户发消息,注意这里也是阻塞的
socket.getInputStream().read(bytes);
System.out.println("收到客户端的消息" + new String(bytes));
}
}
}
服务器有两个阻塞的方法serverSocket.accept()
、socket.getInputStream().read(bytes);
。
这样肯定是不行的
- 当没有客户来连接时,服务器将一直卡在
serverSocket.accept()
,等待客户的连接,而不能做别的事情 - 如果客户A发送了Socket,和服务器进行通信,但它始终都不发送消息,程序就一直卡在
socket.getInputStream().read(bytes);
。其他人再想来通信,都会失败(一次只能处理一个客户,只有当这个客户处理完成后,才能处理下一个客户)
总结:这种方法的最大问题是无法并发,效率太低。如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。
多线程BIO
可以用多线程的方式对单线程BIO进行改良:一个连接一个线程(connection per thread)
即当有客户端连接时,服务器端需为其单独分配一个线程
流程
- 服务器端启动一个SeverSocket
- 客户端启动Socket对服务器端发起通信,默认情况下服务器端需为每个客户端创建一个线程与之通讯
- 客户端发起请求后,先咨询服务器端是否有线程响应,如果没有则会等待或被拒绝
- 如果有线程响应,客户端线程会等待请求结束后,再继续执行
代码
客户端代码
//BIO-客户端
public class BIOClient {
public static void main(String[] args) throws IOException {
//创建Socket
Socket socket = new Socket("localhost", 6666); //服务器地址与端口
OutputStream outputStream = socket.getOutputStream(); //获得输出流
//从控制台获取输入
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String message = scanner.nextLine();
if ("exit".equals(message)) {
break;
}
outputStream.write(message.getBytes()); //将输入的内容发送到服务端
}
outputStream.close();
//关闭socket
socket.close();
}
}
服务端代码
//BIO-服务器端
public class BIOSever {
public static void main(String[] args) throws IOException {
//线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//服务器Socket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器已启动");
//不断轮询
while (true){
//等待客户端连接,注意这里是阻塞的
//只有一个客户端连接时,才会继续下去
System.out.println("等待客户端连接.....(阻塞中)");
Socket socket = serverSocket.accept(); //返回一个与客户连接的Socket对象socket
//有一个客户端连接了,新增一个线程,处理这个连接
System.out.println("客户端连接");
cachedThreadPool.execute(new Runnable() {
public void run() {
handler(socket);
}
});
}
}
//从客服端socket读取数据
public static void handler(Socket socket){
try{
InputStream inputStream = socket.getInputStream();
byte[] b = new byte[1024];
while (true){
//等到客户输入信息,注意这里是阻塞的
//只有当客户输入了一个信息之后,才会继续下去
System.out.println("等待客户端输入.....(阻塞中)");
int read = inputStream.read(b); //获得用户输入的内容
if (read != -1){
System.out.println(new String(b, 0, read));
}else {
break;
}
}
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
评价
这个BIO的改良方法,支持了多并发
- 只适用于连接数目较小切固定的架构
- 适合短连接,一问一答的形式(收到问题,回答一下,立刻关闭)
对服务器资源的要求较高,不适合长连接
- 服务器端在监听客户端连接时(
serverSocket.accept()
),服务器端处于阻塞状态,不能处理其他事务 - 服务器端需要为每个客户端建立一个线程,虽然可以用线程池来优化,但在并发较大时,线程开销依旧很大
- 当连接的客户端没有发送数据时,服务器端会阻塞在read操作上,等待客户端输入,造成线程资源浪费
比喻:餐厅
- 整个服务端,就是一个餐厅
- 大门就是服务器的地址与端口
- ServerSocket是一个门卫,他负责看是否有客人进来。如果有客人进来,要为这个客人分配一个服务员
- 多线程BIO的做法就是:一个服务员只能服务一个客人,只有等这个客人走了,才能服务另外一个
如此,你会发现
- 如果你的客人吃了很久,你这个服务员一直都呆在那,不能服务其他人(线程堵塞,造成资源浪费)
- 如果客人多了起来,你就要有很多服务员,你的餐厅就很难盈利(数量一大,服务器撑不住)
- 如果客人只是过来拿个外卖,立刻就走(适合短连接)
回过头来,想想单线程BIO
- 只有一个员工,这个员工既当服务员,又当门卫
- 如果有一个客户进来,这个员工只能去服务它。外面的客户都进不来了,被堵在门口
-
参考文章
- 传统Socket分析
- 高性能IO之Reactor模式