本文来自DoubleH的BlogJava博客,原文标题为《基于JDK7 NIO2的高性能web服务器实践之二》。该主题的第一篇博文可在这里阅读。
51CTO推荐专题:Java 7 下一代Java开发技术详解
前一篇博客,我简单提了下怎么为NIO2增加TransmitFile支持,文件传送吞吐量是一个性能关注点,此外,并发连接数也是重要的关注点。
不过JDK7中又一次做了简单的实现,不支持同时投递多个AcceptEx请求,只支持一次一个,返回后再投递。这样,客户端连接的接受速度必然大打折扣。不知道为什么sun会做这样的实现,WSASend()/WSAReceive()一次只允许一个还是可以理解,毕竟简化了编程,不用考虑封包乱序问题。
也降低了内存耗尽的风险。AcceptEx却没有这样的理由了。
于是再一次为了性能,我增加了同时投递多个的支持。
另外,在JDK7的默认实现中,AcceptEx返回后,为了设置远程和本地InetSocketAddress也采用了效率很低的方法。4次通过JNI调用getsockname,2次为了取sockaddr,2次为了取port. 这些操作本人采用GetAcceptExSockaddrs一次完成,进一步提高效率。
先看Java部分的代码,框架跟JDK7的一样,细节处理不一样:
- /**
- *
- */
- package sun.nio.ch;
- import java.io.IOException;
- import java.lang.reflect.Field;
- import java.lang.reflect.Method;
- import java.net.InetAddress;
- import java.net.InetSocketAddress;
- import java.nio.channels.AcceptPendingException;
- import java.nio.channels.AsynchronousCloseException;
- import java.nio.channels.AsynchronousServerSocketChannel;
- import java.nio.channels.AsynchronousSocketChannel;
- import java.nio.channels.ClosedChannelException;
- import java.nio.channels.CompletionHandler;
- import java.nio.channels.NotYetBoundException;
- import java.nio.channels.ShutdownChannelGroupException;
- import java.security.AccessControlContext;
- import java.security.AccessController;
- import java.security.PrivilegedAction;
- import java.util.Queue;
- import java.util.concurrent.ConcurrentLinkedQueue;
- import java.util.concurrent.Future;
- import java.util.concurrent.atomic.AtomicBoolean;
- import java.util.concurrent.atomic.AtomicInteger;
- import sun.misc.Unsafe;
- /**
- * This class enable multiple 'AcceptEx' post on the completion port, hence improve the concurrent connection number.
- * @author Yvon
- *
- */
- public class WindowsMultiAcceptSupport {
- WindowsAsynchronousServerSocketChannelImpl schannel;
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- // 2 * (sizeof(SOCKET_ADDRESS) + 16)
- private static final int ONE_DATA_BUFFER_SIZE = 88;
- private long handle;
- private Iocp iocp;
- // typically there will be zero, or one I/O operations pending. In rare
- // cases there may be more. These rare cases arise when a sequence of accept
- // operations complete immediately and handled by the initiating thread.
- // The corresponding OVERLAPPED cannot be reused/released until the completion
- // event has been posted.
- private PendingIoCache ioCache;
- private Queue<Long> dataBuffers;
- // the data buffer to receive the local/remote socket address
- // private final long dataBuffer;
- private AtomicInteger pendingAccept;
- private int maxPending;
- Method updateAcceptContextM;
- Method acceptM;
- WindowsMultiAcceptSupport() {
- //dummy for JNI code.
- }
- public void close() throws IOException {
- schannel.close();
- for (int i = 0; i < maxPending + 1; i++)//assert there is maxPending+1 buffer in the queue
- {
- long addr = dataBuffers.poll();
- // release resources
- unsafe.freeMemory(addr);
- }
- }
- /**
- *
- */
- public WindowsMultiAcceptSupport(AsynchronousServerSocketChannel ch, int maxPost) {
- if (maxPost <= 0 || maxPost > 1024)
- throw new IllegalStateException("maxPost can't less than 1 and greater than 1024");
- this.schannel = (WindowsAsynchronousServerSocketChannelImpl) ch;
- maxPending = maxPost;
- dataBuffers = new ConcurrentLinkedQueue<Long>();
- for (int i = 0; i < maxPending + 1; i++) {
- dataBuffers.add(unsafe.allocateMemory(ONE_DATA_BUFFER_SIZE));
- }
- pendingAccept = new AtomicInteger(0);
- try {
- Field f = WindowsAsynchronousServerSocketChannelImpl.class.getDeclaredField("handle");
- f.setAccessible(true);
- handle = f.getLong(schannel);
- f = WindowsAsynchronousServerSocketChannelImpl.class.getDeclaredField("iocp");
- f.setAccessible(true);
- iocp = (Iocp) f.get(schannel);
- f = WindowsAsynchronousServerSocketChannelImpl.class.getDeclaredField("ioCache");
- f.setAccessible(true);
- ioCache = (PendingIoCache) f.get(schannel);
- f = WindowsAsynchronousServerSocketChannelImpl.class.getDeclaredField("accepting");
- f.setAccessible(true);
- AtomicBoolean accepting = (AtomicBoolean) f.get(schannel);
- accepting.set(true);//disable accepting by origin channel.
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- @SuppressWarnings("unchecked")
- public final <A> void accept(A attachment,
- CompletionHandler<AsynchronousSocketChannel, ? super A> handler) {
- if (handler == null)
- throw new NullPointerException("'handler' is null");
- implAccept(attachment, (CompletionHandler<AsynchronousSocketChannel, Object>) handler);
- }
- /**
- * Task to initiate accept operation and to handle result.
- */
- private class AcceptTask implements Runnable, Iocp.ResultHandler {
- private final WindowsAsynchronousSocketChannelImpl channel;
- private final AccessControlContext acc;
- private final PendingFuture<AsynchronousSocketChannel, Object> result;
- private final long dataBuffer;
- AcceptTask(WindowsAsynchronousSocketChannelImpl channel, AccessControlContext acc,
- long dataBuffer, PendingFuture<AsynchronousSocketChannel, Object> result) {
- this.channel = channel;
- this.acc = acc;
- this.result = result;
- this.dataBuffer = dataBuffer;
- }
- void enableAccept() {
- pendingAccept.decrementAndGet();
- dataBuffers.add(dataBuffer);
- }
- void closeChildChannel() {
- try {
- channel.close();
- } catch (IOException ignore) {
- }
- }
- // caller must have acquired read lock for the listener and child channel.
- void finishAccept() throws IOException {
- /**
- * JDK7 use 4 calls to getsockname to setup
- * local& remote address, this is very inefficient.
- *
- * I change this to use GetAcceptExSockaddrs
- */
- InetAddress[] socks = new InetAddress[2];
- int[] ports = new int[2];
- updateAcceptContext(handle, channel.handle(), socks, ports, dataBuffer);
- InetSocketAddress local = new InetSocketAddress(socks[0], ports[0]);
- final InetSocketAddress remote = new InetSocketAddress(socks[1], ports[1]);
- channel.setConnected(local, remote);
- // permission check (in context of initiating thread)
- if (acc != null) {
- AccessController.doPrivileged(new PrivilegedAction<Void>() {
- public Void run() {
- SecurityManager sm = System.getSecurityManager();
- sm.checkAccept(remote.getAddress().getHostAddress(), remote.getPort());
- return null;
- }
- }, acc);
- }
- }
- /**
- * Initiates the accept operation.
- */
- @Override
- public void run() {
- long overlapped = 0L;
- try {
- // begin usage of listener socket
- schannel.begin();
- try {
- // begin usage of child socket (as it is registered with
- // completion port and so may be closed in the event that
- // the group is forcefully closed).
- channel.begin();
- synchronized (result) {
- overlapped = ioCache.add(result);
- int n = accept0(handle, channel.handle(), overlapped, dataBuffer);//Be careful for the buffer address
- if (n == IOStatus.UNAVAILABLE) {
- return;
- }
- // connection accepted immediately
- finishAccept();
- // allow another accept before the result is set
- enableAccept();
- result.setResult(channel);
- }
- } finally {
- // end usage on child socket
- channel.end();
- }
- } catch (Throwable x) {
- // failed to initiate accept so release resources
- if (overlapped != 0L)
- ioCache.remove(overlapped);
- closeChildChannel();
- if (x instanceof ClosedChannelException)
- x = new AsynchronousCloseException();
- if (!(x instanceof IOException) && !(x instanceof SecurityException))
- x = new IOException(x);
- enableAccept();
- result.setFailure(x);
- } finally {
- // end of usage of listener socket
- schannel.end();
- }
- // accept completed immediately but may not have executed on
- // initiating thread in which case the operation may have been
- // cancelled.
- if (result.isCancelled()) {
- closeChildChannel();
- }
- // invoke completion handler
- Invoker.invokeIndirectly(result);
- }
- /**
- * Executed when the I/O has completed
- */
- @Override
- public void completed(int bytesTransferred, boolean canInvokeDirect) {
- try {
- // connection accept after group has shutdown
- if (iocp.isShutdown()) {
- throw new IOException(new ShutdownChannelGroupException());
- }
- // finish the accept
- try {
- schannel.begin();
- try {
- channel.begin();
- finishAccept();
- } finally {
- channel.end();
- }
- } finally {
- schannel.end();
- }
- // allow another accept before the result is set
- enableAccept();
- result.setResult(channel);
- } catch (Throwable x) {
- enableAccept();
- closeChildChannel();
- if (x instanceof ClosedChannelException)
- x = new AsynchronousCloseException();
- if (!(x instanceof IOException) && !(x instanceof SecurityException))
- x = new IOException(x);
- result.setFailure(x);
- }
- // if an async cancel has already cancelled the operation then
- // close the new channel so as to free resources
- if (result.isCancelled()) {
- closeChildChannel();
- }
- // invoke handler (but not directly)
- Invoker.invokeIndirectly(result);
- }
- @Override
- public void failed(int error, IOException x) {
- enableAccept();
- closeChildChannel();
- // release waiters
- if (schannel.isOpen()) {
- result.setFailure(x);
- } else {
- result.setFailure(new AsynchronousCloseException());
- }
- Invoker.invokeIndirectly(result);
- }
- }
- Future<AsynchronousSocketChannel> implAccept(Object attachment,
- final CompletionHandler<AsynchronousSocketChannel, Object> handler) {
- if (!schannel.isOpen()) {
- Throwable exc = new ClosedChannelException();
- if (handler == null)
- return CompletedFuture.withFailure(exc);
- Invoker.invokeIndirectly(schannel, handler, attachment, null, exc);
- return null;
- }
- if (schannel.isAcceptKilled())
- throw new RuntimeException("Accept not allowed due to cancellation");
- // ensure channel is bound to local address
- if (schannel.localAddress == null)
- throw new NotYetBoundException();
- // create the socket that will be accepted. The creation of the socket
- // is enclosed by a begin/end for the listener socket to ensure that
- // we check that the listener is open and also to prevent the I/O
- // port from being closed as the new socket is registered.
- WindowsAsynchronousSocketChannelImpl ch = null;
- IOException ioe = null;
- try {
- schannel.begin();
- ch = new WindowsAsynchronousSocketChannelImpl(iocp, false);
- } catch (IOException x) {
- ioe = x;
- } finally {
- schannel.end();
- }
- if (ioe != null) {
- if (handler == null)
- return CompletedFuture.withFailure(ioe);
- Invoker.invokeIndirectly(this.schannel, handler, attachment, null, ioe);
- return null;
- }
- // need calling context when there is security manager as
- // permission check may be done in a different thread without
- // any application call frames on the stack
- AccessControlContext acc =
- (System.getSecurityManager() == null) ? null : AccessController.getContext();
- PendingFuture<AsynchronousSocketChannel, Object> result =
- new PendingFuture<AsynchronousSocketChannel, Object>(schannel, handler, attachment);
- // check and set flag to prevent concurrent accepting
- if (pendingAccept.get() >= maxPending)
- throw new AcceptPendingException();
- pendingAccept.incrementAndGet();
- AcceptTask task = new AcceptTask(ch, acc, dataBuffers.poll(), result);
- result.setContext(task);
- // initiate I/O
- if (Iocp.supportsThreadAgnosticIo()) {
- task.run();
- } else {
- Invoker.invokeOnThreadInThreadPool(this.schannel, task);
- }
- return result;
- }
- // //reimplements for performance
- static native void updateAcceptContext(long listenSocket, long acceptSocket,
- InetAddress[] addresses, int[] ports, long dataBuffer) throws IOException;
- static native int accept0(long handle, long handle2, long overlapped, long dataBuffer);
- }
对应的CPP代码如下:
- /*
- * Class: sun_nio_ch_WindowsMultiAcceptSupport
- * Method: updateAcceptContext
- * Signature: (JJ[Ljava/net/InetAddress;[IJ)V
- */
- JNIEXPORT void JNICALL Java_sun_nio_ch_WindowsMultiAcceptSupport_updateAcceptContext
- (JNIEnv *env , jclass clazz, jlong listenSocket, jlong acceptSocket, jobjectArray sockArray,jintArray portArray,jlong buf)
- {
- SOCKET s1 = (SOCKET)jlong_to_ptr(listenSocket);
- SOCKET s2 = (SOCKET)jlong_to_ptr(acceptSocket);
- PVOID outputBuffer = (PVOID)jlong_to_ptr(buf);
- INT iLocalAddrLen=0;
- INT iRemoteAddrLen=0;
- SOCKETADDRESS* lpLocalAddr;
- SOCKETADDRESS* lpRemoteAddr;
- jobject localAddr;
- jobject remoteAddr;
- jint ports[2]={0};
- setsockopt(s2, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char *)&s1, sizeof(s1));
- (lpGetAcceptExSockaddrs)(outputBuffer,
- 0,
- sizeof(SOCKETADDRESS)+16,
- sizeof(SOCKETADDRESS)+16,
- (LPSOCKADDR*)&lpLocalAddr,
- &iLocalAddrLen,
- (LPSOCKADDR*)&lpRemoteAddr,
- &iRemoteAddrLen);
- localAddr=lpNET_SockaddrToInetAddress(env,(struct sockaddr *)lpLocalAddr,(int *)ports);
- remoteAddr=lpNET_SockaddrToInetAddress(env,(struct sockaddr *)lpRemoteAddr,(int *)(ports+1));
- env->SetObjectArrayElement(sockArray,0,localAddr);
- env->SetObjectArrayElement(sockArray,1,remoteAddr);
- env->SetIntArrayRegion(portArray,0,2,ports);
- }
- /*
- * Class: sun_nio_ch_WindowsMultiAcceptSupport
- * Method: accept0
- * Signature: (JJJJ)I
- */
- jint JNICALL Java_sun_nio_ch_WindowsMultiAcceptSupport_accept0
- (JNIEnv *env, jclass clazz, jlong listenSocket, jlong acceptSocket, jlong ov, jlong buf)
- {
- BOOL res;
- SOCKET s1 = (SOCKET)jlong_to_ptr(listenSocket);
- SOCKET s2 = (SOCKET)jlong_to_ptr(acceptSocket);
- PVOID outputBuffer = (PVOID)jlong_to_ptr(buf);
- DWORD nread = 0;
- OVERLAPPED* lpOverlapped = (OVERLAPPED*)jlong_to_ptr(ov);
- ZeroMemory((PVOID)lpOverlapped, sizeof(OVERLAPPED));
- //why use SOCKETADDRESS?
- //because client may use IPv6 to connect to server.
- res = (lpAcceptEx)(s1,
- s2,
- outputBuffer,
- 0,
- sizeof(SOCKETADDRESS)+16,
- sizeof(SOCKETADDRESS)+16,
- &nread,
- lpOverlapped);
- if (res == 0) {
- int error = WSAGetLastError();
- if (error == ERROR_IO_PENDING) {
- return NIO2_IOS_UNAVAILABLE;
- }
- return NIO2_THROWN;
- }
- return 0;
- }
这里用到的lpNET_SockaddrToInetAddress是JDK7中NET.DLL暴露的方法,从DLL里加载。相应代码如下:
- *
- * Class: com_yovn_jabhttpd_utilities_SunPackageFixer
- * Method: initFds
- * Signature: ()V
- */
- JNIEXPORT void JNICALL Java_com_yovn_jabhttpd_utilities_SunPackageFixer_initFds
- (JNIEnv *env, jclass clazz)
- {
- GUID GuidAcceptEx = WSAID_ACCEPTEX;
- GUID GuidTransmitFile = WSAID_TRANSMITFILE;
- GUID GuidGetAcceptExSockAddrs = WSAID_GETACCEPTEXSOCKADDRS;
- SOCKET s;
- int rv;
- DWORD dwBytes;
- HMODULE hModule;
- s = socket(AF_INET, SOCK_STREAM, 0);
- if (s == INVALID_SOCKET) {
- JNU_ThrowByName(env,"java/io/IOException", "socket failed");
- return;
- }
- rv = WSAIoctl(s,
- SIO_GET_EXTENSION_FUNCTION_POINTER,
- (LPVOID)&GuidAcceptEx,
- sizeof(GuidAcceptEx),
- &lpAcceptEx,
- sizeof(lpAcceptEx),
- &dwBytes,
- NULL,
- NULL);
- if (rv != 0)
- {
- JNU_ThrowByName(env, "java/io/IOException","WSAIoctl failed on get AcceptEx ");
- goto _ret;
- }
- rv = WSAIoctl(s,
- SIO_GET_EXTENSION_FUNCTION_POINTER,
- (LPVOID)&GuidTransmitFile,
- sizeof(GuidTransmitFile),
- &lpTransmitFile,
- sizeof(lpTransmitFile),
- &dwBytes,
- NULL,
- NULL);
- if (rv != 0)
- {
- JNU_ThrowByName(env, "java/io/IOException","WSAIoctl failed on get TransmitFile");
- goto _ret;
- }
- rv = WSAIoctl(s,
- SIO_GET_EXTENSION_FUNCTION_POINTER,
- (LPVOID)&GuidGetAcceptExSockAddrs,
- sizeof(GuidGetAcceptExSockAddrs),
- &lpGetAcceptExSockaddrs,
- sizeof(lpGetAcceptExSockaddrs),
- &dwBytes,
- NULL,
- NULL);
- if (rv != 0)
- {
- JNU_ThrowByName(env, "java/io/IOException","WSAIoctl failed on get GetAcceptExSockaddrs");
- goto _ret;
- }
- hModule=LoadLibrary("net.dll");
- if(hModule==NULL)
- {
- JNU_ThrowByName(env, "java/io/IOException","can't load java net.dll");
- goto _ret;
- }
- lpNET_SockaddrToInetAddress=(NET_SockaddrToInetAddress_t)GetProcAddress(hModule,"_NET_SockaddrToInetAddress@12");
- if(lpNET_SockaddrToInetAddress==NULL)
- {
- JNU_ThrowByName(env, "java/io/IOException","can't resolve _NET_SockaddrToInetAddress function ");
- }
- _ret:
- closesocket(s);
- return;
- }
#T#细心的同学可能会发现,在创建socket之前没有初始化WinSock库,因为在这段代码前,我初始化了一个InetSocketAddress对象,这样JVM会加载NET.DLL并初始化WinSock库了。
OK,现在,你可以在支持类上同时发起多个AcceptEx请求了。
PS:基于这个我简单测试了下我的服务器,同时开5000个线程,每个下载3M多点的文件,一分钟内能够全部正确完成。
服务器正在开发中,有兴趣的请加入:http://code.google.com/p/jabhttpd