//先定义一个端口号,这个端口的值是可以自己调整的。
//在服务器端,我们需要使用ServerSocket,所以我们先声明一个ServerSocket变量
ServerSocket serverSocket=null;
//接下来,我们需要绑定监听端口, 那我们怎么做呢?只需要创建使用serverSocket实例
//ServerSocket有很多构造重载,在这里,我们把前边定义的端口传入,表示当前
//ServerSocket监听的端口是8080
serverSocket=new ServerSocket(DEFAULT_PORT);
System.out.println(“启动服务,监听端口:”+DEFAULT_PORT);
//回顾一下前面我们讲的内容,接下来我们就需要开始等待客户端的连接了。
//所以我们要使用的是accept这个函数,并且当accept方法获得一个客户端请求时,会返回
//一个socket对象, 这个socket对象让服务器可以用来和客户端通信的一个端点。
//开始等待客户端连接,如果没有客户端连接,就会一直阻塞在这个位置
Socket socket=serverSocket.accept();
//很可能有多个客户端来发起连接,为了区分客户端,咱们可以输出客户端的端口号
System.out.println(“客户端:”+socket.getPort()+“已连接”);
//一旦有客户端连接过来,我们就可以用到IO来获得客户端传过来的数据。
//使用InputStream来获得客户端的输入数据
//bufferedReader大家还记得吧,他维护了一个缓冲区可以减少数据源读取的频率
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String clientStr=bufferedReader.readLine(); //读取一行信息
System.out.println(“客户端发了一段消息:”+clientStr);
//服务端收到数据以后,可以给到客户端一个回复。这里咱们用到BufferedWriter
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(“我已经收到你的消息了\n”);
bufferedWriter.flush(); //清空缓冲区触发消息发送
}
}
BIOClientSocket
public class BIOClientSocket {
static final int DEFAULT_PORT=8080;
public static void main(String[] args) throws IOException {
//在客户端这边,咱们使用socket来连接到指定的ip和端口
Socket socket=new Socket(“localhost”,8080);
//使用BufferedWriter,像服务器端写入一个消息
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(“我是客户端Client-01\n”);
bufferedWriter.flush();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String serverStr=bufferedReader.readLine(); //通过bufferedReader读取服务端返回的消息
System.out.println(“服务端返回的消息:”+serverStr);
}
}
上述代码构建了一个简单的BIO通信模型,也就是服务端建立一个监听,客户端向服务端发送一个消息,实现简单的网络通信,那BIO有什么弊端呢?
我们通过对BIOServerSocket进行改造,关注case1和case2部分。
-
case1: 增加了while循环,实现重复监听
-
case2: 当服务端收到客户端的请求后,不直接返回,而是等待20s。
public class BIOServerSocket {
//先定义一个端口号,这个端口的值是可以自己调整的。
static final int DEFAULT_PORT=8080;
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket=null;
serverSocket=new ServerSocket(DEFAULT_PORT);
System.out.println(“启动服务,监听端口:”+DEFAULT_PORT);
while(true) { //case1: 增加循环,允许循环接收请求
Socket socket = serverSocket.accept();
System.out.println(“客户端:” + socket.getPort() + “已连接”);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String clientStr = bufferedReader.readLine(); //读取一行信息
System.out.println(“客户端发了一段消息:” + clientStr);
Thread.sleep(20000); //case2: 修改:增加等待时间
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(“我已经收到你的消息了\n”);
bufferedWriter.flush(); //清空缓冲区触发消息发送
}
}
}
接着,把BIOClientSocket复制两份(client1、client2),同时向BIOServerSocket发起请求。
运行后看到的现象应该是: client1先发送请求到Server端,由于Server端等待20s才返回,导致client2的请求一直被阻塞。
这个情况会导致一个问题,如果服务端在同一个时刻只能处理一个客户端的连接,而如果一个网站同时有1000个用户访问,那么剩下的999个用户都需要等待,而这个等待的耗时取决于前面的请求的处理时长,如图4-2所示。
图4-2
基于多线程优化BIO
为了让服务端能够同时处理更多的客户端连接,避免因为某个客户端连接阻塞导致后续请求被阻塞,于是引入多线程技术,代码如下。
ServerSocket
public static void main(String[] args) throws IOException, InterruptedException {
final int DEFAULT_PORT=8080;
ServerSocket serverSocket=null;
serverSocket=new ServerSocket(DEFAULT_PORT);
System.out.println(“启动服务,监听端口:”+DEFAULT_PORT);
ExecutorService executorService= Executors.newFixedThreadPool(5);
while(true) {
Socket socket = serverSocket.accept();
executorService.submit(new SocketThread(socket));
}
}
SocketThread
public class SocketThread implements Runnable{
Socket socket;
public SocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
System.out.println(“客户端:” + socket.getPort() + “已连接”);
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String clientStr = null; //读取一行信息
clientStr = bufferedReader.readLine();
System.out.println(“客户端发了一段消息:” + clientStr);
Thread.sleep(20000);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write(“我已经收到你的消息了\n”);
bufferedWriter.flush(); //清空缓冲区触发消息发送
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如图4-3所示,当引入了多线程之后,每个客户端的链接(Socket),我们可以直接给到线程池去执行,而由于这个过程是异步的,所以并不会同步阻塞影响后续链接的监听,因此在一定程度上可以提升服务端链接的处理数量。
图4-3
NIO非阻塞IO
使用多线程的方式来解决这个问题,仍然有一个缺点,线程的数量取决于硬件配置,所以线程数量是有限的,如果请求量比较大的时候,线程本身会收到限制从而并发量也不会太高。那怎么办呢,我们可以采用非阻塞IO。
NIO 从JDK1.4 提出的,本意是New IO,它的出现为了弥补原本IO的不足,提供了更高效的方式,提出一个通道(channel)的概念,在IO中它始终以流的形式对数据的传输和接受,下面我们演示一下NIO的使用。
NioServerSocket
public class NioServerSocket {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
//读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
System.out.println(new String(buffer.array()));
//写出数据
Thread.sleep(10000); //阻塞一段时间
//当数据读取到缓冲区之后,接下来就需要把缓冲区的数据写出到通道,而在写出之前必须要调用flip方法,实际上就是重置一个有效字节范围,然后把这个数据接触到通道。
buffer.flip();
socketChannel.write(buffer);//写出数据
} else {
Thread.sleep(1000);
System.out.println(“连接未就绪”);
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
NioClientSocket
public class NioClientSocket {
public static void main(String[] args) {
try {
SocketChannel socketChannel= SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(“localhost”,8080));
if(socketChannel.isConnectionPending()){
socketChannel.finishConnect();
}
ByteBuffer byteBuffer= ByteBuffer.allocate(1024);
byteBuffer.put(“Hello I’M SocketChannel Client”.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
//读取服务端数据
byteBuffer.clear();
while(true) {
int i = socketChannel.read(byteBuffer);
if (i > 0) {
System.out.println(“收到服务端的数据:” + new String(byteBuffer.array()));
} else {
System.out.println(“服务端数据未准备好”);
Thread.sleep(1000);
}
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
所谓的NIO(非阻塞IO),其实就是取消了IO阻塞和连接阻塞,当服务端不存在阻塞的时候,就可以不断轮询处理客户端的请求,如图4-4所示,表示NIO下的运行流程。
图4-4
上述这种NIO的使用方式,仍然存在一个问题,就是客户端或者服务端需要通过一个线程不断轮询才能获得结果,而这个轮询过程中会浪费线程资源。
多路复用IO
大家站在全局的角度再思考一下整个过程,有哪些地方可以优化呢?
我们回到NIOClientSocket中下面这段代码,当客户端通过read
方法去读取服务端返回的数据时,如果此时服务端数据未准备好,对于客户端来说就是一次无效的轮询。
我们能不能够设计成,当客户端调用read
方法之后,不仅仅不阻塞,同时也不需要轮询。而是等到服务端的数据就绪之后, 告诉客户端。然后客户端再去读取服务端返回的数据呢?
就像点外卖一样,我们在网上下单之后,继续做其他事情,等到外卖到了公司,外卖小哥主动打电话告诉你,你直接去前台取餐即可。
while(true) {
int i = socketChannel.read(byteBuffer);
if (i > 0) {
System.out.println(“收到服务端的数据:” + new String(byteBuffer.array()));
} else {
System.out.println(“服务端数据未准备好”);
Thread.sleep(1000);
}
}
所以为了优化这个问题,引入了多路复用机制。
I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
什么是fd:在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。
常见的IO多路复用方式有**【select、poll、epoll】**,都是Linux API提供的IO复用方式,那么接下来重点讲一下select、和epoll这两个模型
-
**select:**进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点
-
由于他能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd 处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。
-
同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机上万的TCP连接来说确实有点少
-
epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024
【由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行】
I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销,它的整体实现思想如图4-5所示。
客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。
图4-5
NIOServer的实现如下
测试访问的时候,直接在cmd中通过telnet连接NIOServer,便可发送信息。
public class NIOServer implements Runnable{
Selector selector;
ServerSocketChannel serverSocketChannel;
public NIOServer(int port) throws IOException {
selector=Selector.open(); //多路复用器
serverSocketChannel=ServerSocketChannel.open();
//绑定监听端口
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);//非阻塞配置
//针对serverSocketChannel注册一个ACCEPT连接监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
@Override
public void run() {
while(!Thread.interrupted()){
try {
selector.select(); //阻塞等待事件就绪
Set selected=selector.selectedKeys(); //得到事件列表
Iterator it=selected.iterator();
while(it.hasNext()){
dispatch((SelectionKey) it.next()); //分发事件
it.remove(); //移除当前时间
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void dispatch(SelectionKey key) throws IOException {
if(key.isAcceptable()){ //如果是客户端的连接事件,则需要针对该连接注册读写事件
register(key);
}else if(key.isReadable()){
read(key);
}else if(key.isWritable()){
write(key);
}
}
private void register(SelectionKey key) throws IOException {
//得到事件对应的连接
ServerSocketChannel server=(ServerSocketChannel)key.channel();
SocketChannel channel=server.accept(); //获得客户端的链接
channel.configureBlocking(false);
//把当前客户端连接注册到selector上,注册事件为READ,
// 也就是当前channel可读时,就会触发事件,然后读取客户端的数据
channel.register(this.selector,SelectionKey.OP_READ);
}
private void read(SelectionKey key) throws IOException {
SocketChannel channel=(SocketChannel)key.channel();
ByteBuffer byteBuffer= ByteBuffer.allocate(1024);
channel.read(byteBuffer); //把数据从channel读取到缓冲区
System.out.println(“server receive msg:”+new String(byteBuffer.array()));
}
private void write(SelectionKey key) throws IOException {
SocketChannel channel=(SocketChannel)key.channel();
//写一个信息给到客户端
channel.write(ByteBuffer.wrap(“hello Client,I’m NIO Server\r\n”.getBytes()));
}
public static void main(String[] args) throws IOException {
NIOServer server=new NIOServer(8888);
new Thread(server).start();
}
}
事实上NIO已经解决了上述BIO暴露的下面两个问题:
-
同步阻塞IO,读写阻塞,线程等待时间过长。
-
在制定线程策略的时候,只能根据CPU的数目来限定可用线程资源,不能根据连接并发数目来制定,也就是连接有限制。否则很难保证对客户端请求的高效和公平。
到这里为止,通过NIO的多路复用机制,解决了IO阻塞导致客户端连接处理受限的问题,服务端只需要一个线程就可以维护多个客户端,并且客户端的某个连接如果准备就绪时,会通过事件机制告诉应用程序某个channel可用,应用程序通过select方法选出就绪的channel进行处理。
单线程Reactor 模型(高性能I/O设计模式)
了解了NIO多路复用后,就有必要再和大家说一下Reactor多路复用高性能I/O设计模式,Reactor本质上就是基于NIO多路复用机制提出的一个高性能IO设计模式,它的核心思想是把响应IO事件和业务处理进行分离,通过一个或者多个线程来处理IO事件,然后将就绪得到事件分发到业务处理handlers线程去异步非阻塞处理,如图4-6所示。
Reactor模型有三个重要的组件:
-
**Reactor :**将I/O事件发派给对应的Handler
-
**Acceptor :**处理客户端连接请求
-
**Handlers :**执行非阻塞读/写
图4-6
下面演示一个单线程的Reactor模型。
Reactor
Reactor 负责响应IO事件,一旦发生,广播发送给相应的Handler去处理。
public class Reactor implements Runnable{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;
public Reactor(int port) throws IOException {
//创建选择器
selector= Selector.open();
//创建NIO-Server
serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
SelectionKey key=serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 绑定一个附加对象
key.attach(new Acceptor(selector,serverSocketChannel));
}
@Override
public void run() {
while(!Thread.interrupted()){
try {
selector.select(); //阻塞等待就绪事件
Set selectionKeys=selector.selectedKeys();
Iterator it=selectionKeys.iterator();
while(it.hasNext()){
dispatch((SelectionKey) it.next());
it.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void dispatch(SelectionKey key){
//调用之前注册时附加的对象,也就是attach附加的acceptor
Runnable r=(Runnable)key.attachment();
if(r!=null){
r.run();
}
}
public static void main(String[] args) throws IOException {
new Thread(new Reactor(8888)).start();
}
}
Acceptor
public class Acceptor implements Runnable{
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public Acceptor(Selector selector, ServerSocketChannel serverSocketChannel) {
this.selector = selector;
this.serverSocketChannel = serverSocketChannel;
}
@Override
public void run() {
SocketChannel channel;
try {
channel=serverSocketChannel.accept();
System.out.println(channel.getRemoteAddress()+“: 收到一个客户端连接”);
channel.configureBlocking(false);
//当channel连接中数据就绪时,调用DispatchHandler来处理channel
//巧妙使用了SocketChannel的attach功能,将Hanlder和可能会发生事件的channel链接在一起,当发生事件时,可以立即触发相应链接的Handler。
channel.register(selector, SelectionKey.OP_READ,new DispatchHandler(channel));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Handler
public class DispatchHandler implements Runnable{
private SocketChannel channel;
public DispatchHandler(SocketChannel channel) {
this.channel = channel;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+“—handler”); //case: 打印当前线程名称,证明I/O是同一个线程来处理。
ByteBuffer buffer=ByteBuffer.allocate(1024);
int len=0,total=0;
String msg=“”;
try {
do {
len = channel.read(buffer);
if (len > 0) {
total += len;
msg += new String(buffer.array());
}
buffer.clear();
} while (len > buffer.capacity());
System.out.println(channel.getRemoteAddress()+“:Server Receive msg:”+msg);
}catch (Exception e){
e.printStackTrace();
if(channel!=null){
try {
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
}
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
Docker步步实践
目录文档:
①Docker简介
②基本概念
③安装Docker
④使用镜像:
⑤操作容器:
⑥访问仓库:
⑦数据管理:
⑧使用网络:
⑨高级网络配置:
⑩安全:
⑪底层实现:
⑫其他项目:
理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-bApyrAGi-1710409880907)]
[外链图片转存中…(img-Ggs1JTZ2-1710409880908)]
[外链图片转存中…(img-wcKAsfAr-1710409880908)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-YVKIEoqa-1710409880909)]
Docker步步实践
目录文档:
[外链图片转存中…(img-yASEPXtv-1710409880909)]
[外链图片转存中…(img-QWrlcbpN-1710409880910)]
①Docker简介
②基本概念
③安装Docker
[外链图片转存中…(img-Qj29TmER-1710409880910)]
④使用镜像:
[外链图片转存中…(img-Ylyexch0-1710409880910)]
⑤操作容器:
[外链图片转存中…(img-nqCuEKWb-1710409880911)]
⑥访问仓库:
[外链图片转存中…(img-nlj8vguY-1710409880911)]
⑦数据管理:
[外链图片转存中…(img-PeVfz3tv-1710409880911)]
⑧使用网络:
[外链图片转存中…(img-ZheDItko-1710409880912)]
⑨高级网络配置:
[外链图片转存中…(img-Ing6gKHE-1710409880912)]
⑩安全:
[外链图片转存中…(img-zW6ucqmy-1710409880912)]
⑪底层实现:
[外链图片转存中…(img-eKZtfMzU-1710409880913)]
⑫其他项目:
[外链图片转存中…(img-7dmaTggJ-1710409880913)]