内容来自《深入分析Java Web技术内幕》和《Netty实战》两本书的个人总结,感谢二位作者!
目录
一、Java Socket的工作机制
二、建立通信链路
三、BIO(阻塞I/O示例)
四、Java NIO
五、Netty
一、Java Socket的工作机制
Socket ,它描述计算机之间完成相互通信的一种抽象功能,可以把Socket比作两个城市之间的交通工具,有了它,就可以在城市之间来回穿梭。交通工具有多种,每种交通工具也有相应的交通规则。Socket也一样,也有多种。大部门情况下我们使用的都是基于TCP/IP的流套接字,它是一种稳定的通信协议。
主机应用程序相互通信,需要通过Socket建立连接,而建立Socket连接必须由底层TCP/IP来建立TCP连接。建立TCP连接需要底层IP来寻址网络中的主机。如何才能与指定的应用程序通信就要通过TCP或UDP的地址也就是端口号来指定。这样就可以通过一个Socket实例来唯一外表一个主机上的应用程序的通信链路。
二、建立通信链路
当客户端要与服务端通信时,客户端首先要创建一个Socket实例,操作系统将为这个Socket实例分配一个没有被使用的本地端口号,并创建一个包含本地地址、远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个链接关闭。创建Socket实例的构造函数正确返回之前,将要进行TCP的3次握手协议,TCP握手协议完成后,Socket实例对象将创建完成,否则会抛出IO异常。
三、BIO(阻塞I/O示例)
//创建一个新的ServerSocket,用以监听指定端口上的连接请求
ServerSocket serverSocket = new ServerSocket(portNumber);
//对accept()方法的调用将被阻塞,直到一个连接建立
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//这些流对象都派生于该套接字的流对象
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String request, response;
//处理循环开始
while((request = in.readLine()) != null){
//如果客户端发送了"Done"则退出处理循环
if("Done".equals(request)){
break;;
}
//请求被传递给服务器的处理方法
response = processRequest(request);
//服务器的响应被发送给了客户端
out.println(response);
}
- ServerSocket上的accept()方法将会一直阻塞到一个连接建立,随后返回一个新的Socket用于客户端和服务器之间的通信。该ServerSocket将继续监听传入的连接。
- BufferedReader和PrintWriter都衍生自Socket的输入输出流。前者从一个字符输入流中读取文本,后者打印对象的格式化的标识到文本输出流。
此方案的问题:
- 在任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这可能算是一种资源浪费。
- 需要为每个线程的调用栈都分配内存,其默认值大小区间为64KB到1MB,具体取决于操作系统。
- 即使Java虚拟机在物理上可以支持非常大数量的线程,但是远在到达极限之前,上下文切换所带来的开销就会带来麻烦,例如,在达到一万个连接的时候。
四、Java NIO
NIO中两个核心概念:Channel和Selector,Channel可以把它比作某种具体的交通工具,如汽车或高铁,Selector好比一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态,是已经出战,还是在路上等。可以轮询每个Channel的状态。Buffer比作车上的座位,这些信息都已经呗封装在了运输工具(Socket)里面。
// 1、获取Selector选择器
Selector selector = Selector.open();
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(8080));
// 5、将通道注册到选择器上,并注册的操作为:“接收”操作
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6、采用轮询的方式,查询获取“准备就绪”的注册过的操作
while (selector.select() > 0) {
// 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
// 8、获取“准备就绪”的时间
SelectionKey selectedKey = selectedKeys.next();
// 9、判断key是具体的什么事件
if (selectedKey.isAcceptable()) {
// 10、若接受的事件是“接收就绪” 操作,就获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 11、切换为非阻塞模式
socketChannel.configureBlocking(false);
// 12、将该通道注册到selector选择器上
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectedKey.isReadable()) {
// 13、获取该选择器上的“读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
// 14、读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int length = 0;
while ((length = socketChannel.read(byteBuffer)) != -1) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, length));
byteBuffer.clear();
}
socketChannel.close();
}
// 15、移除选择键
selectedKeys.remove();
}
}
// 7、关闭连接
serverSocketChannel.close();
调用Selector的静态工厂创建一个选择器,创建一个服务端的Channel,绑定到一个Socket对象,并把这个通信信道注册到选择器上,把这个通信信道设置为非阻塞模式。然后就可以调用Selector的selectedKeys方法来检查已经注册在这个选择器上的所有通信信道是否有需要的事件发生,如果有某个事件发生,建辉返回所有的SelectionKeys,通过这个对象Channel方法就可以取得这个通信信道对象,从而读取通信的数据,而这里读取的数据是Buffer,这个Buffer是我们可以控制的缓冲器。
我们通常会使用一个线程专门负责监听客户端的连接请求,而且是以阻塞方式执行;另外一个线程专门负责处理请求,这个专门处理请求的线程才会真正采用NIO的方式。
与阻塞I/O模型相比,这种模型提供了更好的资源管理:
- 使用较少的线程便可以处理许多连接,因此也减少了内存管理和山下文切换所带来的开销。
- 当没有I/O操作需要处理的时候,线程也可以被用于其他任务。
五、Netty
分类 | Netty的特性 |
设计 | 统一的API,支持多种传输类型 阻塞和非阻塞;简单而强大的线程模型 真正的无连接数据报套接字支持 连接逻辑组件支持复用 |
易于使用 | 详实的Javadoc和大量的示例集 |
性能 | 拥有比Java的核心API更高的吞吐量以及更低的延迟 得益于池化复用,拥有更低的资源消耗 最少的内存复制 |
健壮性 | 不会因为慢速、快速或者超载的连接而导致OOM 消除在高速网络中NIO应用程序常见的不公平读/写比率 |
安全性 | 完整的SSL/TLS以及StartTLS支持 可用于受限于环境下,如Applet和OSGI |
社区驱动 | 发布快速而且频繁 |
异步:也就是非同步,非阻塞网络调用使得我们可以不必等待一个操作的完成。完全异步的I/O正是基于这个特性构建的,并且更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户。
选择器使得我们能够通过较少的线程便可监视许多连接上的事件。
事件驱动