既然跟网络内容有关就不得不学习网络IO模型,时代在进步,技术也在进步,采取使用那种网络IO模型就已经确定应用程序规模
阻塞IO(blocking IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
图1 阻塞IO
大部分的IO接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
对于网络编程,在第一阶段系统内核阻塞等侍接收完整数据包,主进程/线程无法执行运算同响应其它请求,非常浪费硬件资源,解决方案是每个socket开个线程/进程独立处理
1 public final class ServerBio { 2 private static int DEFAULT_PORT = 12345; 3 private static ServerSocket server; 4 private static AtomicInteger ai = new AtomicInteger(); 5 6 public static void main(String[] args) throws Exception { 7 try { 8 server = new ServerSocket(DEFAULT_PORT); 9 System.out.println("服务器已启动,端口号:" + DEFAULT_PORT); 10 while (true) { 11 Socket socket = server.accept(); 12 ai.incrementAndGet(); 13 new Thread(new ServerHandler(socket)).start(); 14 } 15 } finally { 16 if (server != null) { 17 System.out.println("服务器已关闭。"); 18 server.close(); 19 server = null; 20 } 21 } 22 } 23 24 public static class ServerHandler implements Runnable { 25 private Socket socket; 26 private BufferedReader in = null; 27 private PrintWriter out = null; 28 29 public ServerHandler(Socket socket) { 30 this.socket = socket; 31 } 32 33 @Override 34 public void run() { 35 try { 36 in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 37 out = new PrintWriter(socket.getOutputStream(), true); 38 String body; 39 while (true) { 40 if ((body = in.readLine()) == null) { 41 continue; 42 } 43 System.out.println("服务器收到消息:" + body); 44 out.println(ai.get()); 45 } 46 } catch (Exception e) { 47 48 } finally { 49 if (in != null) { 50 try { 51 in.close(); 52 } catch (IOException e) { 53 e.printStackTrace(); 54 } 55 } 56 if (out != null) { 57 out.close(); 58 } 59 if (socket != null) { 60 try { 61 socket.close(); 62 } catch (IOException e) { 63 e.printStackTrace(); 64 } 65 } 66 } 67 } 68 }
1 public class ClientBio { 2 private static int DEFAULT_SERVER_PORT = 12345; 3 private static String DEFAULT_SERVER_IP = "127.0.0.1"; 4 private static AtomicInteger ai = new AtomicInteger(); 5 6 private Socket socket = null; 7 private BufferedReader in = null; 8 private PrintWriter out = null; 9 10 public static void main(String[] args) throws InterruptedException { 11 while (true) { 12 send("xxxxxx"); 13 Thread.sleep(1); 14 } 15 } 16 17 public static void send(String body) { 18 new ClientBio().send(DEFAULT_SERVER_PORT, body); 19 } 20 21 public void send(int port, String body) { 22 try { 23 socket = new Socket(DEFAULT_SERVER_IP, port); 24 in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 25 out = new PrintWriter(socket.getOutputStream(), true); 26 out.println(body); 27 ai.incrementAndGet(); 28 System.out.println("客户端 接收:" + in.readLine()); 29 } catch (Exception e) { 30 e.printStackTrace(); 31 } 32 } 33 }
BIO有个致命的缺点,由于线程/进程资源是有限的,在测试发现当开了500线程左右每接收/创建一个socket时间变得越来越长,也就是说采用BIO模型的瓶颈在500左右(大众机器)
解决方案也简单:线程是有限的,那么就用池程线来重用线程
ServerBioPool.class 只需要添加ExecutorService pool 替换掉Thread即可
1 private static ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*10); 2 public static void main(String[] args) throws Exception { 3 try { 4 server = new ServerSocket(DEFAULT_PORT); 5 System.out.println("服务器已启动,端口号:" + DEFAULT_PORT); 6 while (true) { 7 Socket socket = server.accept(); 8 ai.incrementAndGet(); 9 pool.submit(new ServerHandler(socket)); 10 } 11 } finally { 12 // 一些必要的清理工作 13 if (server != null) { 14 System.out.println("服务器已关闭。"); 15 server.close(); 16 server = null; 17 } 18 } 19 }
上面应用场景比较有限,如果是长连接不释放socket资源的话,每个socket占用一个thread,用thread pool只能优化thread创建和销毁的频率并不能解决thread不足问题,读者可以试下把线程数改成800再测试。
现实与理想差距还是很大的
我们来论证瓶颈是否出现在线程上,屏蔽掉 ServerBioPool.class ClientBio.class 发送接收处理
public static void main(String[] args) throws Exception { server = new ServerSocket(DEFAULT_PORT); System.out.println("服务器已启动,端口号:" + DEFAULT_PORT); while (true) { Socket socket = server.accept(); ai.incrementAndGet(); System.out.println(ai.get()); } }
ClientBio.class
public void send(int port, String body) { try { socket = new Socket(DEFAULT_SERVER_IP, port); } catch (Exception e) { e.printStackTrace(); } }
结果打印出的数字能突破成千上万
如果client使用bio是无影响的,因为由始至终只有一个socket
小结:使用BIO模型,性能屏颈受线程/进程数上限影响,client可以使用bio