文章目录
- 〇、前置知识
- 1.套接字
- 2.线程池
- 一、BIO
- 1.工作流程
- 2.代码实现
- 3.缺点
- 二、NIO(基于轮训)
- 1.相比于BIO的优化
- 2.工作流程
- 3.代码实现
- 三、AIO(基于订阅-通知)
- 1.工作流程
- 2.代码实现
- 参考
〇、前置知识
1.套接字
在计算机网络编程技术中,两个进程或者说两台计算机可以通过一个网络通信连接实现数据的交换,这种通信链路的端点就被称为“套接字”(英文名称也就是Socket)。在Java语言中,服务端使用ServerSocket套接字,客户端使用Socket套接字。
- ServerSocket:
// 创建ServerSocket对象
ServerSocket(int port):这个使用指定的端口去创建ServerSocket,IP地址使用默认的本地IP地址
ServetSocket(int port,int backlog):除了端口外,还有一个用来改变队列长度参数的backlog,指定当服务器繁忙时,可以与之保持连接请求的客户端数量,默认为50
ServetSocket(int port,int backlog,InetAddress ip):这个使用指定的端口、backlog、地址去创建ServetSocket
// 两个静态方法获取InetAddress对象:
getByName(String hostName)
getByAddress(byte[] address)
// accept()
serverSocket.accept():没有参数,返回一个Socket,如果接收到客户端的一个Socket,则返回,否则一直处于等待状态,线程也被阻塞。
- Socket:
// 创建Socket对象
Socket(InetAddress address,int port):使用指定IP与指定端口构造Socket,默认使用本地ip,端口则动态分配
Socket(String address,int port):使用String表示IP
Socket(InetAddress address,int port,InetAddress localAddr,int localPort):创建指定了远程IP、远程端口、本地IP、本地端口的Socket
Socket(String address,int port,InetAddress localAddr,int localPort):使用String表示远程IP,用InetAddress表示本地IP
- 简单代码示例:
package socket;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException{
ServerSocket serverSocket = new ServerSocket(8888);
while(true){
Socket socket=serverSocket.accept();//阻塞等待客户端连接
PrintStream printStream = new PrintStream(socket.getOutputStream());//创建输出流
printStream.println("message from server 8888");
printStream.close();
socket.close();
}
}
}
package socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException{
Socket socket = new Socket("127.0.0.1", 8888);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("This message comes from server:"+bufferedReader.readLine());
bufferedReader.close();
socket.close();
}
}
跑一下试试,成功!
2.线程池
请看我写的另一篇博客:跳转
一、BIO
BIO,即Blocking IO,阻塞型I/O。阻塞体现在两个地方,连接线程的阻塞和读写的阻塞。
1.工作流程
服务端启动ServerSocket;
客户端启动 Socket 对服务器进行通信,服务端对每个客户端建立一个线程与之通讯(可以使用线程池进行优化);
客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待(即阻塞);
如果有响应,客户端线程会等待请求结束后,再继续执行。
2.代码实现
- 服务端:
package bio;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class BIOServer {
//创建一个线程池,用于处理客户端连接后的工作
public static ThreadPoolExecutor pool=new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
public static void main(String[] args) throws IOException{
ServerSocket serverSocket=new ServerSocket(8888);
while(true){
//1 等待客户端连接是阻塞的
Socket socket=serverSocket.accept();
System.out.println("客户端连接上了");
//2 连接上以后向线程池提交一个任务用于处理连接
pool.execute(new Runnable() {
@Override
public void run() {
while(true){
try{
//读写也是阻塞的
//创建输出流,server向client输出
PrintStream printStream = new PrintStream(socket.getOutputStream());
printStream.println("message from server 8888");
printStream.close();
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
});
}
}
}
- 客户端:
package bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class BIOClient {
public static void main(String[] args) throws IOException{
Socket socket = new Socket("127.0.0.1", 8888);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("This message comes from server:"+bufferedReader.readLine());
bufferedReader.close();
socket.close();
}
}
3.缺点
- accept()等待客户端连接是阻塞的,有时候需要进行无谓的等待,效率低下,浪费资源。
- 引入线程池进行优化提升了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃。
- 读写操作仍然是阻塞的,如果客户端半天没有操作,也会浪费资源,因此效率不高。
二、NIO(基于轮训)
1.相比于BIO的优化
NIO,即non-blocking lO,非阻塞型IO。
-
非阻塞——减少线程资源的浪费:
BIO提供非阻塞读写模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。可以做到 用一个线程来处理多个操作,体现了一种多路复用的思想。 而不是像BIO那样,一个连接过来就得分配一个线程,造成资源的浪费。 -
三大核心组件——提升效率:
NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
2.工作流程
Channel(通道),Buffer(缓冲区), Selector(选择器)为NIO的三大核心组件。
-
Channel(通道):
相比于BIO流的读写,Channel的读写是双向的,既可以从通道中读取数据,又可以写数据到通道。通道可以非阻塞读取和写入通道/缓冲区,也支持异步地读写。 -
Buffer(缓冲区):
在客户端和Channel之间,增加Buffer缓冲区的支持,更加容易操作和管理。 -
Selector(选择器):
用来检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率。
3.代码实现
代码来自:here
- 服务端:
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 创建一个Selector对象,
Selector selector = Selector.open();
// 绑定端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 把serverSocketChannel注册到selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环等待用户连接
while (true){
if (selector.select(1000) == 0){ //等待(阻塞)一秒, 没有事件发生
// if (selector.selectNow() == 0){ // 也可以设置成非阻塞的
System.out.println("服务器等待了一秒,无连接");
continue;
}
// 如果返回的>0 , 说明客户端有了动作,就获取相关的selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 返回关注事件的集合
// 遍历selectionKeys
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
// 获取到selectionKey
SelectionKey key = keyIterator.next();
//根据key对应的通道获取事件并做相应处理
if (key.isAcceptable()){
//如果是OP_ACCEPT, 表示有新的客户端产生
//给该客户端生成SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将socketChannnel设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到selector上, 设置事件为OP_READ,同时给socketChannel关联一个buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()){
// 发生了OP_READ
SocketChannel channel=(SocketChannel)key.channel();
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("from 客户端"+new String(buffer.array()));
}
// 手动从集合中移除当前的selectionKey, 防止多线程情况下的重复操作
keyIterator.remove();
}
}
}
}
- 客户端:
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
// 获取一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞
socketChannel.configureBlocking(false);
//设置服务器端ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
if (!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
//如果没有连接成功,客户端是非阻塞的,可以做其它工作
System.out.println("等待连接...");
}
}
// 如果连接成功,就发送数据
String str = "hello world";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
// 发送数据 , 将buffer中的数据写入到channel中
socketChannel.write(buffer);
System.in.read();
}
}
三、AIO(基于订阅-通知)
AIO,即Asynchronous I/O,异步非阻塞IO。AIO提供的最大的特点是具备异步功能,采用“订阅-通知”模式,即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。
1.工作流程
2.代码实现
请看他的blog:here
参考
套接字:https://blog.csdn.net/qq_27525611/article/details/102633014
BIO:https://blog.csdn.net/java_lg/article/details/126274158
NIO:https://blog.csdn.net/K_520_W/article/details/123454627
NIO:https://www.cnblogs.com/jobbible/p/16913990.html
AIO:https://zhuanlan.zhihu.com/p/504968873