服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动
一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.ioBIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
通过BIO+线程池完成少量用户的通信架构
通过线程池控制解决为每个请求创建一个独立线程造成线程资源耗尽的问题。
存在的问题:
但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
public class Server {
public static void main(String[] args) {
try {
//注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//初始化一个线程对象
HandlerSocketServerPool pool = new HandlerSocketServerPool(5, 20);
//循环接受客户端的请求
while (true){
Socket socket = serverSocket.accept();
//将socket封装成一个Runnable线程交给线程池
ServerRunnableTarget runnable = new ServerRunnableTarget(socket);
pool.execute(runnable);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//线程池类
class HandlerSocketServerPool{
private ExecutorService executorService;
//创建类的对象的时候初始化线程池对象
public HandlerSocketServerPool(int maxThreadNum,int queuSize){
executorService = new ThreadPoolExecutor(3, maxThreadNum, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queuSize));
}
/**
* 提交一个方法来提交任务给线程池的任务队列来暂时存储,等着线程池的处理
*
* */
public void execute(Runnable target){
executorService.execute(target);
}
}
class ServerRunnableTarget implements Runnable{
private Socket socket;
public ServerRunnableTarget(Socket socket){
this.socket=socket;
}
@Override
public void run() {
//从Sacket得到一个字节输入流
try {
InputStream inputStream = socket.getInputStream();
//使用缓冲字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
while ((s=bufferedReader.readLine())!=null){
System.out.println(s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
Socket socket = new Socket("127.0.0.1",9999);
OutputStream outputStream = socket.getOutputStream();
//打印流
PrintStream printStream = new PrintStream(outputStream);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请说:");
String s = scanner.nextLine();
printStream.println(s);
printStream.flush();
}
}
}
channel和用户操作IO相连,但通道的使用是不能直接访问数据的需要和缓冲区Buffer相连
读数据:将数据从channel中读取到Buffer,从Buffer在获取到数据
写数据:将数据线写入Buffer,Buffer中的数据写入到通道
channel与流stream的区别:
channel不仅能读,也能写,stream通常是要么读要么写
channel可以同步也可以异步写
channel总是读取或写入一个Buffer中
主要的实现类:
FileChannel:用于读取、写入、映射和操作文件的通道
DatagramChannel:通过UDP读写网络中的数据通道
SocketChannel:通过TCP读写网络中的数据,一般是客户端的实现
ServerSocketChannel:监听新进来的TCP连接,对每一个连接创建一个SocketChannel,一般是服务端的实现
基于SocketChannel和ServerSocketChannel实现C/S大致流程:
服务端
1.通过ServerSocketChannel 绑定ip地址和端口号
2.通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
3.创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据
4. 关闭SocketChannel和ServerSocketChannel
Scatter / Gather( 散射/采集 )
Scatter/Gather应该使用直接的ByteBuffers以从本地I/O获取最大性能优势。
Scatter/Gather功能是通道(Channel)提供的 并不是Buffer。
Scatter:从一个Channel读取的信息分散到N个缓冲区中(Buufer).
Gather:将N个Buffer里面内容按照顺序发送到一个Channel
Java NIO 的 Buffer 用于和 NIO Channel(通道)交互。数据是从通道读入缓冲区,从缓冲区写入到通道中。缓冲区本质上是块可以写入数据,再从中读数据的内存。该内存被包装成 NIO 的 Buffer 对象,并提供了一系列方法,方便开发者访问该块内存
使用Buffer读写数据一般四步走:
1、写数据到 Buffer
2、调用buffer.flip切换为读模式
3、从Buffer中读取数据
4、调用clear()或者compact()清除数据
当向 buffer 写数据时,buffer 会记录写了多少数据。一旦要读取数据,需通过 flip() 将 Buffer 从写模式切到读模式。在读模式下,可读之前写到 buffer 的所有数据。一旦读完数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。
clear() 会清空整个缓冲区
compact() 只会清除已经读过的数据。任何未读数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer就像一个数组,可以保存多个相同类型的数据。根据
数据类型不同 ,有以下 Buffer 常用子类:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer的实现底层是通过特定类型(byte、long…)数组来存储数据
数组中数据的操作需要借助4个指针来操作:
private int mark = -1; //标记
private int position = 0; //位置
private int limit; //限制
private int capacity; 容量
// Invariants: mark <= position <= limit <= capacity
容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小,
也称为"容量",缓冲区容量不能为负,并且创建后不能更改。
限制 (limit):表示缓冲区中可以操作数据的大小
(limit 后数据不能进行读写)。缓冲区的限制不能
为负,并且不能大于其容量。 写入模式,限制等于
buffer的容量。读取模式下,limit等于写入的数据量。
位置 (position):下一个要读取或写入的数据的索引。
缓冲区的位置不能为 负,并且不能大于其限制
标记 (mark)与重置 (reset):标记是一个索引,
通过 Buffer 中的 mark() 方法 指定 Buffer 中一个
特定的 position,之后可以通过调用 reset() 方法恢
复到这 个 position.
标记、位置、限制、容量遵守以下不变式:
0 <= mark <= position <= limit <= capacity
Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
int capacity() 返回 Buffer 的 capacity 大小
boolean hasRemaining() 判断缓冲区中是否还有元素
int limit() 返回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 将设置缓冲区界限为 n,并返回一个具有新 limit 的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置 position
Buffer position(int n) 将设置缓冲区的当前位置为 n,并返回修改后的 Buffer 对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark
要想获得一个Buffer对象首先要进行分配。每个Buffer类都有一个allocate方法。
直接与非直接缓冲区
ByteBufferbyte byffer可以是两种类型,一种是基于直接内存(也就是
非堆内存);另一种是非直接内存(也就是堆内存)。对于直
接内存来说,JVM将会在IO操作上具有更高的性能,因为它
直接作用于本地系统的IO操作。而非直接内存,也就是堆内
存中的数据,如果要作IO操作,会先从本进程内存复制到直接
内存,再利用本地IO处理。
从数据流的角度,非直接内存是下面这样的作用链:
本地IO–>直接内存–>非直接内存–>直接内存–>本地IO
而直接内存是:
本地IO–>直接内存–>本地IO
很明显,在做IO处理时,比如网络发送大量数据时,直接内
存会具有更高的效率。直接内存使用allocateDirect创建,但
是它比申请普通的堆内存需要耗费更高的性能。不过,这
部分的数据是在JVM之外的,因此它不会占用应用的内
存。所以呢,当你有很大的数据要缓存,并且它的生命
周期又很长,那么就比较适合使用直接内存。只是一般
来说,如果不是能带来很明显的性能提升,还是推荐直接
使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲
区可通过调用其 isDirect() 方法来确定。
Buffer的创建:
ByteBuffer为例:
ByteBuffer allocate(int capacity):在堆上创建指定大小的缓冲
ByteBuffer allocateDirect(int capacity):在堆外空间创建指定大小的缓冲
ByteBuffer wrap(byte[] array):通过byte数组实例创建一个缓冲区
ByteBuffer wrap(byte[] array, int offset, int length) 指定byte数据中的内容写入到一个新的缓冲区
向Buffer写数据
写数据到Buffer有两种方式:
1、从Channel写到Buffer
inChannel.read(buf);
2、通过Buffer的put()方法写到Buffer里
buf.put(127);
flip()方法:
flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等 —— 现在能读取多少个byte、char等。
从Buffer读数据
两种方式:
1、从Buffer读取数据到Channel。
int bytesWritten = inChannel.write(buf);
2、使用get()方法从Buffer中读取数据。
byte aByte = buf.get();
get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。
mark()与reset()方法
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:
buffer.mark();// call buffer.get() a couple of times, e.g. during parsing.buffer.reset();
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心
Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,且不必为每个连接都创建一个线程,不用去维护多个线程避免了多线程之间的上下文切换导致的开销
selector优势:
使用更少的线程管理更多的通道了,相比多线程,减少了上下文切换
创建 Selector :
通过调用 Selector.open() 方法创建一个 Selector。
向选择器注册通道:
SelectableChannel.register(Selector sel, int ops)
举例:
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
当调用 register(Selector sel, int ops) 将通道注册选择
器时,选择器对通道的监听事件,需要通过第二个参
数 ops 指定。可以监听的事件类型(用 可使
用 SelectionKey 的四个常量 表示):
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
如:
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE
Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
服务器端:可以监测用户上线,离线,并实现消息转发功能
客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息
//服务端群聊系统实现
public class Server {
//定义一些成员属性:选择器 服务器通道 端口
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private static final int PORT = 9999;
//定义初始化代码逻辑
public Server(){
//接受选择器
try {
//初始化选择器
selector = Selector.open();
//初始化通道
serverSocketChannel=ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(PORT));
//通道切换为非阻塞模式
serverSocketChannel.configureBlocking(false);
//将通道注册到选择器上,并且开始指定监听接收事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 监听客户端各种消息事件:连接、群聊、离线
* */
private void listen(){
try {
//循环判断是否存在就绪事件
while (selector.select()>0){
//获取选择器中的所有注册的通道中已经就绪好的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//开始遍历这些准备好的事件
while (iterator.hasNext()){
//提取当前事件
SelectionKey sk = iterator.next();
//判断是否为可接收事件
if (sk.isAcceptable()){
//获取当前接入的客户端通道
SocketChannel socketChannel = serverSocketChannel.accept();
//通道切换为非阻塞模式
socketChannel.configureBlocking(false);
//将本客户端的通道注册到选择器上
System.out.println(socketChannel.getRemoteAddress() + " 上线 ");
socketChannel.register(selector,SelectionKey.OP_READ);
}
//判断是否为可读事件
if(sk.isReadable()){
//读操作和转发给其他客户端
readClientData(sk);
}
iterator.remove();//处理完毕移除当前事件
}
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 接收当前客户端发送的消息,并转发给全部客户端通道
* */
private void readClientData(SelectionKey sk){
SocketChannel socketChannel = null;
try {
//取到该读操作的通道
socketChannel = (SocketChannel) sk.channel();
//创建缓冲区对象开始接收客户端发送的消息
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = socketChannel.read(buffer);
if(count>0){
//设置为读模式
buffer.flip();
//提取读取到的信息
String msg = new String(buffer.array(),0,count);
System.out.println("接收到了客户端信息:"+msg);
//返回给其他在线客户端消息
sendMsgToAllClient(msg,socketChannel);
}
}catch (Exception e){
//该客户断开连接会抛出异常,异常发出下线通知
try {
System.out.println(socketChannel.getRemoteAddress()+"下线了");
sk.channel();
//关闭通道
socketChannel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
//发送消息给所有在线人
private void sendMsgToAllClient(String msg,SocketChannel socketChannel) throws Exception{
System.out.println("服务端开始转发消息,当前处理的线程" + Thread.currentThread().getName());
//循环给所有在线通道发送消息
for (SelectionKey key:selector.keys()){
Channel channel = key.channel();
//不要把数据发送服务器和自己
if(channel instanceof SocketChannel && socketChannel!=channel){
//将消息存储到buffer缓存
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将缓存写入到通道
( (SocketChannel)channel).write(buffer);
}
}
}
public static void main(String[] args) {
//创建服务端对象
Server server = new Server();
//开始监听客户端各种消息事件:连接、群聊、离线
server.listen();
}
}
//客户端群聊系统实现
public class Client {
private Selector selector;
private static final int PDRT = 9999;
private SocketChannel socketChannel;
public Client(){
try {
//初始化选择器
selector = Selector.open();
//初始化通道,并绑定通信地址与端口
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",PDRT));
//通道设置为非阻塞模式
socketChannel.configureBlocking(false);
//将通道加载到选择器上,并开始指定监听读事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("当前客户端准备完成");
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
//定义一个线程负责监听服务端发来的线程消息
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
//接收读事件
client.readInfo();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
//主线程进行发送消息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s= scanner.nextLine();
//数据发送
client.sendTOServer(s);
}
}
private void sendTOServer(String s) {
try {
//数据经过缓存加载到通道上
socketChannel.write(ByteBuffer.wrap(s.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*
*
* */
private void readInfo() throws IOException {
//判断选择器是是否有就绪事件
if(selector.select()>0){
//循环处理这些准备就绪的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
//取得当前就绪事件
SelectionKey key = iterator.next();
//判断当前是否为读事件
if(key.isReadable()){
//取得当前通道
SocketChannel selectableChannel = (SocketChannel) key.channel();
//创建buffer缓存接收事件
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取通道上的数据
socketChannel.read(buffer);
//输出缓存中数据
System.out.println(new String(buffer.array()).trim());
}
//处理完关闭该通道
iterator.remove();
}
}
}
}
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;
而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
NIO比BIO最大的好处是,一个线程可以处理多个socket(channel),这样NIO+多线程会提高网络服务器的性能,最主要是大大降低线程的数量
服务器线程数量过多对系统有什么影响?
1.java里面创建进程和线程,最终映射到本地操作系统上创建进程和线程,拿Linux来说,fork(进程创建函数)和pthread_create(线程创建函数)都是重量级的函数,调用它们开销很大
2.多线程随着CPU的调度,会有上下文切换,如果线程过多,线程上下文切换的时间花费慢慢趋近或者大于线程本身执行指令的时间,那么CPU就完全被浪费掉了,大大降低了系统的性能
3.线程的开辟伴随着线程私有内存的分配,如果线程数量过多,为线程运行准备的内存占去很多,真正能用来分配做业务处理的内存大大减少,系统运行不可靠
4.数量过多的线程,阻塞等待网络事件发生,如果一瞬间客户请求量比较大,系统会瞬间唤醒很多数量的线程,造成系统瞬间的内存使用率和CPU使用率居高不下,服务器系统不应该总是出现锯齿状的系统负载,内存使用率和CPU使用率应该持续的保证平顺运行