作者简介:☕️大家好,我是Aomsir,一个爱折腾的开发者!
个人主页:Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客
当前专栏:Netty应用专栏_Aomsir的博客-CSDN博客
文章目录
- 参考文献
- 前言
- 基础扫盲
- 问题所在
- 调整非阻塞
- 技巧
- 总结
参考文献
- 孙哥suns说Netty
- Netty官方文档
前言
在前面的文章中,我们详细学习并使用了NIO中的Channel和Buffer。然而,我们的示例中Channel的两端都是文件,我们一直使用的是FileChannel,没有与网络通信结合起来。从今天开始,我们将进入网络编程领域,使用NIO中的SocketChannel
和ServerSocketChannel
,为后续的Netty学习打下坚实的基础。
基础扫盲
由于我们现在的开发都是使用的SpringBoot,底层的网络通信都被Tomcat、Jetty等所处理,我们很少能够接触Socket套接字编程,这一个段落就来个基础性知识的扫盲。我们的客户端与服务端之间是使用Socket进行通信的,Socket是计算机网络中的传输层的内容,和TCP与UDP挂钩。在服务端有一个东西叫做ServerSocket,它是用来接收客户端请求,然后和客户端建立Socket连接的。下面的演示案例中,我写了一个服务端和客户端的案例。
服务端就是创建ServerSocketChannel
,绑定端口启动,监听请求获取对应的SocketChannel
客户端就是去连接服务端所在机器的对应端口,发送数据相应数据即可
注意:
- 服务端有两次阻塞,第一次阻塞是接收请求的时候,第二次阻塞是建立后连接等待接收数据的时候。说明NIO是有阻塞的,后面会写案例解决阻塞的
- 端口,是应用进程启动后在传输层给其分配的,客户端是一个进程,服务端是一个进程。有了端口号两个进程就可以跨进程互相访问
- 当服务端启动,就是一个主线程,接收请求和数据处理都是由主线程挨个处理,只要SocketChannel一经建立,服务端或者客户端不主动销毁就会一直在,但其中会不会有数据是两码子事
public class MyServer {
public static void main(String[] args) throws Exception{
// 1、创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2、设置服务端的监听端口
serverSocketChannel.bind(new InetSocketAddress(8000));
List<SocketChannel> channelList = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(20);
// 3、接受客户端的连接,让它一直转(我们不知道啥时候会接收到请求)
while (true) {
// 4、SocketChannel代表服务端与Client链接的一个通道
System.out.println("等待连接服务器...");
// 发生阻塞,服务端在等待客户端的请求,接收到才会放行
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("服务器已连接..." + socketChannel);
// 4-1、接受一个客户端就存一个
channelList.add(socketChannel);
// 5、client与服务端 通信 NIO
for (SocketChannel channel : channelList) {
System.out.println("开始实际的数据通信...");
// 此处阻塞,对于的IO通信的阻塞,将channel中的数据读取到buffer中
channel.read(buffer);
// 开启读模式
buffer.flip();
CharBuffer result = StandardCharsets.UTF_8.decode(buffer);
System.out.println("result = " + result);
// 读完后开启写模式
buffer.clear();
System.out.println("实际的通信已经结束...");
}
}
}
}
public class MyClient {
public static void main(String[] args) throws Exception{
// 1、创建客户端channel,并连接8000端口服务端
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(8000));
// 2、向服务端发送数据
socketChannel.write(Charset.defaultCharset().encode("你好,我是Aomsir\n"));
System.out.println("-------------------------------------");
}
}
问题所在
在上面的案例中,服务端程序会面临两次阻塞。首先,在接收连接请求时,服务端程序需要等待新的连接请求。其次,一旦建立连接后,服务端可能需要等待客户端发送数据。这可能会导致一些问题:
- 单客户端多次请求问题: 如果今天只有一个客户端请求,按照上面的案例代码我们知道服务端在处理完这个客户端的请求后双方都没有选择断开Channel,那客户端第一次给服务端发送完数据后就会回到循环的开始一直阻塞新的连接请求到来,而不会去执行下面的IO处理,导致第一个客户端的后续请求得不到处理。这是当前单线程服务端模型的局限性,只有在接受新连接后才能处理请求。 这是由于ServerSocketChannel等待客户端请求时的阻塞
- 客户端保持连接问题: 第一个客户端发送完数据后没有断开与服务端的连接,服务端channelList中第一个Channel永远是第一个客户端的,服务端会一直等待来自该客户端的数据,但是第一个客户端已经没有数据发送了,这会导致服务端阻塞无法处理其他连接,因为它会一直在等待当前连接上的数据。这是由于SocketChannel处理IO数据时的阻塞。这需要特别处理的情况,通常可以通过设置超时或实现多线程来解决。
调整非阻塞
在上面的代码中,我们使用了标准的NIO编程方法,但同时又称它为非阻塞编程。然而,在代码中的两次阻塞操作似乎没有展现出非阻塞的特性,这是因为我们还未进行必要的设置。通过对ServerSocketChannel和SocketChannel的配置,我们可以将它们转换为非阻塞模式。只需执行以下两行代码:serverSocketChannel.configureBlocking(false);
和 socketChannel.configureBlocking(false);
。
一旦这些设置生效,之前阻塞的客户端请求接收和IO处理不再会阻塞整个程序。当没有客户端请求或没有进行IO请求时,从ServerSocketChannel获取的SocketChannel将为null,从SocketChannel中读取的字节数将为0。为了进一步说明这一点,以下是示例代码:
public class MyServer1 {
public static void main(String[] args) throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置serverSocketChannel为非阻塞,解决连接阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8000));
List<SocketChannel> channelList = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(20);
while (true) {
// 此处等待请求不会阻塞,如果此时没有客户端连接进来,会返回null
SocketChannel socketChannel = serverSocketChannel.accept();
// 4、如果接收到的socketChannel不为null,则将其添加到channelList中
if (socketChannel != null) {
// 设置socketChannel为非阻塞,解决数据读取阻塞
socketChannel.configureBlocking(false);
channelList.add(socketChannel);
}
// 5、客户端与服务端 通信 NIO
for (SocketChannel channel : channelList) {
// 此处从channel中读取数据到buffer中会阻塞
int read = channel.read(buffer);
if (read > 0) {
System.out.println("开始实际的数据通信...");
buffer.flip(); // 开启读模式
CharBuffer result = StandardCharsets.UTF_8.decode(buffer);
System.out.println("result = " + result);
buffer.clear(); // 开启写模式
System.out.println("实际的通信已经结束...");
}
}
}
}
}
技巧
对于一个Java类,比如我们的客户端,我们可能需要在IDEA中运行多个。但是默认情况下我们在IDEA里面将客户端启动以后,它处于一直运行的状态,如果我这个时候再去点击运行,那就会将这个客户端重新运行,而不是启动一个新的进程,我们可以按照下面的方式进行设置。
- 打开IntelliJ IDEA,并确保你的项目已经打开。
- 在IDEA的顶部菜单栏中,点击 “Run”(运行)。
- 选择 “Edit Configurations”(编辑配置)。
- 在左侧窗格中,点击加号(+)以创建一个新的运行配置。
- 选择 “Application”(应用程序)作为配置类型。
- 在 “Name”(名称)字段中,为你的运行配置命名,例如 “Client1”。
- 在 “Main class”(主类)字段中,指定你想要运行的Java客户端的主类。
- 在 “Program arguments”(程序参数)字段中,如果有需要的话,添加客户端程序的参数。
- 在 “Working directory”(工作目录)字段中,指定客户端应运行的工作目录。
- 点击 “OK” 以保存该运行配置。
- 重复上述步骤,创建其他客户端的运行配置,每个配置使用不同的名称,主类和参数。
- 现在,你可以通过选择不同的运行配置来启动不同的客户端。在顶部工具栏 中,选择你想要运行的配置,然后点击运行按钮(绿色的播放按钮)。
总结
在今天的文章中,我们将深入探讨SocketChannel和ServerSocketChannel,这两个在网络通信中起着关键作用的组件。我们将揭开网络通信中服务端的两次阻塞现象的神秘面纱,并详细介绍NIO如何有效地处理非阻塞。让我们一起扫除对这些概念的盲点,深入理解网络通信的内在机制。