Skip to content

传统的网络开发

一、Socket 通信

我们,首先来用 Java 实现一个简单的 Socket 通信的程序

java
@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        try(ServerSocket serverSocket = new ServerSocket(9099);) {
            while(true) {
                Socket socket = serverSocket.accept();
                log.info("接受到客户端的请求");
                InputStream inputStream = socket.getInputStream();
                byte[] bytes = new byte[1024];
                int read = -1;
                while((read = inputStream.read(bytes)) != -1) {
                    String message = new String(bytes,0,read);
                    log.info("接受到客户端的信息为:{}",message);
                }
                socket.close();
            }
        } catch (Exception e) {
            log.error("通信异常",e);
        }
    }
}
java
@Slf4j
public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",9099);
        Scanner scanner = new Scanner(System.in);
        String message = scanner.next();
        socket.getOutputStream().write(message.getBytes(StandardCharsets.UTF_8));
        socket.getOutputStream().close();
        socket.close();
    }
}

对应的流程如下:

对于服务端而言,只会有一个 ServerSocket 用来等待接受客户端的连接,当客户端连接上来之后,服务端就会创建 Socket 与其进行通信。

运行完成上述代码之后,能够发现客户端程序运行完成之后,服务端并没有结束(不仅仅是因为 while 循环),而是一直阻塞在了 accept 这里。

现在,我们修改一下客户端的代码

java
@Slf4j
public class Client1 {
    public static void main(String[] args) {
        try (Socket socket = new Socket("127.0.0.1", 9091)) {
            new CountDownLatch(1).await();
        } catch (Exception e) {
            log.error("启动服务失败", e);
        }
    }
}
java
@Slf4j
public class Client2 {
    public static void main(String[] args) {
        try (Socket socket = new Socket("127.0.0.1", 9091)) {
            String message = "hello,server";
            socket.getOutputStream().write(message.getBytes(StandardCharsets.UTF_8));
            socket.getOutputStream().close();
            new CountDownLatch(1).await();
        } catch (Exception e) {
            log.error("启动服务失败", e);
        }
    }
}

很明显,一个客户端发送了消息,另一个客户端并没有发送消息。服务端代码并没有变化

此时启动服务端之后,服务端的日志如下:

java
00:17:48.043 [main] INFO com.coding.demo2.Server - 服务端启动成功~
00:17:52.649 [main] INFO com.coding.demo2.Server - 接受到客户端连接

此时的阻塞点,实际上是在于等客户端的数据

java
InputStream inputStream = socket.getInputStream();

因为,现在服务端是一个单线程的,接受连接 和 处理请求实际上是在一个线程运行的。这个时候,其实能够发现,能够发现:

  • 如果说一个服务器启动就绪,那么主线程就一直等待客户端的连接,这个等待过程中,主线程就一直阻塞
  • 在连接建立之后,在读取到Socket信息之前,线程也是一直等待,一直处于阻塞状态,不能做其他的事情

二、多线程

这个时候我们尝试修改服务端的代码,每当接受到一个连接之后,我们将 Socket 放入线程里面去执行,来避免单线程的问题

java
@Slf4j
public class Server {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(9091)) {
            log.info("服务端启动成功~");
            while (true) {
                Socket socket = serverSocket.accept();
                new Thread(() -> {
                    log.info("接受到客户端连接");
                    try {
                        InputStream inputStream = socket.getInputStream();
                        byte[] bytes = new byte[1024];
                        int length = -1;
                        while ((length = inputStream.read(bytes)) != -1) {
                            log.info("服务端接受到的数据为:{}", new String(bytes, 0, length));
                        }
                        socket.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }

                }).start();
            }
        } catch (Exception e) {
            log.error("启动服务失败", e);
        }
    }
}

但是,每次接受到一个 Socket 连接,都会创建线程去进行处理,但是,这样做会带来如下问题:

  • 每次创建线程,启动线程都要陷入内核态,这种操作是非常消耗性能的
  • 每次进来都会创建线程,如果连接数少的话,还好说,如果说连接数大了,就会导致内存占用很高

三、池化

既然连接数过多,最为常见的做法就是复用,就像 Druid,线程池等,采用 池化 的思想。接下来,我们将处理请求的逻辑,放入到线程池之中

java
@Slf4j
public class Server {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        try (ServerSocket serverSocket = new ServerSocket(9091)) {
            log.info("服务端启动成功~");
            while (true) {
                Socket socket = serverSocket.accept();
                executorService.execute(() -> {
                    log.info("接受到客户端连接");
                    InputStream inputStream = null;
                    try {
                        inputStream = socket.getInputStream();
                        byte[] bytes = new byte[1024];
                        int length = -1;
                        while ((length = inputStream.read(bytes)) != -1) {
                            log.info("服务端接受到的数据为:{}", new String(bytes, 0, length));
                        }
                        socket.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
            }
        } catch (Exception e) {
            log.error("启动服务失败", e);
        } finally {
            executorService.shutdownNow();
        }
    }
}

那么,通过这样的方式,是否已经解决了现有的问题呢?在这里,我们调整一下服务端的代码,在处理请求的时候,让其等待 20s

java
@Slf4j
public class Server {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        try (ServerSocket serverSocket = new ServerSocket(9091)) {
            log.info("服务端启动成功~");
            while (true) {
                Socket socket = serverSocket.accept();
                log.info("接受到客户端连接");
                executorService.execute(() -> {
                    log.info("executorServer execute.......");
                    // 进行网络通信
                    InputStream inputStream = null;
                    try {
                        inputStream = socket.getInputStream();
                        byte[] bytes = new byte[1024];
                        int length = -1;
                        while ((length = inputStream.read(bytes)) != -1) {
                            log.info("服务端接受到的数据为:{}, {}", new String(bytes, 0, length), length);
                        }
                        TimeUnit.SECONDS.sleep(20);
                        socket.close();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                });
            }
        } catch (Exception e) {
            log.error("启动服务失败", e);
        } finally {
            executorService.shutdown();
        }
    }
}

测试过程如下:

  1. 启动服务端
  2. 启动客户端1
  3. 启动客户端2
  4. 启动客户端2(启动多份)

当我们执行到第四步的时候,能够发现线程池其实核心线程数已经满了,已经将任务放入到了等待队列里面去。所以,这种模式实际上也是有问题的。比如说:

  1. 线程池的资源是有限的,如果说来了 N 多个客户端连接,你的线程池还能够用吗?
  2. 如果说每个线程都需要等待别的资源,他其实没有空去别的事,就被阻塞在哪了

所以,只去考虑增加线程池,没有解决线程阻塞的问题。

接下来,我们就来看一下 NIO 是如何解决这些问题的