使用java nio编写高性能的服务器_使用javaNIO提高服务端性能

Java NIO 进行网络编程

本文由大关总结整理所得,不保证文中正确性,转载请标明出处!

Java NIO中在原有IO的基础上提供了新的特性,并且在Java

1.4之后所有的java基本IO类库也使用NIO进行了重写,因此,在更多的时候,仅使用原有的IO库中的操作,也可以从Java

NIO中获益。

Java

NIO主要提供了两大方面的性能提高,一方面是对文件的操作,可以使用直接映射方式,将一个文件整体映射到内存(使用虚拟内存方式),可以在同一时刻,使用多个线程,甚至是多个进程对文件的不同部分进行操作,同时为文件提供了文件锁机制,使得对文件的操作更加灵活。另外一方面,对于java网络编程的性能提升也是很明显的。在java

NIO中为服务器端提供了非阻塞式服务(异步服务),使用异步服务方式,可以大量的减少服务端的线程创建的数量,从而减少频繁切换线程空间造成的开销,提高了系统的执行性能。

下面的内容主要从网络编程出发,给出如何使用NIO这种异步服务的方式提高服务端的性能,并简要论述了性能是如何得到提升的。

1 传统的服务端连接

使用传统的方式进行搭建的服务端,在每接收一个客户端的请求,服务端就创建一个新的线程,用来处理这个请求,但是在处理过程中,这些新建的线程往往需要等待客户端的输入和其他操作,需要阻塞式等待,因此,如果一个服务器负载很重,他就需要创建大量的这种等待的线程,cpu需要花费大量的时间去查询这些阻塞线程的状态是否可用,而且每次进行查询的时候需要切换线程的状态,造成了很大的性能开销。

Demo-1中给出了这种使用传统模式搭建的交互服务模型。Demo-1中客户端先向服务端发送一条消息,服务端接收这条消息,然后在这条消息开始处添加Hello后,将原消息反馈给客户端,客户端读取服务端的反馈信息。

在Demo-1中,服务端每接收一个客户端的请求,就创建一个新的线程处理这个请求。如果线程在处理过程中需要等待客户端的输入,那么服务端就需要阻塞等待客户端的输入接收完成,因此有可能造成很多线程都在等待客户端输入,因此浪费了大量的系统资源。

Demo-1 一个回声测试的服务器与客户端

package com.upc.upcgrid.guan.chapter02;

import java.io.BufferedInputStream;

import java.io.IOException;

import java.io.OutputStream;

import java.net.ServerSocket;

import java.net.Socket;

import java.net.UnknownHostException;

import java.util.Scanner;

import org.junit.Test;

public class SocketEchoTest {

public static final int

serverPort = 21212;

@Test

public void service() throws

IOException//服务端

{

ServerSocket server = new

ServerSocket(serverPort);

Socket socket;

try{

while(true)//不断接受请求

{

socket = server.accept();//阻塞等待新的请求到来

new OnceServer(socket).start();//创建一个单独的线程来处理请求

}

}finally{

server.close();//关闭服务

}

}

class

OnceServer extends Thread{

Socket socket;

public OnceServer(Socket socket) {

this.socket = socket;

}

public void run() {

try {

System.out.println("new request");

BufferedInputStream in = new

BufferedInputStream(socket.getInputStream());

OutputStream out = socket.getOutputStream();

//从客户端读取一条信息

Scanner scan = new Scanner(in);

String s = scan.nextLine();

System.out.println("server info : "+ s);

//向客户端写出一条信息

out.write(("hello " + s).getBytes());

out.flush();

} catch (IOException e) {

}finally{

try {

socket.close();//关闭socket

} catch (IOException e) {

}

}

}

}

@Test

public void client() throws

UnknownHostException, IOException

{//客户端

Socket socket = null;

try{

socket = new Socket("localhost",

serverPort);//创建与服务器的链接

BufferedInputStream in = new

BufferedInputStream(socket.getInputStream());

//向服务端写入一条信息

OutputStream out = socket.getOutputStream();

out.write("woshiguanxinquan\n".getBytes());

out.flush();

//从服务端获取以条反馈信息

Scanner scanner = new Scanner(in);

System.out.println(scanner.nextLine());

}finally{

socket.close();//关闭与服务端的链接

}

}

}

.2 使用java NIO改善服务器性能

使用java NIO提供的异步等待方式,可以大大降低服务器端的负载,提升服务器的连接能力,提升服务器的响应能力。

在java

NIO中提供的提升服务端性能相关的类,可分为三种类型,第一种是通道,一般可以认为通道等同于一个文件描述符,在web编程中通道可以相当于socket的InputStream和OutputStream,在非Web编程过程中,通道还可以表示管道(用于进程间通信,或者线程间通信),文件,标准输入输出等。其次是监视器(Select),监视器类似于观察者模式中的观察者,它负责观察注册到本Select的通道关心的事件(例如:socket中从客户端传送过来的数据已经准备好,可以随时读取,或者向客户端的传输已经建立,现在可以向客户端写数据了),一旦有通道变得可用,Select就会从睡眠中被唤醒,继续执行可用通道的后续操作。最后是SelectKey,就是键值,这个键值用来连接一个通道,和通道关心的事件,当Select返回后,会将准备好的通道以Key的方式反馈给服务端,服务端通过遍历这些期望事件已经发生通道(Key中关联了相应的通道,和具体发生变化的事件类型),之后执行相应通道在相应事件发生后的处理操作。

在上述的三种类型中,Select是最为复杂的,它类似于Linux中Select的功能。在Java中,Select是调用操作系统的系统函数完成事件查询和等待的。首先Select向系统注册其关切的通道和相应通道关心的事件,之后选择器进入睡眠状态(睡眠也应当是一种阻塞状态),操作系统负责监视这些通道是否有关注的事件发生,一旦其中有一个或者多个通道的注册的事件发生,操作系统会回调并唤醒睡眠的选择器,选择器从睡眠中苏醒,向操作系统查询相应通道的事件发生状态,之后对自身的通道的Key集合中相应的位进行修改,然后返回服务器提供的执行逻辑,处理这些事件发生的可用的通道。简单的说Select就是将通道和通道关心的事件交给操作系统去监视,当相应的通道事件发生,在通知服务程序,处理这些可用的通道。在这个过程中,服务器需要提供一个线程作为Select线程,这个线程在监视期间处于睡眠状态(阻塞),在监视返回阶段作为处理线程。

要想深入了解这种Select监听机制,还必须了解SelectKey,就是键值。SelectKey实际提供的是通道与关注事件的一种关联。在每个SelectKey内部有一个interest(关心)事件集,事件集包括OP_ACCEPT(服务端接收到客户端请求)、OP_CONNECT(套接字连接成功)、OP_READ(通道有数据可读)、OP_WRITE(可用向通道中写入数据)这五种可用事件的一种或多种的组合。这样一个SelectKey就可以关联一个通道和这个通道所关心的事件。并且在SelectKey中还有一个集,用来存放已经发生的事件,称为ready集,这个集在任何时候都是interest集的一个子集,Select在事件发生后,会修改相应ready集的相应位,以标识这个事件已经发生。在Select中存在三种类型的SelectKey集合,分别是已注册键集,已选择键集和已取消键集。所有向选择器注册了的SelectKey(就是通道和其所关心的事件组合),已选择键集是那些关注事件发生了的SelectKey(就是通道关心的事件已经发生,并没有被处理,释放被选择状态的键),已取消键集就是那些用户取消了的键(一般通道被关闭,或者通过SelectKey直接调用cancle的键,这些键将不会再被监视,但不是将键立即取消)。

了解这些概念后,现在让我们来看看监视器一次具体的监视过程是怎样的。

线程调用select将会出现下列事件序列:

1. 检查已取消键集,如果键集非空,将已注册键集和已选择键集中与已取消键集中的交集SelectKey删除,之后清空已取消键集。

2. 检查已注册键集的所有SelectKey的interest集(关心事件集合),调用操作系统的相关操作,将通道与关心的事件注册给操作系统,操作系统负责对所有的通道和关心的事件进行监视。线程在此被阻塞。操作系统中有相应的事件发生,如果发生的事件的SelectKey尚未添加到已选择键集中,那么键的ready集将被清空,之后将对应该键的相应事件的比特位掩码将被设置;如果发生的事件的SelectKey已在已选择键集中,则新发生的事件的相应为比特掩码被设置(有可能这个键的这个位的掩码已经被设置过了,这是一种累积过程)。

3. 重新执行步骤1,将取消的键去除。

4. 执行相应事件的操作。

下面在Demo-2中提供一个简单的文件传输服务器的例子,这个例子给出了如何在web服务端使用NIO来提高服务的可连接性。

Demo-2:使用NIO的Web文件服务。

package com.upc.upcgrid.guan.chapter02;

import java.io.BufferedInputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.net.InetSocketAddress;

import java.net.Socket;

import java.net.UnknownHostException;

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.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

import org.junit.Test;

public class NIOFileTrans{

public static final int PORT =

21213;//端口号

//发送缓冲和接收缓冲区大小

public static final int

BUFFER_SIZE = 1024*1024;

//线程池大小

public static final int

MAX_POOL_SIZE = 6;

public class FileTransContext implements

Runnable{

public ByteBuffer buffer;//发送缓冲

public InputStream inputStream;//发送流(读取文件流)

public boolean isFinished = false;//发送成功标识

public SocketChannel channel;//套接字通道(输出流)

//当前发送线程任务(用于查询当前线程是否执行结束)

public Future> future;

//一次发送

public boolean sendFile() throws

IOException

{

//如果当前的buffer已经传送结束,则从inputStream中读取新的buffer

if(!buffer.hasRemaining())

{

if(isFinished)//如果文件已经发送完毕,直接返回

{

return true;//发送结束

}

getNextBuffer();//从文件中获取下一段数据

}

channel.write(buffer);//向socket写出数据

return false;//文件仍未发送完毕

}

private void getNextBuffer() throws

IOException

{

//获取一段新数据

int i = inputStream.read(buffer.array());

if(i == -1)//如果读到文件末尾

{

isFinished = true;//文件输出结束

}else

{

buffer.position(0);//重新组织缓冲区

buffer.limit(i);

}

}

@Override

public void run() {

try {

sendFile();

} catch (IOException e) {

e.printStackTrace();

}

}

}

public class ReadFileName implements Runnable

{

public SocketChannel channel;//数据将要传输的socket通道

public String fileName = "";//接收到的文件名

public ByteBuffer buffer;//输入缓冲区

public boolean isReadFinished =

false;//读取操作完成标识

public Future>

future;//用于判断当前线程状态

public SelectionKey key;//对应的选择键

public synchronized void run() {

buffer.clear();//清空缓冲区

try {

channel.read(buffer);//从通道中读取数据

buffer.flip();//将数据翻转,用于读出

if(buffer.array()[buffer.limit()-1] == -1)//接收到-1表示读到了末尾

{

fileName += new

String(buffer.array(),0,buffer.limit()-1);//组织字符串

isReadFinished = true;//读取结束

}else

{

fileName += new

String(buffer.array(),0,buffer.limit());//组织字符串

}

} catch (IOException e) {

}

}

}

@Test

public void service() throws IOException

{

ServerSocketChannel server =

ServerSocketChannel.open();//创建Server

server.socket().bind(new

InetSocketAddress(PORT));//绑定端口

server.configureBlocking(false);//设置非阻塞模式

ExecutorService pool =

Executors.newFixedThreadPool(MAX_POOL_SIZE); //创建线程池

Selector select = Selector.open(); //创建监听器

server.register(select,

SelectionKey.OP_ACCEPT);//将server的accept事件放入监听器

while(true)

{

select.select();//阻塞等待,新的事件到来

Iterator keys =

select.selectedKeys().iterator();

//Set keys =

select.selectedKeys();//获取发生的事件

//for(SelectionKey key:keys)//遍历处理事件

while(keys.hasNext())

{

SelectionKey key = keys.next();

if(key.isAcceptable())//如果是远程有请求连接

{

ServerSocketChannel ssc =

(ServerSocketChannel)key.channel();//获取服务

SocketChannel sc = ssc.accept();//创建远程的连接

sc.configureBlocking(false);//设置socket连接为非阻塞模式

sc.register(select,

SelectionKey.OP_READ);//将socket连接的读事件注册

}

if(key.isReadable())//发生了读事件

{

SocketChannel sc = (SocketChannel)key.channel();//获取socket

ReadFileName read =

(ReadFileName)key.attachment();//获取socket的环境参量

if(read == null)//如果环境参量为空,表示首次进行读取

{

read = new ReadFileName();//创建环境参量

read.buffer =

ByteBuffer.allocate(BUFFER_SIZE);//创建缓冲区

read.channel = sc;//将通道赋值到环境中

read.future = pool.submit(read);//将读取任务提交到线程池

read.key = key;//将当前key放入环境中

//注册写入事件,注意这时并没有真的可以写入信息,但是仍然需要提前注册这个事件

sc.register(select, SelectionKey.OP_WRITE);

key.attach(read);//向key添加环境属性

}else if(read.future.isDone()

&& read.isReadFinished ==

false)

{//表明,上次执行的读线程已经读取结束,但是并未读取传入信息的末尾

pool.submit(read);//再次执行新的线程,继续读取剩余数据

}

}

if(key.isWritable())//当通道可写

{

SocketChannel sc = (SocketChannel) key.channel();//获取socket

if(key.attachment() instanceof

ReadFileName)//如果环境是从客户端读文件名

{

ReadFileName read = (ReadFileName) key.attachment();//获取环境

if(read.isReadFinished)//如果读文件名操作已经结束,那么要生成写环境

{

BufferedInputStream inputStream = null; //创建文件输入流

try{

//初始化输入流

inputStream = new BufferedInputStream(new

FileInputStream(new File(read.fileName)));

}catch(IOException e)

{//出现异常要关闭socket

System.err.println("需要下载的文件不存在");

sc.close();

}

FileTransContext transContext = new

FileTransContext();//创建新的文件传输环境

transContext.buffer =

ByteBuffer.allocate(BUFFER_SIZE);

transContext.buffer.limit(0);

transContext.channel = sc;

transContext.inputStream =

inputStream;

key.attach(transContext);//将新环境与key相关

}

}else if(key.attachment() instanceof

FileTransContext)//当前环境是文件传送

{

FileTransContext context = (FileTransContext)

key.attachment();//获取环境

if(context.future == null)//第一次进行文件写操作

{

context.future = pool.submit(context);

}else if(context.future.isDone()

&&

!context.isFinished)//如果上次写操作结束,并且文件并没有传输完成

{

context.future = pool.submit(context);//进行下一个文件块的写入

}else

if(context.future.isDone())//如果上次写入已经结束,并且文件传输结束

{

sc.close();//关闭套接字

}

}

}

//keys.remove(key);//移除本次处理的键

keys.remove();

}

}

}

public static void main(String[] args)

throws IOException {

new NIOFileTrans().service();

}

@Test

public void client() throws

UnknownHostException, IOException

{

//文件要输出的位置

OutputStream out = new FileOutputStream(new

File("I:\\电影\\The.Lincoln.Lawyer.2011.R5.DVDRiP.XViD[林肯律师]test.rmvb"));

Socket socket = new Socket("localhost", PORT);

//获取文件输入流(网络服务器)

InputStream in = socket.getInputStream();

//获取网络服务器的输入流(客户端的输出流,用于输出想要获取的文件名)

OutputStream socketOut = socket.getOutputStream();

//写出需要读取的文件名

socketOut.write("I:\\电影\\The.Lincoln.Lawyer.2011.R5.DVDRiP.XViD[林肯律师].rmvb".getBytes());

socketOut.write(new byte[]{-1});//输出终止符

byte[] buffer = new

byte[BUFFER_SIZE];//新创建一个读入缓冲区

int i = 0;

while(true)

{

i = in.read(buffer);//从网络读入数据

if(i == -1)//读取到文件末尾

break;

out.write(buffer, 0, i);//向文件中写出

}

socket.close();//关闭套接字

out.close();//关闭文件

}

}

下面对demo-2中的代码进行简单分析。首先这个服务将服务器的accept事件交给了select来监听,当有客户请求到来时,服务端又将这个新的请求的读取事件交给了select事件,当客户端向服务器写入信息后,服务端即会变成可读状态,因此激发了服务端的新线程读取客户端传过来的文件名(如果文件名过长,那么将会有几个线程先后创建读取,这些子线程都是很短的,仅是读取一次缓冲),并注册了可writeable事件。writeable事件是在写出缓冲区空闲时触发的,因此在注册时,输出缓冲区即为可用,但此时文件名尚未读取结束,因此通过不同的环境来区分当前的执行状态。在整个过程中服务端使用了线程缓冲区机制,这种机制最适宜执行简短的线程任务。通过使用监听机制,在读取信息时,可用节省传统的服务端等待客户端输入和阻塞线程带来的代价,同时,写入时,仅在可写入时执行线程,节省了写入时缓冲区满阻塞等待的代价。一般在并发访问量巨大,需要减少系统中的阻塞线程数目,提高服务器的响应速度时,需要考虑使用NIO来提升服务器性能。此外,NIO技术需要与线程池技术结合使用才能达到事半功倍的效果。过于线程池可用参考我的相关博客,或者查看java的API。

此外,使用javaNIO技术编写网络服务端比较复杂,一般情况下,需要自己标记消息是否完全读取,有很多情况一条消息不能通过一次接收就完成(比如接收文件名,文件名此时很长),这样在服务端接收的时候,就需要对消息进行判断,看是否到达本条消息的末尾,当所有的消息段都到达后,在将整条消息拼接。这与传统方式不同,传统方式很简单,使用readUTF就可用读取一条消息。

在下一次提供一个使用NIO实现的echo系统。你可以查看其他博客。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值